From c55eaaa9788ff38e146e2b638e04483ff514272c Mon Sep 17 00:00:00 2001
From: Tomasz Maczukin <tomasz@maczukin.pl>
Date: Fri, 14 Jul 2017 21:32:59 +0200
Subject: [PATCH] Support extended docker configuration with Kubernetes
 executor

---
 common/network.go                             |   4 +-
 executors/kubernetes/executor_kubernetes.go   |  45 ++++---
 .../kubernetes/executor_kubernetes_test.go    | 124 +++++++++++++++++-
 3 files changed, 147 insertions(+), 26 deletions(-)

diff --git a/common/network.go b/common/network.go
index 3f9f52a5c..462403a24 100644
--- a/common/network.go
+++ b/common/network.go
@@ -150,8 +150,8 @@ type Step struct {
 type Steps []Step
 
 type Image struct {
-	Name       string `json:"name"`
-	Alias      string `json:"alias,omitempty"`
+	Name       string   `json:"name"`
+	Alias      string   `json:"alias,omitempty"`
 	Command    []string `json:"command,omitempty"`
 	Entrypoint []string `json:"entrypoint,omitempty"`
 }
diff --git a/executors/kubernetes/executor_kubernetes.go b/executors/kubernetes/executor_kubernetes.go
index 704da9f96..e021ccfa4 100644
--- a/executors/kubernetes/executor_kubernetes.go
+++ b/executors/kubernetes/executor_kubernetes.go
@@ -28,8 +28,8 @@ var (
 )
 
 type kubernetesOptions struct {
-	Image    string   `json:"image"`
-	Services []string `json:"services"`
+	Image    common.Image
+	Services common.Services
 }
 
 type executor struct {
@@ -117,7 +117,7 @@ func (s *executor) Prepare(options common.ExecutorPrepareOptions) (err error) {
 		return err
 	}
 
-	s.Println("Using Kubernetes executor with image", s.options.Image, "...")
+	s.Println("Using Kubernetes executor with image", s.options.Image.Name, "...")
 
 	return nil
 }
@@ -174,17 +174,28 @@ func (s *executor) Cleanup() {
 	s.AbstractExecutor.Cleanup()
 }
 
-func (s *executor) buildContainer(name, image string, requests, limits api.ResourceList, command ...string) api.Container {
+func (s *executor) buildContainer(name, image string, imageDefinition common.Image, requests, limits api.ResourceList, command ...string) api.Container {
 	privileged := false
 	if s.Config.Kubernetes != nil {
 		privileged = s.Config.Kubernetes.Privileged
 	}
 
+	if len(command) == 0 && len(imageDefinition.Command) > 0 {
+		command = imageDefinition.Command
+	}
+
+	var args []string
+	if len(imageDefinition.Entrypoint) > 0 {
+		args = command
+		command = imageDefinition.Entrypoint
+	}
+
 	return api.Container{
 		Name:            name,
 		Image:           image,
 		ImagePullPolicy: api.PullPolicy(s.pullPolicy),
 		Command:         command,
+		Args:            args,
 		Env:             buildVariables(s.Build.GetAllVariables().PublicOrInternal()),
 		Resources: api.ResourceRequirements{
 			Limits:   limits,
@@ -357,9 +368,9 @@ func (s *executor) setupCredentials() error {
 
 func (s *executor) setupBuildPod() error {
 	services := make([]api.Container, len(s.options.Services))
-	for i, image := range s.options.Services {
-		resolvedImage := s.Build.GetAllVariables().ExpandValue(image)
-		services[i] = s.buildContainer(fmt.Sprintf("svc-%d", i), resolvedImage, s.serviceRequests, s.serviceLimits)
+	for i, service := range s.options.Services {
+		resolvedImage := s.Build.GetAllVariables().ExpandValue(service.Name)
+		services[i] = s.buildContainer(fmt.Sprintf("svc-%d", i), resolvedImage, service, s.serviceRequests, s.serviceLimits)
 	}
 	labels := make(map[string]string)
 	for k, v := range s.Build.Runner.Kubernetes.PodLabels {
@@ -375,7 +386,7 @@ func (s *executor) setupBuildPod() error {
 		imagePullSecrets = append(imagePullSecrets, api.LocalObjectReference{Name: s.credentials.Name})
 	}
 
-	buildImage := s.Build.GetAllVariables().ExpandValue(s.options.Image)
+	buildImage := s.Build.GetAllVariables().ExpandValue(s.options.Image.Name)
 	pod, err := s.kubeClient.Pods(s.Config.Kubernetes.Namespace).Create(&api.Pod{
 		ObjectMeta: api.ObjectMeta{
 			GenerateName: s.Build.ProjectUniqueName(),
@@ -389,8 +400,8 @@ func (s *executor) setupBuildPod() error {
 			NodeSelector:       s.Config.Kubernetes.NodeSelector,
 			Containers: append([]api.Container{
 				// TODO use the build and helper template here
-				s.buildContainer("build", buildImage, s.buildRequests, s.buildLimits, s.BuildShell.DockerCommand...),
-				s.buildContainer("helper", s.Config.Kubernetes.GetHelperImage(), s.helperRequests, s.helperLimits, s.BuildShell.DockerCommand...),
+				s.buildContainer("build", buildImage, s.options.Image, s.buildRequests, s.buildLimits, s.BuildShell.DockerCommand...),
+				s.buildContainer("helper", s.Config.Kubernetes.GetHelperImage(), common.Image{}, s.helperRequests, s.helperLimits, s.BuildShell.DockerCommand...),
 			}, services...),
 			TerminationGracePeriodSeconds: &s.Config.Kubernetes.TerminationGracePeriodSeconds,
 			ImagePullSecrets:              imagePullSecrets,
@@ -452,25 +463,25 @@ func (s *executor) runInContainer(ctx context.Context, name, command string) <-c
 
 func (s *executor) prepareOptions(job *common.Build) {
 	s.options = &kubernetesOptions{}
-	s.options.Image = job.Image.Name
+	s.options.Image = job.Image
 	for _, service := range job.Services {
-		serviceName := service.Name
-		if serviceName == "" {
+		if service.Name == "" {
 			continue
 		}
-
-		s.options.Services = append(s.options.Services, serviceName)
+		s.options.Services = append(s.options.Services, service)
 	}
 }
 
 // checkDefaults Defines the configuration for the Pod on Kubernetes
 func (s *executor) checkDefaults() error {
-	if s.options.Image == "" {
+	if s.options.Image.Name == "" {
 		if s.Config.Kubernetes.Image == "" {
 			return fmt.Errorf("no image specified and no default set in config")
 		}
 
-		s.options.Image = s.Config.Kubernetes.Image
+		s.options.Image = common.Image{
+			Name: s.Config.Kubernetes.Image,
+		}
 	}
 
 	if s.Config.Kubernetes.Namespace == "" {
diff --git a/executors/kubernetes/executor_kubernetes_test.go b/executors/kubernetes/executor_kubernetes_test.go
index f8392c26b..6d7a89dee 100644
--- a/executors/kubernetes/executor_kubernetes_test.go
+++ b/executors/kubernetes/executor_kubernetes_test.go
@@ -385,7 +385,9 @@ func TestPrepare(t *testing.T) {
 			},
 			Expected: &executor{
 				options: &kubernetesOptions{
-					Image: "test-image",
+					Image: common.Image{
+						Name: "test-image",
+					},
 				},
 				namespaceOverwrite: "",
 				serviceLimits: api.ResourceList{
@@ -446,7 +448,9 @@ func TestPrepare(t *testing.T) {
 			},
 			Expected: &executor{
 				options: &kubernetesOptions{
-					Image: "test-image",
+					Image: common.Image{
+						Name: "test-image",
+					},
 				},
 				serviceAccountOverwrite: "not-default",
 				serviceLimits: api.ResourceList{
@@ -517,7 +521,9 @@ func TestPrepare(t *testing.T) {
 			},
 			Expected: &executor{
 				options: &kubernetesOptions{
-					Image: "test-image",
+					Image: common.Image{
+						Name: "test-image",
+					},
 				},
 				namespaceOverwrite: "namespacee",
 				serviceLimits: api.ResourceList{
@@ -589,7 +595,9 @@ func TestPrepare(t *testing.T) {
 			},
 			Expected: &executor{
 				options: &kubernetesOptions{
-					Image: "test-image",
+					Image: common.Image{
+						Name: "test-image",
+					},
 				},
 				namespaceOverwrite: "namespacee",
 				serviceLimits: api.ResourceList{
@@ -645,7 +653,9 @@ func TestPrepare(t *testing.T) {
 			},
 			Expected: &executor{
 				options: &kubernetesOptions{
-					Image: "test-image",
+					Image: common.Image{
+						Name: "test-image",
+					},
 				},
 				namespaceOverwrite: "",
 				serviceLimits:      api.ResourceList{},
@@ -676,7 +686,60 @@ func TestPrepare(t *testing.T) {
 			},
 			Expected: &executor{
 				options: &kubernetesOptions{
-					Image: "test-image",
+					Image: common.Image{
+						Name: "test-image",
+					},
+				},
+				namespaceOverwrite: "",
+				serviceLimits:      api.ResourceList{},
+				buildLimits:        api.ResourceList{},
+				helperLimits:       api.ResourceList{},
+				serviceRequests:    api.ResourceList{},
+				buildRequests:      api.ResourceList{},
+				helperRequests:     api.ResourceList{},
+			},
+		},
+		{
+			GlobalConfig: &common.Config{},
+			RunnerConfig: &common.RunnerConfig{
+				RunnerSettings: common.RunnerSettings{
+					Kubernetes: &common.KubernetesConfig{
+						Host: "test-server",
+					},
+				},
+			},
+			Build: &common.Build{
+				JobResponse: common.JobResponse{
+					GitInfo: common.GitInfo{
+						Sha: "1234567890",
+					},
+					Image: common.Image{
+						Name:       "test-image",
+						Entrypoint: []string{"/init", "run"},
+					},
+					Services: common.Services{
+						{
+							Name:       "test-service",
+							Entrypoint: []string{"/init", "run"},
+							Command:    []string{"application", "--debug"},
+						},
+					},
+				},
+				Runner: &common.RunnerConfig{},
+			},
+			Expected: &executor{
+				options: &kubernetesOptions{
+					Image: common.Image{
+						Name:       "test-image",
+						Entrypoint: []string{"/init", "run"},
+					},
+					Services: common.Services{
+						{
+							Name:       "test-service",
+							Entrypoint: []string{"/init", "run"},
+							Command:    []string{"application", "--debug"},
+						},
+					},
 				},
 				namespaceOverwrite: "",
 				serviceLimits:      api.ResourceList{},
@@ -852,6 +915,7 @@ func TestSetupBuildPod(t *testing.T) {
 
 	type testDef struct {
 		RunnerConfig common.RunnerConfig
+		Options      *kubernetesOptions
 		PrepareFn    func(*testing.T, testDef, *executor)
 		VerifyFn     func(*testing.T, testDef, *api.Pod)
 		Variables    []common.JobVariable
@@ -968,6 +1032,47 @@ func TestSetupBuildPod(t *testing.T) {
 				{Key: "test", Value: "sometestvar"},
 			},
 		},
+		{
+			RunnerConfig: common.RunnerConfig{
+				RunnerSettings: common.RunnerSettings{
+					Kubernetes: &common.KubernetesConfig{
+						Namespace:   "default",
+						HelperImage: "custom/helper-image",
+					},
+				},
+			},
+			Options: &kubernetesOptions{
+				Image: common.Image{
+					Name:       "test-image",
+					Entrypoint: []string{"/init", "run"},
+				},
+				Services: common.Services{
+					{
+						Name:       "test-service",
+						Entrypoint: []string{"/init", "run"},
+						Command:    []string{"application", "--debug"},
+					},
+				},
+			},
+			VerifyFn: func(t *testing.T, test testDef, pod *api.Pod) {
+				require.Len(t, pod.Spec.Containers, 3)
+
+				assert.Equal(t, pod.Spec.Containers[0].Name, "build")
+				assert.Equal(t, pod.Spec.Containers[0].Image, "test-image")
+				assert.Equal(t, pod.Spec.Containers[0].Command, []string{"/init", "run"})
+				assert.Empty(t, pod.Spec.Containers[0].Args, "Build container args should be empty")
+
+				assert.Equal(t, pod.Spec.Containers[1].Name, "helper")
+				assert.Equal(t, pod.Spec.Containers[1].Image, "custom/helper-image")
+				assert.Empty(t, pod.Spec.Containers[1].Command, "Helper container command should be empty")
+				assert.Empty(t, pod.Spec.Containers[1].Args, "Helper container args should be empty")
+
+				assert.Equal(t, pod.Spec.Containers[2].Name, "svc-0")
+				assert.Equal(t, pod.Spec.Containers[2].Image, "test-service")
+				assert.Equal(t, pod.Spec.Containers[2].Command, []string{"/init", "run"})
+				assert.Equal(t, pod.Spec.Containers[2].Args, []string{"application", "--debug"})
+			},
+		},
 	}
 
 	executed := false
@@ -1014,9 +1119,14 @@ func TestSetupBuildPod(t *testing.T) {
 		if vars == nil {
 			vars = []common.JobVariable{}
 		}
+
+		options := test.Options
+		if options == nil {
+			options = &kubernetesOptions{}
+		}
 		ex := executor{
 			kubeClient: c,
-			options:    &kubernetesOptions{},
+			options:    options,
 			AbstractExecutor: executors.AbstractExecutor{
 				Config:     test.RunnerConfig,
 				BuildShell: &common.ShellConfiguration{},
-- 
GitLab