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  }