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 }