diff --git a/api/v2beta2/helmrelease_types.go b/api/v2beta2/helmrelease_types.go index 12cdd4173..19d49937f 100644 --- a/api/v2beta2/helmrelease_types.go +++ b/api/v2beta2/helmrelease_types.go @@ -158,6 +158,12 @@ type HelmReleaseSpec struct { // +optional PersistentClient *bool `json:"persistentClient,omitempty"` + // DriftDetection holds the configuration for detecting and handling + // differences between the manifest in the Helm storage and the resources + // currently existing in the cluster. + // +optional + DriftDetection *DriftDetection `json:"driftDetection,omitempty"` + // Install holds the configuration for Helm install actions for this HelmRelease. // +optional Install *Install `json:"install,omitempty"` @@ -192,6 +198,91 @@ type HelmReleaseSpec struct { PostRenderers []PostRenderer `json:"postRenderers,omitempty"` } +// DriftDetectionMode represents the modes in which a controller can detect and +// handle differences between the manifest in the Helm storage and the resources +// currently existing in the cluster. +type DriftDetectionMode string + +var ( + // DriftDetectionEnabled instructs the controller to actively detect any + // changes between the manifest in the Helm storage and the resources + // currently existing in the cluster. + // If any differences are detected, the controller will automatically + // correct the cluster state by performing a Helm upgrade. + DriftDetectionEnabled DriftDetectionMode = "enabled" + + // DriftDetectionWarn instructs the controller to actively detect any + // changes between the manifest in the Helm storage and the resources + // currently existing in the cluster. + // If any differences are detected, the controller will emit a warning + // without automatically correcting the cluster state. + DriftDetectionWarn DriftDetectionMode = "warn" + + // DriftDetectionDisabled instructs the controller to skip detection of + // differences entirely. + // This is the default behavior, and the controller will not actively + // detect or respond to differences between the manifest in the Helm + // storage and the resources currently existing in the cluster. + DriftDetectionDisabled DriftDetectionMode = "disabled" +) + +var ( + // DriftDetectionMetadataKey is the label or annotation key used to disable + // the diffing of an object. + DriftDetectionMetadataKey = GroupVersion.Group + "/driftDetection" + // DriftDetectionDisabledValue is the value used to disable the diffing of + // an object using DriftDetectionMetadataKey. + DriftDetectionDisabledValue = "disabled" +) + +// IgnoreRule defines a rule to selectively disregard specific changes during +// the drift detection process. +type IgnoreRule struct { + // Paths is a list of JSON Pointer (RFC 6901) paths to be excluded from + // consideration in a Kubernetes object. + // +required + Paths []string `json:"paths"` + + // Target is a selector for specifying Kubernetes objects to which this + // rule applies. + // If Target is not set, the Paths will be ignored for all Kubernetes + // objects within the manifest of the Helm release. + // +optional + Target *kustomize.Selector `json:"target,omitempty"` +} + +// DriftDetection defines the strategy for performing differential analysis and +// provides a way to define rules for ignoring specific changes during this +// process. +type DriftDetection struct { + // Mode defines how differences should be handled between the Helm manifest + // and the manifest currently applied to the cluster. + // If not explicitly set, it defaults to DiffModeDisabled. + // +kubebuilder:validation:Enum=enabled;warn;disabled + // +optional + Mode DriftDetectionMode `json:"mode,omitempty"` + + // Ignore contains a list of rules for specifying which changes to ignore + // during diffing. + // +optional + Ignore []IgnoreRule `json:"ignore,omitempty"` +} + +// GetMode returns the DiffMode set on the Diff, or DiffModeDisabled if not +// set. +func (d DriftDetection) GetMode() DriftDetectionMode { + if d.Mode == "" { + return DriftDetectionDisabled + } + return d.Mode +} + +// MustDetectChanges returns true if the DiffMode is set to DiffModeEnabled or +// DiffModeWarn. +func (d DriftDetection) MustDetectChanges() bool { + return d.GetMode() == DriftDetectionEnabled || d.GetMode() == DriftDetectionWarn +} + // HelmChartTemplate defines the template from which the controller will // generate a v1beta2.HelmChart object in the same namespace as the referenced // v1.Source. @@ -970,6 +1061,16 @@ type HelmRelease struct { Status HelmReleaseStatus `json:"status,omitempty"` } +// GetDriftDetection returns the configuration for detecting and handling +// differences between the manifest in the Helm storage and the resources +// currently existing in the cluster. +func (in *HelmRelease) GetDriftDetection() DriftDetection { + if in.Spec.DriftDetection == nil { + return DriftDetection{} + } + return *in.Spec.DriftDetection +} + // GetInstall returns the configuration for Helm install actions for the // HelmRelease. func (in *HelmRelease) GetInstall() Install { diff --git a/api/v2beta2/zz_generated.deepcopy.go b/api/v2beta2/zz_generated.deepcopy.go index a2f3b8f65..f58cdc02f 100644 --- a/api/v2beta2/zz_generated.deepcopy.go +++ b/api/v2beta2/zz_generated.deepcopy.go @@ -44,6 +44,28 @@ func (in *CrossNamespaceObjectReference) DeepCopy() *CrossNamespaceObjectReferen return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DriftDetection) DeepCopyInto(out *DriftDetection) { + *out = *in + if in.Ignore != nil { + in, out := &in.Ignore, &out.Ignore + *out = make([]IgnoreRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DriftDetection. +func (in *DriftDetection) DeepCopy() *DriftDetection { + if in == nil { + return nil + } + out := new(DriftDetection) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Filter) DeepCopyInto(out *Filter) { *out = *in @@ -249,6 +271,11 @@ func (in *HelmReleaseSpec) DeepCopyInto(out *HelmReleaseSpec) { *out = new(bool) **out = **in } + if in.DriftDetection != nil { + in, out := &in.DriftDetection, &out.DriftDetection + *out = new(DriftDetection) + (*in).DeepCopyInto(*out) + } if in.Install != nil { in, out := &in.Install, &out.Install *out = new(Install) @@ -337,6 +364,31 @@ func (in *HelmReleaseStatus) DeepCopy() *HelmReleaseStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IgnoreRule) DeepCopyInto(out *IgnoreRule) { + *out = *in + if in.Paths != nil { + in, out := &in.Paths, &out.Paths + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(kustomize.Selector) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IgnoreRule. +func (in *IgnoreRule) DeepCopy() *IgnoreRule { + if in == nil { + return nil + } + out := new(IgnoreRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Install) DeepCopyInto(out *Install) { *out = *in diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml index 7a17c4441..757d39530 100644 --- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml +++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml @@ -1221,6 +1221,80 @@ spec: - name type: object type: array + driftDetection: + description: DriftDetection holds the configuration for detecting + and handling differences between the manifest in the Helm storage + and the resources currently existing in the cluster. + properties: + ignore: + description: Ignore contains a list of rules for specifying which + changes to ignore during diffing. + items: + description: IgnoreRule defines a rule to selectively disregard + specific changes during the drift detection process. + properties: + paths: + description: Paths is a list of JSON Pointer (RFC 6901) + paths to be excluded from consideration in a Kubernetes + object. + items: + type: string + type: array + target: + description: Target is a selector for specifying Kubernetes + objects to which this rule applies. If Target is not set, + the Paths will be ignored for all Kubernetes objects within + the manifest of the Helm release. + properties: + annotationSelector: + description: AnnotationSelector is a string that follows + the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource annotations. + type: string + group: + description: Group is the API group to select resources + from. Together with Version and Kind it is capable + of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + kind: + description: Kind of the API Group to select resources + from. Together with Group and Version it is capable + of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + labelSelector: + description: LabelSelector is a string that follows + the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource labels. + type: string + name: + description: Name to match resources with. + type: string + namespace: + description: Namespace to select resources from. + type: string + version: + description: Version of the API Group to select resources + from. Together with Group and Kind it is capable of + unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + type: object + required: + - paths + type: object + type: array + mode: + description: Mode defines how differences should be handled between + the Helm manifest and the manifest currently applied to the + cluster. If not explicitly set, it defaults to DiffModeDisabled. + enum: + - enabled + - warn + - disabled + type: string + type: object install: description: Install holds the configuration for Helm install actions for this HelmRelease. diff --git a/docs/api/v2beta2/helm.md b/docs/api/v2beta2/helm.md index bd2677ab2..49216b3a8 100644 --- a/docs/api/v2beta2/helm.md +++ b/docs/api/v2beta2/helm.md @@ -246,6 +246,22 @@ available by e.g. post-install hooks.
driftDetection
DriftDetection holds the configuration for detecting and handling +differences between the manifest in the Helm storage and the resources +currently existing in the cluster.
+install
+(Appears on: +HelmReleaseSpec) +
+DriftDetection defines the strategy for performing differential analysis and +provides a way to define rules for ignoring specific changes during this +process.
+Field | +Description | +
---|---|
+mode + + +DriftDetectionMode + + + |
+
+(Optional)
+ Mode defines how differences should be handled between the Helm manifest +and the manifest currently applied to the cluster. +If not explicitly set, it defaults to DiffModeDisabled. + |
+
+ignore + + +[]IgnoreRule + + + |
+
+(Optional)
+ Ignore contains a list of rules for specifying which changes to ignore +during diffing. + |
+
string
alias)+(Appears on: +DriftDetection) +
+DriftDetectionMode represents the modes in which a controller can detect and +handle differences between the manifest in the Helm storage and the resources +currently existing in the cluster.
@@ -1098,6 +1177,22 @@ available by e.g. post-install hooks.
driftDetection
DriftDetection holds the configuration for detecting and handling +differences between the manifest in the Helm storage and the resources +currently existing in the cluster.
+install
+(Appears on: +DriftDetection) +
+IgnoreRule defines a rule to selectively disregard specific changes during +the drift detection process.
+Field | +Description | +
---|---|
+paths + +[]string + + |
+
+ Paths is a list of JSON Pointer (RFC 6901) paths to be excluded from +consideration in a Kubernetes object. + |
+
+target + + +github.com/fluxcd/pkg/apis/kustomize.Selector + + + |
+
+(Optional)
+ Target is a selector for specifying Kubernetes objects to which this +rule applies. +If Target is not set, the Paths will be ignored for all Kubernetes +objects within the manifest of the Helm release. + |
+
diff --git a/go.mod b/go.mod
index bc788da16..1d7dddf98 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,6 @@ replace (
)
require (
- github.com/fluxcd/cli-utils v0.36.0-flux.1
github.com/fluxcd/helm-controller/api v0.36.2
github.com/fluxcd/pkg/apis/acl v0.1.0
github.com/fluxcd/pkg/apis/event v0.6.0
@@ -33,6 +32,7 @@ require (
github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98
github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98
github.com/spf13/pflag v1.0.5
+ github.com/wI2L/jsondiff v0.4.1-0.20230626084051-c85fb8ce3cac
golang.org/x/text v0.14.0
helm.sh/helm/v3 v3.13.2
k8s.io/api v0.28.4
@@ -77,6 +77,7 @@ require (
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fatih/color v1.13.0 // indirect
+ github.com/fluxcd/cli-utils v0.36.0-flux.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
diff --git a/go.sum b/go.sum
index eec871cf6..5604a3a98 100644
--- a/go.sum
+++ b/go.sum
@@ -369,6 +369,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/wI2L/jsondiff v0.4.1-0.20230626084051-c85fb8ce3cac h1:X+MGDuQHQ2i4UoSsb2n4dESJoSCg7aTfvtk6Bj7nlcE=
+github.com/wI2L/jsondiff v0.4.1-0.20230626084051-c85fb8ce3cac/go.mod h1:nR/vyy1efuDeAtMwc3AF6nZf/2LD1ID8GTyyJ+K8YB0=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
diff --git a/internal/action/diff.go b/internal/action/diff.go
new file mode 100644
index 000000000..bbe49774a
--- /dev/null
+++ b/internal/action/diff.go
@@ -0,0 +1,123 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ helmaction "helm.sh/helm/v3/pkg/action"
+ helmrelease "helm.sh/helm/v3/pkg/release"
+ "k8s.io/apimachinery/pkg/util/errors"
+ "k8s.io/utils/pointer"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
+
+ "github.com/fluxcd/pkg/ssa"
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2beta2"
+)
+
+// Diff returns a jsondiff.DiffSet of the changes between the state of the
+// cluster and the Helm release.Release manifest.
+func Diff(ctx context.Context, config *helmaction.Configuration, rls *helmrelease.Release, fieldOwner string, ignore ...v2.IgnoreRule) (jsondiff.DiffSet, error) {
+ // Create a dry-run only client to use solely for diffing.
+ cfg, err := config.RESTClientGetter.ToRESTConfig()
+ if err != nil {
+ return nil, err
+ }
+ c, err := client.New(cfg, client.Options{DryRun: pointer.Bool(true)})
+ if err != nil {
+ return nil, err
+ }
+
+ // Read the release manifest and normalize the objects.
+ objects, err := ssa.ReadObjects(strings.NewReader(rls.Manifest))
+ if err != nil {
+ return nil, fmt.Errorf("failed to read objects from release manifest: %w", err)
+ }
+ if err = ssa.NormalizeUnstructuredListWithScheme(objects, c.Scheme()); err != nil {
+ return nil, fmt.Errorf("failed to normalize release objects: %w", err)
+ }
+
+ var (
+ isNamespacedGVK = map[string]bool{}
+ errs []error
+ )
+ for _, obj := range objects {
+ if obj.GetNamespace() == "" {
+ // Manifest does not contain the namespace of the release.
+ // Figure out if the object is namespaced if the namespace is not
+ // explicitly set, and configure the namespace accordingly.
+ objGVK := obj.GetObjectKind().GroupVersionKind().String()
+ if _, ok := isNamespacedGVK[objGVK]; !ok {
+ namespaced, err := apiutil.IsObjectNamespaced(obj, c.Scheme(), c.RESTMapper())
+ if err != nil {
+ errs = append(errs, fmt.Errorf("failed to determine if %s is namespace scoped: %w",
+ obj.GetObjectKind().GroupVersionKind().Kind, err))
+ continue
+ }
+ // Cache the result, so we don't have to do this for every object
+ isNamespacedGVK[objGVK] = namespaced
+ }
+ if isNamespacedGVK[objGVK] {
+ obj.SetNamespace(rls.Namespace)
+ }
+ }
+ }
+
+ // Base configuration for the diffing of the object.
+ diffOpts := []jsondiff.ListOption{
+ jsondiff.FieldOwner(fieldOwner),
+ jsondiff.ExclusionSelector{v2.DriftDetectionMetadataKey: v2.DriftDetectionDisabledValue},
+ jsondiff.MaskSecrets(true),
+ jsondiff.Rationalize(true),
+ jsondiff.Graceful(true),
+ }
+
+ // Add ignore rules to the diffing configuration.
+ var ignoreRules jsondiff.IgnoreRules
+ for _, rule := range ignore {
+ r := jsondiff.IgnoreRule{
+ Paths: rule.Paths,
+ }
+ if rule.Target != nil {
+ r.Selector = &jsondiff.Selector{
+ Group: rule.Target.Group,
+ Version: rule.Target.Version,
+ Kind: rule.Target.Kind,
+ Name: rule.Target.Name,
+ Namespace: rule.Target.Namespace,
+ AnnotationSelector: rule.Target.AnnotationSelector,
+ LabelSelector: rule.Target.LabelSelector,
+ }
+ }
+ ignoreRules = append(ignoreRules, r)
+ }
+ if len(ignoreRules) > 0 {
+ diffOpts = append(diffOpts, ignoreRules)
+ }
+
+ // Actually diff the objects.
+ set, err := jsondiff.UnstructuredList(ctx, c, objects, diffOpts...)
+ if err != nil {
+ errs = append(errs, err)
+ }
+ return set, errors.Reduce(errors.Flatten(errors.NewAggregate(errs)))
+}
diff --git a/internal/action/diff_test.go b/internal/action/diff_test.go
new file mode 100644
index 000000000..e3d71e3cb
--- /dev/null
+++ b/internal/action/diff_test.go
@@ -0,0 +1,554 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ extjsondiff "github.com/wI2L/jsondiff"
+ helmaction "helm.sh/helm/v3/pkg/action"
+ helmrelease "helm.sh/helm/v3/pkg/release"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/rest"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+
+ "github.com/fluxcd/pkg/apis/kustomize"
+ "github.com/fluxcd/pkg/ssa"
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2beta2"
+ "github.com/fluxcd/helm-controller/internal/kube"
+)
+
+func TestDiff(t *testing.T) {
+ // Normally, we would create e.g. a `suite_test.go` file with a `TestMain`
+ // function. But because this is the only test in this package which needs
+ // a test cluster, we create it here instead.
+ config, cleanup := newTestCluster(t)
+ t.Cleanup(func() {
+ t.Log("Stopping the test environment")
+ if err := cleanup(); err != nil {
+ t.Logf("Failed to stop the test environment: %v", err)
+ }
+ })
+
+ // Construct a REST client getter for Helm's action configuration.
+ getter := kube.NewMemoryRESTClientGetter(config)
+
+ // Construct a client for to be able to mutate the cluster.
+ c, err := client.New(config, client.Options{})
+ if err != nil {
+ t.Fatalf("Failed to create client for test environment: %v", err)
+ }
+
+ const testOwner = "helm-controller"
+
+ tests := []struct {
+ name string
+ manifest string
+ ignoreRules []v2.IgnoreRule
+ mutateCluster func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error)
+ want func(namepace string) jsondiff.DiffSet
+ wantErr bool
+ }{
+ {
+ name: "detects drift",
+ manifest: `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: changed
+data:
+ key: value
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: deleted
+data:
+ key: value
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: unchanged
+data:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ if obj.GetName() == "deleted" {
+ continue
+ }
+
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+
+ if obj.GetName() == "changed" {
+ if err := unstructured.SetNestedField(obj.Object, "changed", "data", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ }
+
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ want: func(namespace string) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "ConfigMap",
+ },
+ Namespace: namespace,
+ Name: "changed",
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ OldValue: "changed",
+ Value: "value",
+ Path: "/data/key",
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeCreate,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "ConfigMap",
+ },
+ Namespace: namespace,
+ Name: "deleted",
+ },
+ {
+ Type: jsondiff.DiffTypeNone,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "ConfigMap",
+ },
+ Namespace: namespace,
+ Name: "unchanged",
+ },
+ }
+ },
+ },
+ {
+ name: "empty release manifest",
+ manifest: "",
+ },
+ {
+ name: "manifest with disabled annotation",
+ manifest: fmt.Sprintf(`---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: disabled
+ annotations:
+ %[1]s: %[2]s
+data:
+ key: value`, v2.DriftDetectionMetadataKey, v2.DriftDetectionDisabledValue),
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+ if err := unstructured.SetNestedField(obj.Object, "changed", "data", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ want: func(namespace string) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeExclude,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "ConfigMap",
+ },
+ Namespace: namespace,
+ Name: "disabled",
+ },
+ }
+ },
+ },
+ {
+ name: "manifest with disabled annotation",
+ manifest: fmt.Sprintf(`---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: disabled
+ labels:
+ %[1]s: %[2]s
+data:
+ key: value`, v2.DriftDetectionMetadataKey, v2.DriftDetectionDisabledValue),
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+ if err := unstructured.SetNestedField(obj.Object, "changed", "data", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ want: func(namespace string) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeExclude,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "ConfigMap",
+ },
+ Namespace: namespace,
+ Name: "disabled",
+ },
+ }
+ },
+ },
+ {
+ name: "adheres to ignore rules",
+ manifest: `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: fully-ignored
+data:
+ key: value
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: partially-ignored
+stringData:
+ key: value
+ otherKey: otherValue
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: globally-ignored
+stringData:
+ globalKey: globalValue
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: not-ignored
+data:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+
+ switch obj.GetName() {
+ case "fully-ignored", "not-ignored":
+ if err := unstructured.SetNestedField(obj.Object, "changed", "data", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ case "partially-ignored":
+ if err := unstructured.SetNestedField(obj.Object, "changed", "stringData", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ if err := unstructured.SetNestedField(obj.Object, "changed", "stringData", "otherKey"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ case "globally-ignored":
+ if err := unstructured.SetNestedField(obj.Object, "changed", "stringData", "globalKey"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ ignoreRules: []v2.IgnoreRule{
+ {Target: &kustomize.Selector{Name: "fully-ignored"}, Paths: []string{""}},
+ {Target: &kustomize.Selector{Name: "partially-ignored"}, Paths: []string{"/data/key"}},
+ {Paths: []string{"/data/globalKey"}},
+ },
+ want: func(namespace string) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeExclude,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "ConfigMap",
+ },
+ Namespace: namespace,
+ Name: "fully-ignored",
+ },
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "Secret",
+ },
+ Namespace: namespace,
+ Name: "partially-ignored",
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ Path: "/data/otherKey",
+ OldValue: "*** (before)",
+ Value: "*** (after)",
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeNone,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "Secret",
+ },
+ Namespace: namespace,
+ Name: "globally-ignored",
+ },
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "ConfigMap",
+ },
+ Namespace: namespace,
+ Name: "not-ignored",
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ Path: "/data/key",
+ OldValue: "changed",
+ Value: "value",
+ },
+ },
+ },
+ }
+ },
+ },
+ {
+ name: "configures namespace",
+ manifest: `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: without-namespace
+data:
+ key: value
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: with-namespace
+ namespace: diff-fixed-ns
+data:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+
+ otherNS := unstructured.Unstructured{Object: map[string]interface{}{
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": map[string]interface{}{
+ "name": "diff-fixed-ns",
+ },
+ }}
+ clusterObjs = append(clusterObjs, &otherNS)
+
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ if obj.GetNamespace() == "" {
+ obj.SetNamespace(namespace)
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ want: func(namespace string) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeNone,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "ConfigMap",
+ },
+ Namespace: namespace,
+ Name: "without-namespace",
+ },
+ {
+ Type: jsondiff.DiffTypeNone,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "ConfigMap",
+ },
+ Namespace: "diff-fixed-ns",
+ Name: "with-namespace",
+ },
+ }
+ },
+ },
+ {
+ name: "masks Secret data",
+ manifest: `---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: secret
+stringData:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+ if err := unstructured.SetNestedField(obj.Object, "changed", "stringData", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ want: func(namespace string) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ GroupVersionKind: schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "Secret",
+ },
+ Namespace: namespace,
+ Name: "secret",
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ Path: "/data/key",
+ OldValue: "*** (before)",
+ Value: "*** (after)",
+ },
+ },
+ },
+ }
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ t.Cleanup(cancel)
+
+ ns, err := generateNamespace(ctx, c, "diff-action")
+ if err != nil {
+ t.Fatalf("Failed to generate namespace: %v", err)
+ }
+ t.Cleanup(func() {
+ if err := c.Delete(context.Background(), ns); client.IgnoreNotFound(err) != nil {
+ t.Logf("Failed to delete generated namespace: %v", err)
+ }
+ })
+
+ objs, err := ssa.ReadObjects(strings.NewReader(tt.manifest))
+ if err != nil {
+ t.Fatalf("Failed to read release objects: %v", err)
+ }
+
+ clusterObjs := objs
+ if tt.mutateCluster != nil {
+ if clusterObjs, err = tt.mutateCluster(objs, ns.Name); err != nil {
+ t.Fatalf("Failed to modify cluster resource: %v", err)
+ }
+ }
+
+ t.Cleanup(func() {
+ for _, obj := range clusterObjs {
+ if err := c.Delete(context.Background(), obj); client.IgnoreNotFound(err) != nil {
+ t.Logf("Failed to delete object: %v", err)
+ }
+ }
+ })
+
+ for _, obj := range clusterObjs {
+ if err = ssa.NormalizeUnstructured(obj); err != nil {
+ t.Fatalf("Failed to normalize cluster manifest: %v", err)
+ }
+ if err := c.Create(ctx, obj, client.FieldOwner(testOwner)); err != nil {
+ t.Fatalf("Failed to create object: %v", err)
+ }
+ }
+
+ rls := &helmrelease.Release{Namespace: ns.Name, Manifest: tt.manifest}
+
+ got, err := Diff(ctx, &helmaction.Configuration{RESTClientGetter: getter}, rls, testOwner, tt.ignoreRules...)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Diff() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+
+ var want jsondiff.DiffSet
+ if tt.want != nil {
+ want = tt.want(ns.Name)
+ }
+ if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(extjsondiff.Operation{})); diff != "" {
+ t.Errorf("Diff() mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
+// newTestCluster creates a new test cluster and returns a rest.Config and a
+// function to stop the test cluster.
+func newTestCluster(t *testing.T) (*rest.Config, func() error) {
+ t.Helper()
+
+ testEnv := &envtest.Environment{}
+
+ t.Log("Starting the test environment")
+ if _, err := testEnv.Start(); err != nil {
+ t.Fatalf("Failed to start the test environment: %v", err)
+ }
+
+ return testEnv.Config, testEnv.Stop
+}
+
+// generateNamespace creates a new namespace with the given generateName and
+// returns the namespace object.
+func generateNamespace(ctx context.Context, c client.Client, generateName string) (*corev1.Namespace, error) {
+ ns := &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ GenerateName: fmt.Sprintf("%s-", generateName),
+ },
+ }
+ if err := c.Create(ctx, ns); err != nil {
+ return nil, err
+ }
+ return ns, nil
+}
diff --git a/internal/diff/differ.go b/internal/diff/differ.go
deleted file mode 100644
index d97d15774..000000000
--- a/internal/diff/differ.go
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
-Copyright 2023 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package diff
-
-import (
- "context"
- "fmt"
- "strings"
-
- "helm.sh/helm/v3/pkg/release"
- "k8s.io/apimachinery/pkg/util/errors"
- ctrl "sigs.k8s.io/controller-runtime"
-
- "github.com/fluxcd/pkg/runtime/client"
- "github.com/fluxcd/pkg/runtime/logger"
- "github.com/fluxcd/pkg/ssa"
-
- v2 "github.com/fluxcd/helm-controller/api/v2beta2"
- "github.com/fluxcd/helm-controller/internal/util"
-)
-
-var (
- // MetadataKey is the label or annotation key used to disable the diffing
- // of an object.
- MetadataKey = v2.GroupVersion.Group + "/driftDetection"
- // MetadataDisabledValue is the value used to disable the diffing of an
- // object using MetadataKey.
- MetadataDisabledValue = "disabled"
-)
-
-type Differ struct {
- impersonator *client.Impersonator
- controllerName string
-}
-
-func NewDiffer(impersonator *client.Impersonator, controllerName string) *Differ {
- return &Differ{
- impersonator: impersonator,
- controllerName: controllerName,
- }
-}
-
-// Manager returns a new ssa.ResourceManager constructed using the client.Impersonator.
-func (d *Differ) Manager(ctx context.Context) (*ssa.ResourceManager, error) {
- c, poller, err := d.impersonator.GetClient(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to get client to configure resource manager: %w", err)
- }
- owner := ssa.Owner{
- Field: d.controllerName,
- }
- return ssa.NewResourceManager(c, poller, owner), nil
-}
-
-func (d *Differ) Diff(ctx context.Context, rel *release.Release) (*ssa.ChangeSet, bool, error) {
- objects, err := ssa.ReadObjects(strings.NewReader(rel.Manifest))
- if err != nil {
- return nil, false, fmt.Errorf("failed to read objects from release manifest: %w", err)
- }
-
- if err := ssa.SetNativeKindsDefaults(objects); err != nil {
- return nil, false, fmt.Errorf("failed to set native kind defaults on release objects: %w", err)
- }
-
- resourceManager, err := d.Manager(ctx)
- if err != nil {
- return nil, false, err
- }
-
- var (
- changeSet = ssa.NewChangeSet()
- isNamespacedGVK = map[string]bool{}
- diff bool
- errs []error
- )
- for _, obj := range objects {
- if obj.GetNamespace() == "" {
- // Manifest does not contain the namespace of the release.
- // Figure out if the object is namespaced if the namespace is not
- // explicitly set, and configure the namespace accordingly.
- objGVK := obj.GetObjectKind().GroupVersionKind().String()
- if _, ok := isNamespacedGVK[objGVK]; !ok {
- namespaced, err := util.IsAPINamespaced(obj, resourceManager.Client().Scheme(), resourceManager.Client().RESTMapper())
- if err != nil {
- errs = append(errs, fmt.Errorf("failed to determine if %s is namespace scoped: %w",
- obj.GetObjectKind().GroupVersionKind().Kind, err))
- continue
- }
- // Cache the result, so we don't have to do this for every object
- isNamespacedGVK[objGVK] = namespaced
- }
- if isNamespacedGVK[objGVK] {
- obj.SetNamespace(rel.Namespace)
- }
- }
-
- entry, releaseObject, clusterObject, err := resourceManager.Diff(ctx, obj, ssa.DiffOptions{
- Exclusions: map[string]string{
- MetadataKey: MetadataDisabledValue,
- },
- })
- if err != nil {
- errs = append(errs, err)
- }
-
- if entry == nil {
- continue
- }
-
- switch entry.Action {
- case ssa.CreatedAction, ssa.ConfiguredAction:
- diff = true
- changeSet.Add(*entry)
-
- if entry.Action == ssa.ConfiguredAction {
- // TODO: remove this once we have a better way to log the diff
- // for example using a custom dyff reporter, or a flux CLI command
- if d, equal := Unstructured(releaseObject, clusterObject, WithoutStatus()); !equal {
- ctrl.LoggerFrom(ctx).V(logger.DebugLevel).Info(entry.Subject + " diff:\n" + d)
- }
- }
- case ssa.SkippedAction:
- changeSet.Add(*entry)
- }
- }
-
- err = errors.Reduce(errors.Flatten(errors.NewAggregate(errs)))
- if len(changeSet.Entries) == 0 {
- return nil, diff, err
- }
- return changeSet, diff, err
-}
diff --git a/internal/diff/differ_test.go b/internal/diff/differ_test.go
deleted file mode 100644
index bc9e53b3f..000000000
--- a/internal/diff/differ_test.go
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
-Copyright 2023 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package diff
-
-import (
- "context"
- "fmt"
- "testing"
-
- . "github.com/onsi/gomega"
- corev1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/meta"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/client/fake"
-
- "github.com/fluxcd/cli-utils/pkg/kstatus/polling"
- "github.com/fluxcd/cli-utils/pkg/object"
- runtimeClient "github.com/fluxcd/pkg/runtime/client"
- "github.com/fluxcd/pkg/ssa"
-
- "helm.sh/helm/v3/pkg/release"
-)
-
-func TestDiffer_Diff(t *testing.T) {
- scheme, mapper := testSchemeWithMapper()
-
- // We do not test all the possible scenarios here, as the ssa package is
- // already tested in depth. We only test the integration with the ssa package.
- tests := []struct {
- name string
- client client.Client
- rel *release.Release
- want *ssa.ChangeSet
- wantDrift bool
- wantErr string
- }{
- {
- name: "manifest read error",
- client: fake.NewClientBuilder().Build(),
- rel: &release.Release{
- Manifest: "invalid",
- },
- wantErr: "failed to read objects from release manifest",
- },
- {
- name: "error on failure to determine namespace scope",
- client: fake.NewClientBuilder().Build(),
- rel: &release.Release{
- Namespace: "release",
- Manifest: `apiVersion: v1
-kind: Secret
-metadata:
- name: test
-stringData:
- foo: bar
-`,
- },
- wantErr: "failed to determine if Secret is namespace scoped",
- },
- {
- name: "detects changes",
- client: fake.NewClientBuilder().
- WithScheme(scheme).
- WithRESTMapper(mapper).
- Build(),
- rel: &release.Release{
- Namespace: "release",
- Manifest: `---
-apiVersion: v1
-kind: Secret
-metadata:
- name: test
-stringData:
- foo: bar
----
-apiVersion: v1
-kind: Secret
-metadata:
- name: test-ns
- namespace: other
-stringData:
- foo: bar
-`,
- },
- want: &ssa.ChangeSet{
- Entries: []ssa.ChangeSetEntry{
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "release",
- Name: "test",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/release/test",
- Action: ssa.CreatedAction,
- },
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "other",
- Name: "test-ns",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/other/test-ns",
- Action: ssa.CreatedAction,
- },
- },
- },
- wantDrift: true,
- },
- {
- name: "ignores exclusions",
- client: fake.NewClientBuilder().
- WithScheme(scheme).
- WithRESTMapper(mapper).
- Build(),
- rel: &release.Release{
- Namespace: "release",
- Manifest: fmt.Sprintf(`---
-apiVersion: v1
-kind: Secret
-metadata:
- name: test
- labels:
- %[1]s: %[2]s
-stringData:
- foo: bar
----
-apiVersion: v1
-kind: Secret
-metadata:
- name: test2
-stringData:
- foo: bar
-`, MetadataKey, MetadataDisabledValue),
- },
- want: &ssa.ChangeSet{
- Entries: []ssa.ChangeSetEntry{
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "release",
- Name: "test",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/release/test",
- Action: ssa.SkippedAction,
- },
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "release",
- Name: "test2",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/release/test2",
- Action: ssa.CreatedAction,
- },
- },
- },
- wantDrift: true,
- },
- {
- name: "ignores exclusions (without diff)",
- client: fake.NewClientBuilder().
- WithScheme(scheme).
- WithRESTMapper(mapper).
- Build(),
- rel: &release.Release{
- Namespace: "release",
- Manifest: fmt.Sprintf(`---
-apiVersion: v1
-kind: Secret
-metadata:
- name: test
- labels:
- %[1]s: %[2]s
-stringData:
- foo: bar`, MetadataKey, MetadataDisabledValue),
- },
- want: &ssa.ChangeSet{
- Entries: []ssa.ChangeSetEntry{
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "release",
- Name: "test",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/release/test",
- Action: ssa.SkippedAction,
- },
- },
- },
- wantDrift: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- g := NewWithT(t)
-
- d := NewDiffer(runtimeClient.NewImpersonator(tt.client, nil, polling.Options{}, nil, runtimeClient.KubeConfigOptions{}, "", "", ""), "test-controller")
- got, drift, err := d.Diff(context.TODO(), tt.rel)
-
- if tt.wantErr != "" {
- g.Expect(err).To(HaveOccurred())
- g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
- } else {
- g.Expect(err).NotTo(HaveOccurred())
- }
-
- g.Expect(got).To(Equal(tt.want))
- g.Expect(drift).To(Equal(tt.wantDrift))
- })
- }
-}
-
-func testSchemeWithMapper() (*runtime.Scheme, meta.RESTMapper) {
- scheme := runtime.NewScheme()
- _ = corev1.AddToScheme(scheme)
- mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
- mapper.Add(corev1.SchemeGroupVersion.WithKind("Secret"), meta.RESTScopeNamespace)
- return scheme, mapper
-}
diff --git a/internal/diff/summarize.go b/internal/diff/summarize.go
new file mode 100644
index 000000000..c553120c7
--- /dev/null
+++ b/internal/diff/summarize.go
@@ -0,0 +1,164 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package diff
+
+import (
+ "fmt"
+ "strings"
+
+ extjsondiff "github.com/wI2L/jsondiff"
+
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+)
+
+// DefaultDiffTypes is the default set of jsondiff.DiffType types to include in
+// summaries.
+var DefaultDiffTypes = []jsondiff.DiffType{
+ jsondiff.DiffTypeCreate,
+ jsondiff.DiffTypeUpdate,
+ jsondiff.DiffTypeExclude,
+}
+
+// SummarizeDiffSet returns a summary of the given DiffSet, including only
+// the given jsondiff.DiffType types. If no types are given, the
+// DefaultDiffTypes set is used.
+//
+// The summary is a string with one line per Diff, in the format:
+// `Kind/namespace/name: