diff --git a/Dockerfile b/Dockerfile index eb064c1c..d9ddf6ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,10 @@ COPY static/ /static/ RUN \ apk --no-cache add curl ca-certificates git go musl-dev && \ - curl -sSL -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.9.3/bin/linux/amd64/kubectl && \ + curl -sSL -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.10.4/bin/linux/amd64/kubectl && \ + curl -sSL -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v1.0.3/kustomize_1.0.3_linux_amd64 && \ chmod +x /usr/local/bin/kubectl && \ + chmod +x /usr/local/bin/kustomize && \ go get -t ./... && \ go test ./... && \ CGO_ENABLED=0 go build -ldflags '-s -extldflags "-static"' -o /kube-applier . && \ diff --git a/kube/client.go b/kube/client.go index 4f3714dc..4ca231ac 100644 --- a/kube/client.go +++ b/kube/client.go @@ -36,7 +36,6 @@ var execCommand = exec.Command //todo(catalin-ilea) Add core/v1/Secret when we plug in strongbox var pruneWhitelist = []string{ "apps/v1/DaemonSet", - "apps/v1beta1/DaemonSet", "apps/v1/Deployment", "apps/v1/StatefulSet", "autoscaling/v1/HorizontalPodAutoscaler", @@ -61,8 +60,7 @@ const ( // ClientInterface allows for mocking out the functionality of Client when testing the full process of an apply run. type ClientInterface interface { - Apply(path, namespace string, dryRun, prune bool) (string, string, error) - StrictApply(path, namespace string, dryRun, prune bool) (string, string, error) + Apply(path, namespace string, dryRun, prune, strict, kustomize bool) (string, string, error) CheckVersion() error GetNamespaceStatus(namespace string) (AutomaticDeploymentOption, error) GetNamespaceUserSecretName(namespace, username string) (string, error) @@ -163,51 +161,73 @@ func isCompatible(clientMajor, clientMinor, serverMajor, serverMinor string) err return nil } -func prepareApplyArgs(path, namespace, label string, dryRun, prune bool) []string { - args := []string{"kubectl", "apply", fmt.Sprintf("--dry-run=%t", dryRun), "-R", "-f", path, fmt.Sprintf("-l %s!=%s", label, Off), "-n", namespace} +// Apply attempts to "kubectl apply" the files located at path. It returns the +// full apply command and its output. +// +// strict - attempt to "kubectl apply" the files located at path using a +// `kube-applier` service account under the given namespace. +// `kube-applier` service account must exist for the given namespace and +// must contain a secret that include token and ca.cert. It returns the +// full apply command and its output. +// +// kustomize - Do a `kuztomize build` on the path before piping to `kubectl +// apply`, set to if there is a `kustomization.yaml` found in the path +func (c *Client) Apply(path, namespace string, dryRun, prune, strict, kustomize bool) (string, string, error) { + var args []string + + if kustomize { + args = []string{"kubectl", "apply", fmt.Sprintf("--dry-run=%t", dryRun), "-f", "-", fmt.Sprintf("-l %s!=%s", c.Label, Off), "-n", namespace} + } else { + args = []string{"kubectl", "apply", fmt.Sprintf("--dry-run=%t", dryRun), "-R", "-f", path, fmt.Sprintf("-l %s!=%s", c.Label, Off), "-n", namespace} + } + if prune { args = append(args, "--prune") for _, w := range pruneWhitelist { args = append(args, "--prune-whitelist="+w) } } - return args -} - -func executeApply(args []string) (string, string, error) { - cmd := strings.Join(args, " ") - stdout, err := execCommand(args[0], args[1:]...).CombinedOutput() - if err != nil { - err = errors.Wrap(err, "kubectl apply command failed") - } - return cmd, string(stdout), err -} -// Apply attempts to "kubectl apply" the file located at path. -// It returns the full apply command and its output. -func (c *Client) Apply(path, namespace string, dryRun, prune bool) (string, string, error) { - args := prepareApplyArgs(path, namespace, c.Label, dryRun, prune) - if c.Server != "" { + if strict { + tempKubeConfigFilepath, tempCertFilepath, err := c.CreateTempConfig(namespace, "kube-applier") + if err != nil { + return "", "", fmt.Errorf("error creating temp config: %v", err) + } + defer func() { os.Remove(tempKubeConfigFilepath); os.Remove(tempCertFilepath) }() + args = append(args, fmt.Sprintf("--kubeconfig=%s", tempKubeConfigFilepath)) + } else if c.Server != "" { args = append(args, fmt.Sprintf("--kubeconfig=%s", kubeconfigFilePath)) } - return executeApply(args) -} + kubectlCmd := exec.Command(args[0], args[1:]...) + + var out []byte + var err error + var cmdStr string -// StrictApply will attempt to "kubectl apply" the file located at path using a `kube-applier` service account under the given namespace. -// `kube-applier` service account must exist for the given namespace and must contain a secret that include token and ca.cert. -// It returns the full apply command and its output. -func (c *Client) StrictApply(path, namespace string, dryRun, prune bool) (string, string, error) { - args := prepareApplyArgs(path, namespace, c.Label, dryRun, prune) + if kustomize { + cmdStr = "kustomize build " + path + " | " + strings.Join(args, " ") + kustomizeCmd := exec.Command("kustomize", "build", path) + pipe, err := kustomizeCmd.StdoutPipe() + if err != nil { + return cmdStr, "", err + } + kubectlCmd.Stdin = pipe + + err = kustomizeCmd.Start() + if err != nil { + return cmdStr, "", err + } + } else { + cmdStr = strings.Join(args, " ") + } - tempKubeConfigFilepath, tempCertFilepath, err := c.CreateTempConfig(namespace, "kube-applier") + out, err = kubectlCmd.CombinedOutput() if err != nil { - return "", "", fmt.Errorf("error creating temp config: %v", err) + return cmdStr, string(out), err } - defer func() { os.Remove(tempKubeConfigFilepath); os.Remove(tempCertFilepath) }() - args = append(args, fmt.Sprintf("--kubeconfig=%s", tempKubeConfigFilepath)) - return executeApply(args) + return cmdStr, string(out), err } // GetNamespaceStatus returns the AutmaticDeployment label for the given namespace diff --git a/kube/client_test.go b/kube/client_test.go index 59276553..16fa150e 100644 --- a/kube/client_test.go +++ b/kube/client_test.go @@ -163,20 +163,3 @@ func TestGetUserDataFromSecret(t *testing.T) { t.Fatal("Got unexpected cert") } } - -func TestPrepareApplyArgsPrune(t *testing.T) { - assert := assert.New(t) - - // Prune == false - expected := []string{"kubectl", "apply", "--dry-run=false", "-R", "-f", "/path", "-l autoDeployment!=off", "-n", "namespace"} - actual := prepareApplyArgs("/path", "namespace", "autoDeployment", false, false) - assert.Equal(expected, actual) - - // Prune == true - expected = append(expected, "--prune") - for _, w := range pruneWhitelist { - expected = append(expected, "--prune-whitelist="+w) - } - actual = prepareApplyArgs("/path", "namespace", "autoDeployment", false, true) - assert.Equal(expected, actual) -} diff --git a/kube/mock_client.go b/kube/mock_client.go index 3703c418..31da9ab9 100644 --- a/kube/mock_client.go +++ b/kube/mock_client.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: client.go +// Source: kube/client.go // Package kube is a generated GoMock package. package kube @@ -33,8 +33,8 @@ func (m *MockClientInterface) EXPECT() *MockClientInterfaceMockRecorder { } // Apply mocks base method -func (m *MockClientInterface) Apply(path, namespace string, dryRun, prune bool) (string, string, error) { - ret := m.ctrl.Call(m, "Apply", path, namespace, dryRun, prune) +func (m *MockClientInterface) Apply(path, namespace string, dryRun, prune, strict, kustomize bool) (string, string, error) { + ret := m.ctrl.Call(m, "Apply", path, namespace, dryRun, prune, strict, kustomize) ret0, _ := ret[0].(string) ret1, _ := ret[1].(string) ret2, _ := ret[2].(error) @@ -42,22 +42,8 @@ func (m *MockClientInterface) Apply(path, namespace string, dryRun, prune bool) } // Apply indicates an expected call of Apply -func (mr *MockClientInterfaceMockRecorder) Apply(path, namespace, dryRun, prune interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockClientInterface)(nil).Apply), path, namespace, dryRun, prune) -} - -// StrictApply mocks base method -func (m *MockClientInterface) StrictApply(path, namespace string, dryRun, prune bool) (string, string, error) { - ret := m.ctrl.Call(m, "StrictApply", path, namespace, dryRun, prune) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(string) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// StrictApply indicates an expected call of StrictApply -func (mr *MockClientInterfaceMockRecorder) StrictApply(path, namespace, dryRun, prune interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StrictApply", reflect.TypeOf((*MockClientInterface)(nil).StrictApply), path, namespace, dryRun, prune) +func (mr *MockClientInterfaceMockRecorder) Apply(path, namespace, dryRun, prune, strict, kustomize interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockClientInterface)(nil).Apply), path, namespace, dryRun, prune, strict, kustomize) } // CheckVersion mocks base method diff --git a/run/batch_applier.go b/run/batch_applier.go index 74ca78c3..695356ec 100644 --- a/run/batch_applier.go +++ b/run/batch_applier.go @@ -60,12 +60,14 @@ func (a *BatchApplier) Apply(applyList []string) ([]ApplyAttempt, []ApplyAttempt default: continue } - var cmd, output string - if a.StrictApply { - cmd, output, err = a.KubeClient.StrictApply(path, ns, a.DryRun || disabled, a.Prune) - } else { - cmd, output, err = a.KubeClient.Apply(path, ns, a.DryRun || disabled, a.Prune) + + var kustomize bool + if _, err := os.Stat(path + "/kustomization.yaml"); err == nil { + kustomize = true } + + var cmd, output string + cmd, output, err = a.KubeClient.Apply(path, ns, a.DryRun || disabled, a.Prune, a.StrictApply, kustomize) success := (err == nil) appliedFile := ApplyAttempt{path, cmd, output, ""} if success { diff --git a/run/batch_applier_test.go b/run/batch_applier_test.go index 9277afd9..a28275f8 100644 --- a/run/batch_applier_test.go +++ b/run/batch_applier_test.go @@ -286,11 +286,11 @@ func expectCheckVersionAndReturnNil(kubeClient *kube.MockClientInterface) *gomoc } func expectApplyAndReturnSuccess(file, namespace string, dryRun, prune bool, kubeClient *kube.MockClientInterface) *gomock.Call { - return kubeClient.EXPECT().Apply(file, namespace, dryRun, prune).Times(1).Return("cmd "+file, "output "+file, nil) + return kubeClient.EXPECT().Apply(file, namespace, dryRun, prune, false, false).Times(1).Return("cmd "+file, "output "+file, nil) } func expectApplyAndReturnFailure(file, namespace string, dryRun, prune bool, kubeClient *kube.MockClientInterface) *gomock.Call { - return kubeClient.EXPECT().Apply(file, namespace, dryRun, prune).Times(1).Return("cmd "+file, "output "+file, fmt.Errorf("error "+file)) + return kubeClient.EXPECT().Apply(file, namespace, dryRun, prune, false, false).Times(1).Return("cmd "+file, "output "+file, fmt.Errorf("error "+file)) } func expectGetNamespaceStatusAndReturn(ret kube.AutomaticDeploymentOption, namespace string, kubeClient *kube.MockClientInterface) *gomock.Call {