From adfa3103b32754cf86eb6747eee64dc1b505301e Mon Sep 17 00:00:00 2001 From: Jane Liu L Date: Tue, 29 Jun 2021 14:58:23 +0800 Subject: [PATCH 1/2] Detect changes/drift through a filtered diff of the apply Signed-off-by: Jane Liu L --- controllers/kustomization_controller.go | 22 ++++- controllers/kustomization_controller_test.go | 92 ++++++++++++++++++++ controllers/utils.go | 44 ++++++++++ 3 files changed, 155 insertions(+), 3 deletions(-) diff --git a/controllers/kustomization_controller.go b/controllers/kustomization_controller.go index 782c235c..041b57e4 100644 --- a/controllers/kustomization_controller.go +++ b/controllers/kustomization_controller.go @@ -624,11 +624,15 @@ func (r *KustomizationReconciler) apply(ctx context.Context, kustomization kusto start := time.Now() timeout := kustomization.GetTimeout() + (time.Second * 1) applyCtx, cancel := context.WithTimeout(ctx, timeout) + var resources map[string]string defer cancel() fieldManager := "kustomize-controller" cmd := fmt.Sprintf("cd %s && kubectl apply --field-manager=%s -f %s.yaml --timeout=%s --cache-dir=/tmp --force=%t", dirPath, fieldManager, kustomization.GetUID(), kustomization.Spec.Interval.Duration.String(), kustomization.Spec.Force) + // kubectl diff command to get the changeset + diffcmd := fmt.Sprintf("cd %s && kubectl diff --field-manager=%s -f %s.yaml --cache-dir=/tmp", + dirPath, fieldManager, kustomization.GetUID()) if kustomization.Spec.KubeConfig != nil { kubeConfig, err := imp.WriteKubeConfig(ctx) @@ -636,6 +640,7 @@ func (r *KustomizationReconciler) apply(ctx context.Context, kustomization kusto return "", err } cmd = fmt.Sprintf("%s --kubeconfig=%s", cmd, kubeConfig) + diffcmd = fmt.Sprintf("%s --kubeconfig %s", diffcmd, kubeConfig) } else { // impersonate SA if kustomization.Spec.ServiceAccountName != "" { @@ -645,18 +650,30 @@ func (r *KustomizationReconciler) apply(ctx context.Context, kustomization kusto } cmd = fmt.Sprintf("%s --token %s", cmd, saToken) + diffcmd = fmt.Sprintf("%s --token %s", diffcmd, saToken) } } + // The reason for we parse the output via kubectl diff could be check from : https://github.com/fluxcd/kustomize-controller/issues/305 + diffcommand := exec.CommandContext(applyCtx, "/bin/sh", "-c", diffcmd) + diffoutput, err := diffcommand.CombinedOutput() + command := exec.CommandContext(applyCtx, "/bin/sh", "-c", cmd) - output, err := command.CombinedOutput() + output, applyerr := command.CombinedOutput() + if err != nil { + resources = parseApplyOutput(output) + } else { + resources = parseDiffOutput(diffoutput) + } + + if applyerr != nil { if errors.Is(applyCtx.Err(), context.DeadlineExceeded) { return "", fmt.Errorf("apply timeout: %w", applyCtx.Err()) } if string(output) == "" { - return "", fmt.Errorf("apply failed: %w, kubectl process was killed, probably due to OOM", err) + return "", fmt.Errorf("apply failed: %w, kubectl process was killed, probably due to OOM", applyerr) } applyErr := parseApplyError(output) @@ -666,7 +683,6 @@ func (r *KustomizationReconciler) apply(ctx context.Context, kustomization kusto return "", fmt.Errorf("apply failed: %s", applyErr) } - resources := parseApplyOutput(output) log.Info( fmt.Sprintf("Kustomization applied in %s", time.Now().Sub(start).String()), diff --git a/controllers/kustomization_controller_test.go b/controllers/kustomization_controller_test.go index bdf69ac5..2f6ce8dd 100644 --- a/controllers/kustomization_controller_test.go +++ b/controllers/kustomization_controller_test.go @@ -33,8 +33,11 @@ import ( . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + eventv1beta1 "k8s.io/api/events/v1beta1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -429,6 +432,95 @@ spec: Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "test", Namespace: namespace.Name}, deployment)).NotTo(Succeed()) }) }) + + Describe("Kustomization resources event tests", func() { + configMapManifest := func(name string) string { + return fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s +data: + value: %[1]s +`, name) + } + It("should get event for resources output", func() { + manifests := []testserver.File{ + { + Name: "configmap.yaml", + Body: configMapManifest("test-configmap"), + }, + } + artifact, err := httpServer.ArtifactFromFiles(manifests) + Expect(err).NotTo(HaveOccurred()) + + url := fmt.Sprintf("%s/%s", httpServer.URL(), artifact) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("%s", randStringRunes(5)), + Namespace: namespace.Name, + } + repository := readyGitRepository(repositoryName, url, "v1", "") + Expect(k8sClient.Create(context.Background(), repository)).To(Succeed()) + Expect(k8sClient.Status().Update(context.Background(), repository)).To(Succeed()) + defer k8sClient.Delete(context.Background(), repository) + + kName := types.NamespacedName{ + Name: fmt.Sprintf("kustomization-test-%s", randStringRunes(5)), + Namespace: namespace.Name, + } + k := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kName.Name, + Namespace: kName.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + KubeConfig: kubeconfig, + Interval: metav1.Duration{Duration: reconciliationInterval}, + Path: "./", + Prune: true, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Kind: sourcev1.GitRepositoryKind, + Name: repository.Name, + }, + Suspend: false, + Timeout: &metav1.Duration{Duration: 60 * time.Second}, + Validation: "client", + Force: true, + }, + } + Expect(k8sClient.Create(context.Background(), k)).To(Succeed()) + defer k8sClient.Delete(context.Background(), k) + + got := &kustomizev1.Kustomization{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), kName, got) + c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition) + return c != nil && c.Reason == meta.ReconciliationSucceededReason + }, timeout, interval).Should(BeTrue()) + Expect(got.Status.LastAppliedRevision).To(Equal("v1")) + + var configMap corev1.ConfigMap + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "test-configmap", Namespace: "default"}, &configMap)).To(Succeed()) + + Consistently(func() bool { + eventList := &eventv1beta1.EventList{} + labelSelector := labels.NewSelector() + requirement, err := labels.NewRequirement("name", selection.Equals, []string{k.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector.Add(*requirement) + err = k8sClient.List(context.Background(), eventList, &client.ListOptions{LabelSelector: labelSelector}) + Expect(err).NotTo(HaveOccurred(), "could not list Kustomization events") + + for _, event := range eventList.Items { + if event.Note == "configmap/test-configmap created\n" { + return true + } + } + return false + }, timeout, interval).Should(BeTrue()) + }) + }) }) }) diff --git a/controllers/utils.go b/controllers/utils.go index 809a3037..b384ada8 100644 --- a/controllers/utils.go +++ b/controllers/utils.go @@ -48,6 +48,50 @@ func parseApplyOutput(in []byte) map[string]string { return result } +// parasDiffOutput extracts the objects and the action +// performed by kubectl diff e.g. : +//diff -u -N /tmp/LIVE-090729454/v1.ConfigMap.default.first /tmp/MERGED-357794933/v1.ConfigMap.default.first +//--- /tmp/LIVE-090729454/v1.ConfigMap.default.first 2021-06-29 05:52:39.456678181 +0200 +//+++ /tmp/MERGED-357794933/v1.ConfigMap.default.first 2021-06-29 05:52:39.461678214 +0200 +// @@ -8,7 +8,7 @@ +// ... +// diff -u -N /tmp/LIVE-656588208/kustomize.toolkit.fluxcd.io.v1beta1.Kustomization.namespace.name /tmp/MERGED-532750671/kustomize.toolkit.fluxcd.io.v1beta1.Kustomization.namespace.name +// --- /tmp/LIVE-656588208/kustomize.toolkit.fluxcd.io.v1beta1.Kustomization.namespace.name 2021-06-07 12:58:20.738794982 +0200 +// +++ /tmp/MERGED-532750671/kustomize.toolkit.fluxcd.io.v1beta1.Kustomization.namespace.name 2021-06-07 12:58:20.798795908 +0200 +// @@ -0,0 +1,36 @@ +func parseDiffOutput(in []byte) map[string]string { + result := make(map[string]string) + var parts []string + var actions []string + + input := strings.Split(string(in), "\n") + if len(input) == 0 { + return result + } + + for _, str := range input { + if strings.Contains(str, "diff -u -N ") { + s := strings.Split(str, "/") + parts = append(parts, s[len(s)-1]) + } + + if strings.Contains(str, "@@") { + if strings.Contains(str, "@@ -0,0") { + actions = append(actions, "created") + + } else { + actions = append(actions, "configured") + } + } + } + + for k, v := range parts { + result[v] = actions[k] + } + + return result +} + // parseApplyError extracts the errors from the kubectl // apply output by removing the successfully applied objects func parseApplyError(in []byte) string { From bc59ad3f2e584c7359143287bdb5e42b6a57bf95 Mon Sep 17 00:00:00 2001 From: Jane Liu L Date: Thu, 1 Jul 2021 17:00:44 +0800 Subject: [PATCH 2/2] add UT for proves ignore objects that are changed only due to the checksum annotation and test diff failures Signed-off-by: Jane Liu L --- controllers/kustomization_controller.go | 47 ++-- controllers/kustomization_controller_test.go | 236 ++++++++++++++++++- controllers/utils.go | 73 ++++-- 3 files changed, 313 insertions(+), 43 deletions(-) diff --git a/controllers/kustomization_controller.go b/controllers/kustomization_controller.go index 041b57e4..40df1682 100644 --- a/controllers/kustomization_controller.go +++ b/controllers/kustomization_controller.go @@ -658,29 +658,32 @@ func (r *KustomizationReconciler) apply(ctx context.Context, kustomization kusto diffcommand := exec.CommandContext(applyCtx, "/bin/sh", "-c", diffcmd) diffoutput, err := diffcommand.CombinedOutput() - command := exec.CommandContext(applyCtx, "/bin/sh", "-c", cmd) - output, applyerr := command.CombinedOutput() - - if err != nil { - resources = parseApplyOutput(output) - } else { - resources = parseDiffOutput(diffoutput) - } - - if applyerr != nil { - if errors.Is(applyCtx.Err(), context.DeadlineExceeded) { - return "", fmt.Errorf("apply timeout: %w", applyCtx.Err()) - } - - if string(output) == "" { - return "", fmt.Errorf("apply failed: %w, kubectl process was killed, probably due to OOM", applyerr) - } - - applyErr := parseApplyError(output) - if applyErr == "" { - applyErr = "no error output found, this may happen because of a timeout" + // The reason that we still parse diff output when exit is for : https://github.com/kubernetes/kubernetes/pull/82336/files + // For the "diff" command Exit status: + // 0 + // No differences were found. + // 1 + // Differences were found. + // >1 + // Kubectl or diff failed with an error. + if differr, ok := err.(*exec.ExitError); ok { + if differr.ExitCode() > 1 { + log.V(1).Info("Kubectl or diff failed with an error, will execute apply soon") + output, applyErr := execApply(ctx, cmd) + if applyErr != nil { + return "", applyErr + } + resources = parseApplyOutput(output) + } else if differr.ExitCode() == 1 { + resources = parseDiffOutput(diffoutput) + // Since we found difference in "diff" command, so we need to execute apply + log.V(1).Info("Differences found, will execute apply soon") + _, applyErr := execApply(ctx, cmd) + if applyErr != nil { + log.Error(applyErr, "unable to apply") + return "", applyErr + } } - return "", fmt.Errorf("apply failed: %s", applyErr) } log.Info( diff --git a/controllers/kustomization_controller_test.go b/controllers/kustomization_controller_test.go index 2f6ce8dd..177773f3 100644 --- a/controllers/kustomization_controller_test.go +++ b/controllers/kustomization_controller_test.go @@ -435,8 +435,7 @@ spec: Describe("Kustomization resources event tests", func() { configMapManifest := func(name string) string { - return fmt.Sprintf(`--- -apiVersion: v1 + return fmt.Sprintf(`apiVersion: v1 kind: ConfigMap metadata: name: %[1]s @@ -513,13 +512,244 @@ data: Expect(err).NotTo(HaveOccurred(), "could not list Kustomization events") for _, event := range eventList.Items { - if event.Note == "configmap/test-configmap created\n" { + if event.Note == "v1.ConfigMap.default.test-configmap created\n" { return true } } return false }, timeout, interval).Should(BeTrue()) }) + + It("should succeed on update for configmap in new namespace", func() { + configMapManifest := func(namespace, value string) string { + return fmt.Sprintf(`--- +apiVersion: v1 +kind: Namespace +metadata: + name: %[1]s +--- +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: %[1]s + name: %[2]s +data: + value: %[2]s +`, namespace, value) + } + resourceNamespace := fmt.Sprintf("%s", randStringRunes(5)) + manifests := []testserver.File{ + { + Name: "configmap.yaml", + Body: configMapManifest(resourceNamespace, "second-configmap"), + }, + } + artifact, err := httpServer.ArtifactFromFiles(manifests) + Expect(err).NotTo(HaveOccurred()) + + url := fmt.Sprintf("%s/%s", httpServer.URL(), artifact) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("%s", randStringRunes(5)), + Namespace: namespace.Name, + } + repository := readyGitRepository(repositoryName, url, "v2", "") + Expect(k8sClient.Create(context.Background(), repository)).To(Succeed()) + Expect(k8sClient.Status().Update(context.Background(), repository)).To(Succeed()) + defer k8sClient.Delete(context.Background(), repository) + + kName := types.NamespacedName{ + Name: fmt.Sprintf("kustomization-test-%s", randStringRunes(5)), + Namespace: namespace.Name, + } + k := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kName.Name, + Namespace: kName.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + KubeConfig: kubeconfig, + Interval: metav1.Duration{Duration: reconciliationInterval}, + Path: "./", + Prune: true, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Kind: sourcev1.GitRepositoryKind, + Name: repository.Name, + }, + Suspend: false, + Timeout: &metav1.Duration{Duration: 60 * time.Second}, + Validation: "client", + Force: true, + }, + } + Expect(k8sClient.Create(context.Background(), k)).To(Succeed()) + defer k8sClient.Delete(context.Background(), k) + + got := &kustomizev1.Kustomization{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), kName, got) + c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition) + return c != nil && c.Reason == meta.ReconciliationSucceededReason + }, timeout, interval).Should(BeTrue()) + Expect(got.Status.LastAppliedRevision).To(Equal("v2")) + + ns := &corev1.Namespace{} + Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: resourceNamespace}, ns)).Should(Succeed()) + + var configMap corev1.ConfigMap + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "second-configmap", Namespace: resourceNamespace}, &configMap)).To(Succeed()) + }) + + It("Ignore objects that are changed only due to the checksum annotation", func() { + manifests := []testserver.File{ + { + Name: "configmap.yaml", + Body: configMapManifest("third-configmap"), + }, + } + artifact, err := httpServer.ArtifactFromFiles(manifests) + Expect(err).NotTo(HaveOccurred()) + + url := fmt.Sprintf("%s/%s", httpServer.URL(), artifact) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("%s", randStringRunes(5)), + Namespace: namespace.Name, + } + repository := readyGitRepository(repositoryName, url, "v3", "") + Expect(k8sClient.Create(context.Background(), repository)).To(Succeed()) + Expect(k8sClient.Status().Update(context.Background(), repository)).To(Succeed()) + defer k8sClient.Delete(context.Background(), repository) + + kName := types.NamespacedName{ + Name: fmt.Sprintf("kustomization-test-%s", randStringRunes(5)), + Namespace: namespace.Name, + } + k := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kName.Name, + Namespace: kName.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + KubeConfig: kubeconfig, + Interval: metav1.Duration{Duration: reconciliationInterval}, + Path: "./", + Prune: true, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Kind: sourcev1.GitRepositoryKind, + Name: repository.Name, + }, + Suspend: false, + Timeout: &metav1.Duration{Duration: 60 * time.Second}, + Validation: "client", + Force: true, + }, + } + Expect(k8sClient.Create(context.Background(), k)).To(Succeed()) + defer k8sClient.Delete(context.Background(), k) + + got := &kustomizev1.Kustomization{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), kName, got) + c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition) + return c != nil && c.Reason == meta.ReconciliationSucceededReason + }, timeout, interval).Should(BeTrue()) + Expect(got.Status.LastAppliedRevision).To(Equal("v3")) + + var configMap corev1.ConfigMap + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "third-configmap", Namespace: "default"}, &configMap)).To(Succeed()) + + checksum := configMap.Annotations[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)] + + Consistently(func() bool { + eventList := &eventv1beta1.EventList{} + labelSelector := labels.NewSelector() + requirement, err := labels.NewRequirement("name", selection.Equals, []string{k.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector.Add(*requirement) + err = k8sClient.List(context.Background(), eventList, &client.ListOptions{LabelSelector: labelSelector}) + Expect(err).NotTo(HaveOccurred(), "could not list Kustomization events") + + configmapEvent := false + for _, event := range eventList.Items { + if event.Note == "v1.ConfigMap.default.third-configmap created\n" { + configmapEvent = true + } + } + return configmapEvent + }, timeout, interval).Should(BeTrue()) + + manifests = []testserver.File{ + { + Name: "configmap.yaml", + Body: configMapManifest("third-configmap"), + }, + { + Name: "fourthconfigmap.yaml", + Body: configMapManifest("fourth-configmap"), + }, + } + + artifact, err = httpServer.ArtifactFromFiles(manifests) + Expect(err).ToNot(HaveOccurred()) + + url = fmt.Sprintf("%s/%s", httpServer.URL(), artifact) + repository.Status.Artifact.URL = url + repository.Status.Artifact.Revision = "v4" + Expect(k8sClient.Status().Update(context.Background(), repository)).To(Succeed()) + + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), kName, got) + return got.Status.LastAppliedRevision == repository.Status.Artifact.Revision + }, timeout, time.Second).Should(BeTrue()) + + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "fourth-configmap", Namespace: "default"}, &configMap)).To(Succeed()) + Expect(configMap.Annotations[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]).NotTo(Equal(checksum)) + + Consistently(func() bool { + eventList := &eventv1beta1.EventList{} + labelSelector := labels.NewSelector() + requirement, err := labels.NewRequirement("name", selection.Equals, []string{k.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector.Add(*requirement) + err = k8sClient.List(context.Background(), eventList, &client.ListOptions{LabelSelector: labelSelector}) + Expect(err).NotTo(HaveOccurred(), "could not list Kustomization events") + + configmapEvent := false + for _, event := range eventList.Items { + if event.Note == "v1.ConfigMap.default.fourth-configmap created\n" { + configmapEvent = true + } + } + return configmapEvent + }, timeout, interval).Should(BeTrue()) + + repository.Status.Artifact.Revision = "v5" + Expect(k8sClient.Status().Update(context.Background(), repository)).To(Succeed()) + + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), kName, got) + return got.Status.LastAppliedRevision == repository.Status.Artifact.Revision + }, timeout, time.Second).Should(BeTrue()) + + Consistently(func() bool { + eventList := &eventv1beta1.EventList{} + labelSelector := labels.NewSelector() + requirement, err := labels.NewRequirement("name", selection.Equals, []string{k.Name}) + Expect(err).NotTo(HaveOccurred()) + labelSelector.Add(*requirement) + err = k8sClient.List(context.Background(), eventList, &client.ListOptions{LabelSelector: labelSelector}) + Expect(err).NotTo(HaveOccurred(), "could not list Kustomization events") + + configmapEvent := false + for _, event := range eventList.Items { + if event.Note == "v1.ConfigMap.default.fourth-configmap created\n" { + configmapEvent = true + } + } + return configmapEvent + }, timeout, interval).Should(BeTrue()) + }) }) }) }) diff --git a/controllers/utils.go b/controllers/utils.go index b384ada8..cfab919f 100644 --- a/controllers/utils.go +++ b/controllers/utils.go @@ -17,7 +17,12 @@ limitations under the License. package controllers import ( + "context" + "errors" + "fmt" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "os/exec" "sigs.k8s.io/controller-runtime/pkg/client" "strings" ) @@ -61,32 +66,30 @@ func parseApplyOutput(in []byte) map[string]string { // @@ -0,0 +1,36 @@ func parseDiffOutput(in []byte) map[string]string { result := make(map[string]string) - var parts []string - var actions []string + var resourcename string + var action string - input := strings.Split(string(in), "\n") + input := strings.Split(string(in), "diff -u -N") if len(input) == 0 { return result } for _, str := range input { - if strings.Contains(str, "diff -u -N ") { - s := strings.Split(str, "/") - parts = append(parts, s[len(s)-1]) + stringSlice := strings.Split(str, "\n") + s := strings.Split(stringSlice[0], "/") + resourcename = s[len(s)-1] + + if containsChangeInSlice(stringSlice) { + action = "configured" + } else { + action = "unchanged" } - - if strings.Contains(str, "@@") { - if strings.Contains(str, "@@ -0,0") { - actions = append(actions, "created") - - } else { - actions = append(actions, "configured") - } + if strings.Contains(str, "@@ -0,0") { + action = "created" + } + if resourcename != "" { + result[resourcename] = action } - } - - for k, v := range parts { - result[v] = actions[k] } return result @@ -115,6 +118,27 @@ func parseApplyError(in []byte) string { return errors } +func execApply(ctx context.Context, cmd string) ([]byte, error) { + command := exec.CommandContext(ctx, "/bin/sh", "-c", cmd) + output, applyerr := command.CombinedOutput() + if applyerr != nil { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return output, fmt.Errorf("apply timeout: %w", ctx.Err()) + } + + if string(output) == "" { + return output, fmt.Errorf("apply failed: %w, kubectl process was killed, probably due to OOM", applyerr) + } + + applyErr := parseApplyError(output) + if applyErr == "" { + applyErr = "no error output found, this may happen because of a timeout" + } + return output, fmt.Errorf("apply failed: %s", applyErr) + } + return output, nil +} + func containsString(slice []string, s string) bool { for _, item := range slice { if item == s { @@ -131,3 +155,16 @@ func ObjectKey(object metav1.Object) client.ObjectKey { Name: object.GetName(), } } + +func containsChangeInSlice(tmpslice []string) bool { + checksumAnnotation := fmt.Sprintf(" %s/checksum:", kustomizev1.GroupVersion.Group) + for _, s := range tmpslice { + if strings.HasPrefix(s, "+"+checksumAnnotation) || strings.HasPrefix(s, "-"+checksumAnnotation) || strings.HasPrefix(s, "--- /tmp") || strings.HasPrefix(s, "+++ /tmp") { + continue + } + if strings.Contains(s, "- ") || strings.Contains(s, "+ ") { + return true + } + } + return false +}