github.com/grahambrereton-form3/tilt@v0.10.18/integration/k8s_fixture_test.go (about) 1 //+build integration 2 3 package integration 4 5 import ( 6 "bytes" 7 "context" 8 "encoding/base64" 9 "fmt" 10 "io/ioutil" 11 "log" 12 "net/http" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "strings" 17 "testing" 18 "time" 19 20 "github.com/pkg/errors" 21 v1 "k8s.io/api/core/v1" 22 23 "github.com/windmilleng/tilt/internal/testutils/tempdir" 24 ) 25 26 var k8sInstalled bool 27 28 type k8sFixture struct { 29 *fixture 30 tempDir *tempdir.TempDirFixture 31 32 token string 33 cert string 34 kubeconfigPath string 35 } 36 37 func newK8sFixture(t *testing.T, dir string) *k8sFixture { 38 f := newFixture(t, dir) 39 td := tempdir.NewTempDirFixture(t) 40 41 kf := &k8sFixture{fixture: f, tempDir: td} 42 43 if !k8sInstalled { 44 kf.checkKubectlConnection() 45 46 // Delete the namespace when the test starts, 47 // to make sure nothing is left over from previous tests. 48 kf.deleteNamespace() 49 50 k8sInstalled = true 51 } else { 52 kf.ClearNamespace() 53 } 54 55 return kf 56 } 57 58 func (f *k8sFixture) checkKubectlConnection() { 59 cmd := exec.CommandContext(f.ctx, "kubectl", "version") 60 f.runOrFail(cmd, "Checking kubectl connection") 61 } 62 63 func (f *k8sFixture) deleteNamespace() { 64 cmd := exec.CommandContext(f.ctx, "kubectl", "delete", "namespace", "tilt-integration", "--ignore-not-found") 65 f.runOrFail(cmd, "Deleting namespace tilt-integration") 66 67 // block until the namespace doesn't exist, since kubectl often returns and the namespace is still "terminating" 68 // which causes the creation of objects in that namespace to fail 69 var b []byte 70 args := []string{"kubectl", "get", "namespace", "tilt-integration", "--ignore-not-found"} 71 timeout := time.Now().Add(10 * time.Second) 72 for time.Now().Before(timeout) { 73 cmd := exec.CommandContext(f.ctx, args[0], args[1:]...) 74 b, err := cmd.Output() 75 if err != nil { 76 f.t.Fatalf("Error: checking that deletion of the tilt-integration namespace has completed: %v", err) 77 } 78 if len(b) == 0 { 79 return 80 } 81 } 82 f.t.Fatalf("timed out waiting for tilt-integration deletion to complete. last output of %q: %q", args, string(b)) 83 } 84 85 func (f *k8sFixture) Curl(url string) (string, error) { 86 resp, err := http.Get(url) 87 if err != nil { 88 return "", errors.Wrap(err, "Curl") 89 } 90 defer resp.Body.Close() 91 92 if resp.StatusCode != 200 { 93 f.t.Errorf("Error fetching %s: %s", url, resp.Status) 94 } 95 96 body, err := ioutil.ReadAll(resp.Body) 97 if err != nil { 98 return "", errors.Wrap(err, "Curl") 99 } 100 return string(body), nil 101 } 102 103 func (f *k8sFixture) CurlUntil(ctx context.Context, url string, expectedContents string) { 104 f.WaitUntil(ctx, fmt.Sprintf("curl(%s)", url), func() (string, error) { 105 return f.Curl(url) 106 }, expectedContents) 107 } 108 109 // Waits until all pods matching the selector are ready (i.e. phase = "Running") 110 // At least one pod must match. 111 // Returns the names of the ready pods. 112 func (f *k8sFixture) WaitForAllPodsReady(ctx context.Context, selector string) []string { 113 return f.WaitForAllPodsInPhase(ctx, selector, v1.PodRunning) 114 } 115 116 func (f *k8sFixture) WaitForAllPodsInPhase(ctx context.Context, selector string, phase v1.PodPhase) []string { 117 for { 118 allPodsReady, output, podNames := f.AllPodsInPhase(ctx, selector, phase) 119 if allPodsReady { 120 return podNames 121 } 122 123 select { 124 case <-f.activeTiltDone(): 125 f.t.Fatalf("Tilt died while waiting for pods to be ready: %v", f.activeTiltErr()) 126 case <-ctx.Done(): 127 f.t.Fatalf("Timed out waiting for pods to be ready. Selector: %s. Output:\n:%s\n", selector, output) 128 case <-time.After(200 * time.Millisecond): 129 } 130 } 131 } 132 133 // Checks that all pods are in the given phase 134 // Returns the output (for diagnostics) and the name of the pods in the given phase. 135 func (f *k8sFixture) AllPodsInPhase(ctx context.Context, selector string, phase v1.PodPhase) (bool, string, []string) { 136 cmd := exec.Command("kubectl", "get", "pods", 137 namespaceFlag, "--selector="+selector, "-o=template", 138 "--template", "{{range .items}}{{.metadata.name}} {{.status.phase}}{{println}}{{end}}") 139 out, err := cmd.CombinedOutput() 140 if err != nil { 141 f.t.Fatal(errors.Wrap(err, "get pods")) 142 } 143 144 outStr := string(out) 145 lines := strings.Split(outStr, "\n") 146 podNames := []string{} 147 hasOneMatchingPod := false 148 for _, line := range lines { 149 line = strings.TrimSpace(line) 150 if line == "" { 151 continue 152 } 153 154 elements := strings.Split(line, " ") 155 if len(elements) < 2 { 156 f.t.Fatalf("Unexpected output of kubect get pods: %s", outStr) 157 } 158 159 name, actualPhase := elements[0], elements[1] 160 var matchedPhase bool 161 if actualPhase == string(phase) { 162 matchedPhase = true 163 hasOneMatchingPod = true 164 165 } 166 167 if !matchedPhase { 168 return false, outStr, nil 169 } 170 171 podNames = append(podNames, name) 172 } 173 return hasOneMatchingPod, outStr, podNames 174 } 175 176 func (f *k8sFixture) ForwardPort(name string, portMap string) { 177 cmd := exec.CommandContext(f.ctx, "kubectl", "port-forward", namespaceFlag, name, portMap) 178 cmd.Stdout = os.Stdout 179 cmd.Stderr = os.Stdout 180 err := cmd.Start() 181 if err != nil { 182 f.t.Fatal(err) 183 } 184 185 f.cmds = append(f.cmds, cmd) 186 go func() { 187 err := cmd.Wait() 188 if err != nil && !f.tearingDown { 189 fmt.Printf("port forward failed: %v\n", err) 190 } 191 }() 192 } 193 194 func (f *k8sFixture) ClearResource(name string) { 195 outWriter := bytes.NewBuffer(nil) 196 cmd := exec.CommandContext(f.ctx, "kubectl", "delete", name, namespaceFlag, "--all") 197 cmd.Stdout = outWriter 198 cmd.Stderr = outWriter 199 err := cmd.Run() 200 if err != nil { 201 f.t.Fatalf("Error deleting deployments: %v. Logs:\n%s", err, outWriter.String()) 202 } 203 } 204 205 func (f *k8sFixture) ClearNamespace() { 206 f.ClearResource("jobs") 207 f.ClearResource("deployments") 208 f.ClearResource("services") 209 } 210 211 func (f *k8sFixture) setupNewKubeConfig() { 212 cmd := exec.CommandContext(f.ctx, "kubectl", "config", "view", "--minify") 213 current, err := cmd.Output() 214 if err != nil { 215 f.t.Fatalf("Error reading KUBECONFIG: %v", err) 216 } 217 218 // Create a file with the same basename as the current kubeconfig, 219 // because we sometimes use that for env detection. 220 kubeconfigBaseName := filepath.Base(os.Getenv("KUBECONFIG")) 221 if kubeconfigBaseName == "" || kubeconfigBaseName == "." { 222 kubeconfigBaseName = "config" 223 } 224 f.kubeconfigPath = f.tempDir.JoinPath(kubeconfigBaseName) 225 f.tempDir.WriteFile(f.kubeconfigPath, string(current)) 226 f.fixture.tilt.Environ["KUBECONFIG"] = f.kubeconfigPath 227 log.Printf("New kubeconfig: %s", f.kubeconfigPath) 228 } 229 230 func (f *k8sFixture) runCommand(name string, arg ...string) (*bytes.Buffer, error) { 231 outWriter := bytes.NewBuffer(nil) 232 cmd := exec.CommandContext(f.ctx, name, arg...) 233 cmd.Stdout = outWriter 234 cmd.Stderr = outWriter 235 cmd.Dir = packageDir 236 if f.kubeconfigPath != "" { 237 cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", f.kubeconfigPath)) 238 } 239 err := cmd.Run() 240 return outWriter, err 241 } 242 243 func (f *k8sFixture) runCommandSilently(name string, arg ...string) { 244 _, err := f.runCommand(name, arg...) 245 if err != nil { 246 f.t.Fatalf("Error running command silently %s %v: %v", name, arg, err) 247 } 248 } 249 250 func (f *k8sFixture) runCommandGetOutput(cmdStr string) string { 251 outWriter := bytes.NewBuffer(nil) 252 cmd := exec.CommandContext(f.ctx, "bash", "-c", cmdStr) 253 cmd.Stderr = outWriter 254 cmd.Dir = packageDir 255 if f.kubeconfigPath != "" { 256 cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", f.kubeconfigPath)) 257 } 258 output, err := cmd.Output() 259 if err != nil { 260 f.t.Fatalf("Error running command with output %s: %v", cmdStr, err) 261 } 262 263 return strings.TrimSpace(string(output)) 264 } 265 266 func (f *k8sFixture) getSecrets() { 267 cmdStr := `kubectl get secrets -n tilt-integration -o json | jq -r '.items[] | select(.metadata.name | startswith("tilt-integration-user-token-")) | .data.token'` 268 tokenBase64 := f.runCommandGetOutput(cmdStr) 269 tokenBytes, err := base64.StdEncoding.DecodeString(tokenBase64) 270 if err != nil { 271 f.t.Fatalf("Unable to decode token: %v", err) 272 } 273 274 cmdStr = `kubectl get secrets -n tilt-integration -o json | jq -r '.items[] | select(.metadata.name | startswith("tilt-integration-user-token-")) | .data["ca.crt"]'` 275 cert := f.runCommandGetOutput(cmdStr) 276 277 f.token = string(tokenBytes) 278 f.cert = cert 279 } 280 281 func (f *k8sFixture) SetRestrictedCredentials() { 282 // docker-for-desktop has a default binding that gives service accounts access to everything. 283 // See: https://github.com/docker/for-mac/issues/3694 284 f.runCommandSilently("kubectl", "delete", "clusterrolebinding", "docker-for-desktop-binding", "--ignore-not-found") 285 286 // The service account needs the namespace to exist. 287 f.runCommandSilently("kubectl", "apply", "-f", "namespace.yaml") 288 f.runCommandSilently("kubectl", "apply", "-f", "service-account.yaml") 289 f.runCommandSilently("kubectl", "apply", "-f", "access.yaml") 290 f.getSecrets() 291 292 f.setupNewKubeConfig() 293 294 f.runCommandSilently("kubectl", "config", "set-credentials", "tilt-integration-user", fmt.Sprintf("--token=%s", f.token)) 295 f.runCommandSilently("kubectl", "config", "set", "users.tilt-integration-user.client-key-data", f.cert) 296 297 currentContext := f.runCommandGetOutput("kubectl config current-context") 298 299 f.runCommandSilently("kubectl", "config", "set-context", currentContext, "--user=tilt-integration-user", "--namespace=tilt-integration") 300 301 cmdStr := fmt.Sprintf(`kubectl config view -o json | jq -r '.contexts[] | select(.name == "%s") | .context.cluster'`, currentContext) 302 currentCluster := f.runCommandGetOutput(cmdStr) 303 304 f.runCommandSilently("kubectl", "config", "set", fmt.Sprintf("clusters.%s.certificate-authority-data", currentCluster), f.cert) 305 f.runCommandSilently("kubectl", "config", "unset", fmt.Sprintf("clusters.%s.certificate-authority", currentCluster)) 306 } 307 308 func (f *k8sFixture) TearDown() { 309 f.StartTearDown() 310 f.ClearNamespace() 311 f.fixture.TearDown() 312 f.tempDir.TearDown() 313 }