github.com/GoogleContainerTools/skaffold/v2@v2.13.2/pkg/webhook/kubernetes/deployment.go (about)

     1  /*
     2  Copyright 2019 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package kubernetes
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"log"
    23  	"net/http"
    24  	"os/exec"
    25  	"path"
    26  	"time"
    27  
    28  	"github.com/google/go-github/github"
    29  	appsv1 "k8s.io/api/apps/v1"
    30  	v1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  
    34  	pkgkubernetes "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes"
    35  	kubernetesclient "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes/client"
    36  	"github.com/GoogleContainerTools/skaffold/v2/pkg/webhook/constants"
    37  	"github.com/GoogleContainerTools/skaffold/v2/pkg/webhook/labels"
    38  )
    39  
    40  const (
    41  	initContainerName = "git-clone"
    42  	emptyVol          = "empty-vol"
    43  	emptyVolPath      = "/empty"
    44  )
    45  
    46  // CreateDeployment creates a deployment for this pull request
    47  // The deployment has two containers:
    48  //  1. An init container to git clone the PR branch
    49  //  2. A container to run hugo server
    50  //
    51  // and one emptyDir volume to hold the git repository
    52  func CreateDeployment(pr *github.PullRequestEvent, svc *v1.Service, externalIP string) (*appsv1.Deployment, error) {
    53  	client, err := kubernetesclient.DefaultClient()
    54  	if err != nil {
    55  		return nil, fmt.Errorf("getting Kubernetes client: %w", err)
    56  	}
    57  
    58  	deploymentLabels := svc.Spec.Selector
    59  	_, name := labels.RetrieveLabel(pr.GetNumber())
    60  
    61  	userRepo := fmt.Sprintf("https://github.com/%s.git", *pr.PullRequest.Head.Repo.FullName)
    62  	// path to the docs directory, which we will run "hugo server -D" in
    63  	repoPath := path.Join(emptyVolPath, *pr.PullRequest.Head.Repo.Name)
    64  
    65  	d := &appsv1.Deployment{
    66  		ObjectMeta: metav1.ObjectMeta{
    67  			Name:   name,
    68  			Labels: deploymentLabels,
    69  		},
    70  		Spec: appsv1.DeploymentSpec{
    71  			Selector: &metav1.LabelSelector{
    72  				MatchLabels: deploymentLabels,
    73  			},
    74  			Template: v1.PodTemplateSpec{
    75  				ObjectMeta: metav1.ObjectMeta{
    76  					Labels: deploymentLabels,
    77  				},
    78  				Spec: v1.PodSpec{
    79  					InitContainers: []v1.Container{
    80  						{
    81  							Name:       initContainerName,
    82  							Image:      constants.DeploymentImage,
    83  							Args:       []string{"git", "clone", userRepo, "--branch", pr.PullRequest.Head.GetRef()},
    84  							WorkingDir: emptyVolPath,
    85  							VolumeMounts: []v1.VolumeMount{
    86  								{
    87  									Name:      emptyVol,
    88  									MountPath: emptyVolPath,
    89  								},
    90  							},
    91  						},
    92  					},
    93  					Containers: []v1.Container{
    94  						{
    95  							Name:       "server",
    96  							Image:      constants.DeploymentImage,
    97  							Args:       []string{fmt.Sprintf("deploy/docs%s/preview.sh", constants.DocsVersion), BaseURL(externalIP)},
    98  							WorkingDir: repoPath,
    99  							VolumeMounts: []v1.VolumeMount{
   100  								{
   101  									Name:      emptyVol,
   102  									MountPath: emptyVolPath,
   103  								},
   104  							},
   105  							Ports: []v1.ContainerPort{
   106  								{
   107  									ContainerPort: constants.HugoPort,
   108  								},
   109  							},
   110  						},
   111  					},
   112  					Volumes: []v1.Volume{
   113  						{
   114  							Name: emptyVol,
   115  							VolumeSource: v1.VolumeSource{
   116  								EmptyDir: &v1.EmptyDirVolumeSource{},
   117  							},
   118  						},
   119  					},
   120  				},
   121  			},
   122  		},
   123  	}
   124  	return client.AppsV1().Deployments(constants.Namespace).Create(context.Background(), d, metav1.CreateOptions{})
   125  }
   126  
   127  // WaitForDeploymentToStabilize waits till the Deployment has stabilized
   128  func WaitForDeploymentToStabilize(d *appsv1.Deployment, ip string) error {
   129  	client, err := kubernetesclient.DefaultClient()
   130  	if err != nil {
   131  		return fmt.Errorf("getting Kubernetes client: %w", err)
   132  	}
   133  
   134  	if err := pkgkubernetes.WaitForDeploymentToStabilize(context.Background(), client, d.Namespace, d.Name, 5*time.Minute); err != nil {
   135  		return fmt.Errorf("waiting for deployment to stabilize: %w", err)
   136  	}
   137  
   138  	// wait up to five minutes for the URL to return a valid endpoint
   139  	url := BaseURL(ip)
   140  	log.Printf("Waiting up to 2 minutes for %s to return an OK response...", url)
   141  	return wait.PollImmediate(5*time.Second, 2*time.Minute, func() (bool, error) {
   142  		resp, err := http.Get(url)
   143  		if err != nil {
   144  			return false, nil
   145  		}
   146  		defer resp.Body.Close()
   147  		return resp.StatusCode == http.StatusOK, nil
   148  	})
   149  }
   150  
   151  // BaseURL returns the base url of the deployment
   152  func BaseURL(ip string) string {
   153  	return fmt.Sprintf("http://%s:%d", ip, constants.HugoPort)
   154  }
   155  
   156  // Logs returns the logs for both containers for the given deployment
   157  func Logs(d *appsv1.Deployment) string {
   158  	deploy := fmt.Sprintf("deployment/%s", d.Name)
   159  	// get init container logs
   160  	cmd := exec.Command("kubectl", "logs", deploy, "-c", initContainerName)
   161  	initLogs, err := cmd.CombinedOutput()
   162  	if err != nil {
   163  		log.Printf("Error retrieving init container logs for %s: %v", d.Name, err)
   164  	}
   165  	// get deployment logs
   166  	cmd = exec.Command("kubectl", "logs", deploy)
   167  	logs, err := cmd.CombinedOutput()
   168  	if err != nil {
   169  		log.Printf("Error retrieving deployment logs for %s: %v", d.Name, err)
   170  	}
   171  	return fmt.Sprintf("Init container logs: \n %s \nContainer Logs: \n %s", initLogs, logs)
   172  }