github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/topgun/k8s/k8s_suite_test.go (about) 1 package k8s_test 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "math/rand" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strconv" 15 "testing" 16 "time" 17 18 "github.com/caarlos0/env" 19 "github.com/onsi/gomega/gbytes" 20 "github.com/onsi/gomega/gexec" 21 corev1 "k8s.io/api/core/v1" 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 "k8s.io/client-go/kubernetes" 24 "k8s.io/client-go/tools/clientcmd" 25 26 . "github.com/pf-qiu/concourse/v6/topgun" 27 . "github.com/onsi/ginkgo" 28 . "github.com/onsi/gomega" 29 30 _ "k8s.io/client-go/plugin/pkg/client/auth" 31 ) 32 33 func TestK8s(t *testing.T) { 34 RegisterFailHandler(Fail) 35 RunSpecs(t, "K8s Suite") 36 } 37 38 type environment struct { 39 HelmChartsDir string `env:"HELM_CHARTS_DIR,required"` 40 ConcourseChartDir string `env:"CONCOURSE_CHART_DIR,required"` 41 ConcourseImageDigest string `env:"CONCOURSE_IMAGE_DIGEST"` 42 ConcourseImageName string `env:"CONCOURSE_IMAGE_NAME,required"` 43 ConcourseImageTag string `env:"CONCOURSE_IMAGE_TAG"` 44 FlyPath string `env:"FLY_PATH"` 45 K8sEngine string `env:"K8S_ENGINE" envDefault:"GKE"` 46 InCluster bool `env:"IN_CLUSTER" envDefault:"false"` 47 } 48 49 var ( 50 Environment environment 51 endpointFactory EndpointFactory 52 fly FlyCli 53 releaseName, namespace string 54 kubeClient *kubernetes.Clientset 55 deployedReleases map[string]string // map[releaseName] = namespace 56 ) 57 58 var _ = SynchronizedBeforeSuite(func() []byte { 59 var parsedEnv environment 60 61 err := env.Parse(&parsedEnv) 62 Expect(err).ToNot(HaveOccurred()) 63 64 if parsedEnv.FlyPath == "" { 65 parsedEnv.FlyPath = BuildBinary() 66 } 67 68 By("Checking if kubectl has a context set for port forwarding later") 69 Wait(Start(nil, "kubectl", "config", "current-context")) 70 71 By("Updating the dependencies of the Concourse chart locally") 72 Wait(Start(nil, "helm", "dependency", "update", parsedEnv.ConcourseChartDir)) 73 74 envBytes, err := json.Marshal(parsedEnv) 75 Expect(err).ToNot(HaveOccurred()) 76 77 return envBytes 78 }, func(data []byte) { 79 err := json.Unmarshal(data, &Environment) 80 Expect(err).ToNot(HaveOccurred()) 81 82 deployedReleases = make(map[string]string) 83 }) 84 85 var _ = BeforeEach(func() { 86 SetDefaultEventuallyTimeout(90 * time.Second) 87 SetDefaultConsistentlyDuration(30 * time.Second) 88 89 tmp, err := ioutil.TempDir("", "topgun-tmp") 90 Expect(err).ToNot(HaveOccurred()) 91 92 fly = FlyCli{ 93 Bin: Environment.FlyPath, 94 Target: "concourse-topgun-k8s-" + strconv.Itoa(GinkgoParallelNode()), 95 Home: filepath.Join(tmp, "fly-home-"+strconv.Itoa(GinkgoParallelNode())), 96 } 97 98 endpointFactory = PortForwardingEndpointFactory{} 99 if Environment.InCluster { 100 endpointFactory = AddressEndpointFactory{} 101 } 102 103 err = os.Mkdir(fly.Home, 0755) 104 Expect(err).ToNot(HaveOccurred()) 105 106 By("Checking if kubeconfig exists") 107 kubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config") 108 _, err = os.Stat(kubeconfig) 109 Expect(err).ToNot(HaveOccurred(), "kubeconfig should exist") 110 111 By("Creating a kubernetes client") 112 config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 113 Expect(err).ToNot(HaveOccurred()) 114 115 kubeClient, err = kubernetes.NewForConfig(config) 116 Expect(err).ToNot(HaveOccurred()) 117 }) 118 119 // In case one of the tests exited before entering the It block, make sure to cleanup 120 // any releases that might've already been deployed 121 var _ = AfterSuite(func() { 122 cleanupReleases() 123 }) 124 125 func setReleaseNameAndNamespace(description string) { 126 rand.Seed(time.Now().UTC().UnixNano()) 127 releaseName = fmt.Sprintf("topgun-"+description+"-%d", rand.Int63n(100000000)) 128 namespace = releaseName 129 } 130 131 // pod corresponds to the Json object that represents a Kuberneted pod from the 132 // apiserver perspective. 133 // 134 type pod struct { 135 Status struct { 136 Conditions []struct { 137 Type string `json:"type"` 138 Status string `json:"status"` 139 } `json:"conditions"` 140 ContainerStatuses []struct { 141 Name string `json:"name"` 142 Ready bool `json:"ready"` 143 } `json:"containerStatuses"` 144 Ip string `json:"podIP"` 145 } `json:"status"` 146 Metadata struct { 147 Name string `json:"name"` 148 } `json:"metadata"` 149 } 150 151 // Endpoint represents a service that can be reached from a given address. 152 // 153 type Endpoint interface { 154 Address() (addr string) 155 Close() (err error) 156 } 157 158 // EndpointFactory represents those entities able to generate Endpoints for 159 // both services and pods. 160 // 161 type EndpointFactory interface { 162 NewServiceEndpoint(namespace, service, port string) (endpoint Endpoint) 163 NewPodEndpoint(namespace, pod, port string) (endpoint Endpoint) 164 } 165 166 // PortForwardingEndpoint is a service that can be reached through a local 167 // address, having connections port forwarded to entities in a cluster. 168 // 169 type PortForwardingEndpoint struct { 170 session *gexec.Session 171 address string 172 } 173 174 func (p PortForwardingEndpoint) Address() string { 175 return p.address 176 } 177 178 func (p PortForwardingEndpoint) Close() error { 179 p.session.Interrupt() 180 return nil 181 } 182 183 // AddressEndpoint represents a direct address without any underlying session. 184 // 185 type AddressEndpoint struct { 186 address string 187 } 188 189 func (p AddressEndpoint) Address() string { 190 return p.address 191 } 192 193 func (p AddressEndpoint) Close() error { 194 return nil 195 } 196 197 // PortForwardingFactory deals with creating endpoints that reach the targets 198 // through port-forwarding. 199 // 200 type PortForwardingEndpointFactory struct{} 201 202 func (f PortForwardingEndpointFactory) NewServiceEndpoint(namespace, service, port string) Endpoint { 203 session, address := portForward(namespace, "service/"+service, port) 204 205 return PortForwardingEndpoint{ 206 session: session, 207 address: address, 208 } 209 } 210 211 func (f PortForwardingEndpointFactory) NewPodEndpoint(namespace, pod, port string) Endpoint { 212 session, address := portForward(namespace, "pod/"+pod, port) 213 214 return PortForwardingEndpoint{ 215 session: session, 216 address: address, 217 } 218 } 219 220 // AddressFactory deals with creating endpoints that reach the targets 221 // through port-forwarding. 222 // 223 type AddressEndpointFactory struct{} 224 225 func (f AddressEndpointFactory) NewServiceEndpoint(namespace, service, port string) Endpoint { 226 address := serviceAddress(namespace, service) 227 228 return AddressEndpoint{ 229 address: address + ":" + port, 230 } 231 } 232 233 func (f AddressEndpointFactory) NewPodEndpoint(namespace, pod, port string) Endpoint { 234 address := podAddress(namespace, pod) 235 236 return AddressEndpoint{ 237 address: address + ":" + port, 238 } 239 } 240 241 func podAddress(namespace, pod string) string { 242 pods := getPods(namespace, metav1.ListOptions{FieldSelector: "metadata.name=" + pod}) 243 Expect(pods).To(HaveLen(1)) 244 245 return pods[0].Status.PodIP 246 } 247 248 // serviceAddress retrieves the ClusterIP address of a service on a given 249 // namespace. 250 // 251 func serviceAddress(namespace, serviceName string) (address string) { 252 return serviceName + "." + namespace 253 } 254 255 // portForward establishes a port-forwarding session against a given kubernetes 256 // resource, for a particular port. 257 // 258 func portForward(namespace, resource, port string) (*gexec.Session, string) { 259 sess := Start(nil, 260 "kubectl", "port-forward", 261 "--namespace="+namespace, 262 resource, 263 ":"+port, 264 ) 265 266 Eventually(sess.Out).Should(gbytes.Say("Forwarding")) 267 268 address := regexp.MustCompile(`127\.0\.0\.1:[0-9]+`). 269 FindStringSubmatch(string(sess.Out.Contents())) 270 Expect(address).NotTo(BeEmpty()) 271 272 return sess, address[0] 273 } 274 275 func helmDeploy(releaseName, namespace, chartDir string, args ...string) *gexec.Session { 276 277 helmArgs := []string{ 278 "upgrade", 279 "--install", 280 "--force", 281 "--namespace", namespace, 282 "--create-namespace", 283 } 284 285 helmArgs = append(helmArgs, args...) 286 helmArgs = append(helmArgs, releaseName, chartDir) 287 288 sess := Start(nil, "helm", helmArgs...) 289 <-sess.Exited 290 291 if sess.ExitCode() == 0 { 292 deployedReleases[releaseName] = namespace 293 } 294 return sess 295 } 296 297 func helmInstallArgs(args ...string) []string { 298 helmArgs := []string{ 299 "--set=concourse.web.kubernetes.keepNamespaces=false", 300 "--set=concourse.worker.bindIp=0.0.0.0", 301 "--set=postgresql.persistence.enabled=false", 302 "--set=web.resources.requests.cpu=500m", 303 "--set=worker.readinessProbe.httpGet.path=/", 304 "--set=worker.readinessProbe.httpGet.port=worker-hc", 305 "--set=worker.resources.requests.cpu=500m", 306 "--set=image=" + Environment.ConcourseImageName} 307 308 if Environment.ConcourseImageTag != "" { 309 helmArgs = append(helmArgs, "--set=imageTag="+Environment.ConcourseImageTag) 310 } 311 312 if Environment.ConcourseImageDigest != "" { 313 helmArgs = append(helmArgs, "--set=imageDigest="+Environment.ConcourseImageDigest) 314 } 315 316 return append(helmArgs, args...) 317 } 318 319 func deployFailingConcourseChart(releaseName string, expectedErr string, args ...string) { 320 helmArgs := helmInstallArgs(args...) 321 sess := helmDeploy(releaseName, releaseName, Environment.ConcourseChartDir, helmArgs...) 322 Expect(sess.ExitCode()).ToNot(Equal(0)) 323 Expect(sess.Err).To(gbytes.Say(expectedErr)) 324 } 325 326 func deployConcourseChart(releaseName string, args ...string) { 327 helmArgs := helmInstallArgs(args...) 328 sess := helmDeploy(releaseName, releaseName, Environment.ConcourseChartDir, helmArgs...) 329 Expect(sess.ExitCode()).To(Equal(0)) 330 } 331 332 func helmDestroy(releaseName, namespace string) { 333 helmArgs := []string{ 334 "delete", 335 "--namespace", 336 namespace, 337 releaseName, 338 } 339 340 Wait(Start(nil, "helm", helmArgs...)) 341 } 342 343 func getPods(namespace string, listOptions metav1.ListOptions) []corev1.Pod { 344 pods, err := kubeClient.CoreV1().Pods(namespace).List(context.TODO(), listOptions) 345 Expect(err).ToNot(HaveOccurred()) 346 347 return pods.Items 348 } 349 350 func isPodReady(p corev1.Pod) bool { 351 for _, condition := range p.Status.Conditions { 352 if condition.Type != corev1.ContainersReady { 353 continue 354 } 355 356 return condition.Status == "True" 357 } 358 359 return false 360 } 361 362 func waitAllPodsInNamespaceToBeReady(namespace string) { 363 Eventually(func() bool { 364 expectedPods := getPods(namespace, metav1.ListOptions{}) 365 actualPods := getPods(namespace, metav1.ListOptions{FieldSelector: "status.phase=Running"}) 366 367 if len(expectedPods) != len(actualPods) { 368 return false 369 } 370 371 podsReady := 0 372 for _, pod := range actualPods { 373 if isPodReady(pod) { 374 podsReady++ 375 } 376 } 377 378 return podsReady == len(expectedPods) 379 }, 15*time.Minute, 10*time.Second).Should(BeTrue(), "expected all pods to be running") 380 } 381 382 func deletePods(namespace string, flags ...string) []string { 383 var ( 384 podNames []string 385 args = append([]string{"delete", "pod", 386 "--namespace=" + namespace, 387 }, flags...) 388 session = Start(nil, "kubectl", args...) 389 ) 390 391 Wait(session) 392 393 scanner := bufio.NewScanner(bytes.NewBuffer(session.Out.Contents())) 394 for scanner.Scan() { 395 podNames = append(podNames, scanner.Text()) 396 } 397 398 return podNames 399 } 400 401 func getRunningWorkers(workers []Worker) (running []Worker) { 402 for _, w := range workers { 403 if w.State == "running" { 404 running = append(running, w) 405 } 406 } 407 return 408 } 409 410 func waitAndLogin(namespace, service string) Endpoint { 411 waitAllPodsInNamespaceToBeReady(namespace) 412 413 atc := endpointFactory.NewServiceEndpoint( 414 namespace, 415 service, 416 "8080", 417 ) 418 419 fly.Login("test", "test", "http://"+atc.Address()) 420 421 Eventually(func() []Worker { 422 return getRunningWorkers(fly.GetWorkers()) 423 }, 2*time.Minute, 10*time.Second). 424 ShouldNot(HaveLen(0)) 425 426 return atc 427 } 428 429 func cleanupReleases() { 430 for releaseName, namespace := range deployedReleases { 431 helmDestroy(releaseName, namespace) 432 kubeClient.CoreV1().Namespaces().Delete(context.TODO(), namespace, metav1.DeleteOptions{}) 433 } 434 435 deployedReleases = make(map[string]string) 436 } 437 438 func onPks(f func()) { 439 Context("PKS", func() { 440 441 BeforeEach(func() { 442 if Environment.K8sEngine != "PKS" { 443 Skip("not running on PKS") 444 } 445 }) 446 447 f() 448 }) 449 } 450 451 func onGke(f func()) { 452 Context("GKE", func() { 453 454 BeforeEach(func() { 455 if Environment.K8sEngine != "GKE" { 456 Skip("not running on GKE") 457 } 458 }) 459 460 f() 461 }) 462 }