Skip to content

Commit

Permalink
action: introduce Diff action
Browse files Browse the repository at this point in the history
The `Diff` action can be used to detect changes between the manifest
from a Helm release and the current cluster state.

Compared to the previous diff functionality, it allows for ignoring
specific fields in a resource using the newly introduced ignore rules
in the API.

Signed-off-by: Hidde Beydals <hidde@hhh.computer>
  • Loading branch information
hiddeco committed Nov 24, 2023
1 parent 20ac67e commit dd9d3c0
Show file tree
Hide file tree
Showing 4 changed files with 683 additions and 3 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/fluxcd/pkg/apis/kustomize v1.1.1
github.com/fluxcd/pkg/apis/meta v1.1.2
github.com/fluxcd/pkg/runtime v0.42.0
github.com/fluxcd/pkg/ssa v0.32.0
github.com/fluxcd/pkg/ssa v0.33.1-0.20231026140034-ee4c4a064707
github.com/fluxcd/pkg/testserver v0.4.0
github.com/fluxcd/source-controller/api v1.1.2
github.com/go-logr/logr v1.2.4
Expand All @@ -32,6 +32,7 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/go-digest/blake3 v0.0.0-20230815154656-802ce17c4f59
github.com/spf13/pflag v1.0.5
github.com/wI2L/jsondiff v0.4.1-0.20230626084051-c85fb8ce3cac
golang.org/x/text v0.13.0
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.12.3
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ github.com/fluxcd/pkg/apis/meta v1.1.2 h1:Unjo7hxadtB2dvGpeFqZZUdsjpRA08YYSBb7dF
github.com/fluxcd/pkg/apis/meta v1.1.2/go.mod h1:BHQyRHCskGMEDf6kDGbgQ+cyiNpUHbLsCOsaMYM2maI=
github.com/fluxcd/pkg/runtime v0.42.0 h1:a5DQ/f90YjoHBmiXZUpnp4bDSLORjInbmqP7K11L4uY=
github.com/fluxcd/pkg/runtime v0.42.0/go.mod h1:p6A3xWVV8cKLLQW0N90GehKgGMMmbNYv+OSJ/0qB0vg=
github.com/fluxcd/pkg/ssa v0.32.0 h1:RBqs9DNrbJkFHjpfsiKilyean7gwqWFspSBTLOaBIHs=
github.com/fluxcd/pkg/ssa v0.32.0/go.mod h1:+Kf5euYAbvgJX645bo+IL7V/NlH0X7kGgFTr1W++I3c=
github.com/fluxcd/pkg/ssa v0.33.1-0.20231026140034-ee4c4a064707 h1:pW3qg+r+yEXUgVm3uU5ZrW9zAVFGgPTkKbbrmW1/O8k=
github.com/fluxcd/pkg/ssa v0.33.1-0.20231026140034-ee4c4a064707/go.mod h1:0SRSYkqJKuX2nSVzVOvzpw5ax31r4fpO1nKrv7oapDQ=
github.com/fluxcd/pkg/testserver v0.4.0 h1:pDZ3gistqYhwlf3sAjn1Q8NzN4Qe6I1BEmHMHi46lMg=
github.com/fluxcd/pkg/testserver v0.4.0/go.mod h1:gjOKX41okmrGYOa4oOF2fiLedDAfPo1XaG/EzrUUGBI=
github.com/fluxcd/source-controller/api v1.1.2 h1:FfKDKVWnopo+Q2pOAxgHEjrtr4MP41L8aapR4mqBhBk=
Expand Down Expand Up @@ -613,6 +613,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
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=
Expand Down
123 changes: 123 additions & 0 deletions internal/action/diff.go
Original file line number Diff line number Diff line change
@@ -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)))
}
Loading

0 comments on commit dd9d3c0

Please sign in to comment.