Skip to content

Commit d310c8b

Browse files
authored
Merge pull request #822 from fluxcd/correct-drift-apply
Correct cluster drift using patches
2 parents 113bf54 + 0131f22 commit d310c8b

13 files changed

+1207
-228
lines changed

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ replace (
1515
)
1616

1717
require (
18+
github.com/fluxcd/cli-utils v0.36.0-flux.1
1819
github.com/fluxcd/helm-controller/api v0.36.2
1920
github.com/fluxcd/pkg/apis/acl v0.1.0
2021
github.com/fluxcd/pkg/apis/event v0.6.0
2122
github.com/fluxcd/pkg/apis/kustomize v1.2.0
2223
github.com/fluxcd/pkg/apis/meta v1.2.0
2324
github.com/fluxcd/pkg/runtime v0.43.0
24-
github.com/fluxcd/pkg/ssa v0.34.0
25+
github.com/fluxcd/pkg/ssa v0.35.0
2526
github.com/fluxcd/pkg/testserver v0.5.0
2627
github.com/fluxcd/source-controller/api v1.1.2
2728
github.com/go-logr/logr v1.3.0
@@ -77,7 +78,6 @@ require (
7778
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
7879
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
7980
github.com/fatih/color v1.13.0 // indirect
80-
github.com/fluxcd/cli-utils v0.36.0-flux.1 // indirect
8181
github.com/fsnotify/fsnotify v1.6.0 // indirect
8282
github.com/go-errors/errors v1.4.2 // indirect
8383
github.com/go-gorp/gorp/v3 v3.1.0 // indirect

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ github.com/fluxcd/pkg/apis/meta v1.2.0 h1:O766PzGAdMdQKybSflGL8oV0+GgCNIkdsxfalR
100100
github.com/fluxcd/pkg/apis/meta v1.2.0/go.mod h1:fU/Az9AoVyIxC0oI4ihG0NVMNnvrcCzdEym3wxjIQsc=
101101
github.com/fluxcd/pkg/runtime v0.43.0 h1:dU4cWct5VTpddGzJUU80zxNl80jbbVEN5Y5rbt4YUnw=
102102
github.com/fluxcd/pkg/runtime v0.43.0/go.mod h1:RuqJ9VEXELjzgurK2+UXBBgVN1vS0hZ7CYVG2xBAEVM=
103-
github.com/fluxcd/pkg/ssa v0.34.0 h1:hpMo0D7G3faieRYH39e9YD8Jl+aC2hTgUep8ojG5+LE=
104-
github.com/fluxcd/pkg/ssa v0.34.0/go.mod h1:rhVh0EtYVUOznKXlz6E7JOSgdc8xWbIwA4L5HVtJRLA=
103+
github.com/fluxcd/pkg/ssa v0.35.0 h1:8T3WY4P9SQWApa2hq1rU1u2WE8oqP3MMTsAiEWwhmfo=
104+
github.com/fluxcd/pkg/ssa v0.35.0/go.mod h1:rhVh0EtYVUOznKXlz6E7JOSgdc8xWbIwA4L5HVtJRLA=
105105
github.com/fluxcd/pkg/testserver v0.5.0 h1:n/Iskk0tXNt2AgIgjz9qeFK/VhEXGfqeazABXZmO2Es=
106106
github.com/fluxcd/pkg/testserver v0.5.0/go.mod h1:/p4st6d0uPLy8wXydeF/kDJgxUYO9u2NqySuXb9S+Fo=
107107
github.com/fluxcd/source-controller/api v1.1.2 h1:FfKDKVWnopo+Q2pOAxgHEjrtr4MP41L8aapR4mqBhBk=

internal/action/diff.go

+158-3
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,27 @@ package action
1818

1919
import (
2020
"context"
21+
"encoding/json"
22+
"errors"
2123
"fmt"
24+
"sort"
2225
"strings"
2326

2427
helmaction "helm.sh/helm/v3/pkg/action"
2528
helmrelease "helm.sh/helm/v3/pkg/release"
26-
"k8s.io/apimachinery/pkg/util/errors"
29+
apierrors "k8s.io/apimachinery/pkg/api/errors"
30+
"k8s.io/apimachinery/pkg/types"
31+
apierrutil "k8s.io/apimachinery/pkg/util/errors"
2732
"k8s.io/utils/ptr"
2833
"sigs.k8s.io/controller-runtime/pkg/client"
2934
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
3035

36+
"github.com/fluxcd/cli-utils/pkg/object"
3137
"github.com/fluxcd/pkg/ssa"
3238
"github.com/fluxcd/pkg/ssa/jsondiff"
3339

3440
v2 "github.com/fluxcd/helm-controller/api/v2beta2"
41+
"github.com/fluxcd/helm-controller/internal/diff"
3542
)
3643

3744
// Diff returns a jsondiff.DiffSet of the changes between the state of the
@@ -61,6 +68,11 @@ func Diff(ctx context.Context, config *helmaction.Configuration, rls *helmreleas
6168
errs []error
6269
)
6370
for _, obj := range objects {
71+
// Set the Helm metadata on the object which is normally set by Helm
72+
// during object creation.
73+
setHelmMetadata(obj, rls)
74+
75+
// Set the namespace of the object if it is not set.
6476
if obj.GetNamespace() == "" {
6577
// Manifest does not contain the namespace of the release.
6678
// Figure out if the object is namespaced if the namespace is not
@@ -86,7 +98,6 @@ func Diff(ctx context.Context, config *helmaction.Configuration, rls *helmreleas
8698
diffOpts := []jsondiff.ListOption{
8799
jsondiff.FieldOwner(fieldOwner),
88100
jsondiff.ExclusionSelector{v2.DriftDetectionMetadataKey: v2.DriftDetectionDisabledValue},
89-
jsondiff.MaskSecrets(true),
90101
jsondiff.Rationalize(true),
91102
jsondiff.Graceful(true),
92103
}
@@ -119,5 +130,149 @@ func Diff(ctx context.Context, config *helmaction.Configuration, rls *helmreleas
119130
if err != nil {
120131
errs = append(errs, err)
121132
}
122-
return set, errors.Reduce(errors.Flatten(errors.NewAggregate(errs)))
133+
return set, apierrutil.Reduce(apierrutil.Flatten(apierrutil.NewAggregate(errs)))
134+
}
135+
136+
// ApplyDiff applies the changes described in the provided jsondiff.DiffSet to
137+
// the Kubernetes cluster.
138+
func ApplyDiff(ctx context.Context, config *helmaction.Configuration, diffSet jsondiff.DiffSet, fieldOwner string) (*ssa.ChangeSet, error) {
139+
cfg, err := config.RESTClientGetter.ToRESTConfig()
140+
if err != nil {
141+
return nil, err
142+
}
143+
c, err := client.New(cfg, client.Options{})
144+
if err != nil {
145+
return nil, err
146+
}
147+
148+
var toCreate, toPatch sortableDiffs
149+
for _, d := range diffSet {
150+
switch d.Type {
151+
case jsondiff.DiffTypeCreate:
152+
toCreate = append(toCreate, d)
153+
case jsondiff.DiffTypeUpdate:
154+
toPatch = append(toPatch, d)
155+
}
156+
}
157+
158+
var (
159+
changeSet = ssa.NewChangeSet()
160+
errs []error
161+
)
162+
163+
sort.Sort(toCreate)
164+
for _, d := range toCreate {
165+
obj := d.DesiredObject.DeepCopyObject().(client.Object)
166+
if err := c.Create(ctx, obj, client.FieldOwner(fieldOwner)); err != nil {
167+
errs = append(errs, fmt.Errorf("%s creation failure: %w", diff.ResourceName(obj), err))
168+
continue
169+
}
170+
changeSet.Add(objectToChangeSetEntry(obj, ssa.CreatedAction))
171+
}
172+
173+
sort.Sort(toPatch)
174+
for _, d := range toPatch {
175+
data, err := json.Marshal(d.Patch)
176+
if err != nil {
177+
errs = append(errs, fmt.Errorf("%s patch failure: %w", diff.ResourceName(d.DesiredObject), err))
178+
continue
179+
}
180+
181+
obj := d.DesiredObject.DeepCopyObject().(client.Object)
182+
patch := client.RawPatch(types.JSONPatchType, data)
183+
if err := c.Patch(ctx, obj, patch, client.FieldOwner(fieldOwner)); err != nil {
184+
if obj.GetObjectKind().GroupVersionKind().Kind == "Secret" {
185+
err = maskSensitiveErrData(err)
186+
}
187+
errs = append(errs, fmt.Errorf("%s patch failure: %w", diff.ResourceName(obj), err))
188+
continue
189+
}
190+
changeSet.Add(objectToChangeSetEntry(obj, ssa.ConfiguredAction))
191+
}
192+
193+
return changeSet, apierrutil.NewAggregate(errs)
194+
}
195+
196+
const (
197+
appManagedByLabel = "app.kubernetes.io/managed-by"
198+
appManagedByHelm = "Helm"
199+
helmReleaseNameAnnotation = "meta.helm.sh/release-name"
200+
helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace"
201+
)
202+
203+
// setHelmMetadata sets the metadata on the given object to indicate that it is
204+
// managed by Helm. This is safe to do, because we apply it to objects that
205+
// originate from the Helm release itself.
206+
// xref: https://github.com/helm/helm/blob/v3.13.2/pkg/action/validate.go
207+
// xref: https://github.com/helm/helm/blob/v3.13.2/pkg/action/rollback.go#L186-L191
208+
func setHelmMetadata(obj client.Object, rls *helmrelease.Release) {
209+
labels := obj.GetLabels()
210+
if labels == nil {
211+
labels = make(map[string]string, 1)
212+
}
213+
labels[appManagedByLabel] = appManagedByHelm
214+
obj.SetLabels(labels)
215+
216+
annotations := obj.GetAnnotations()
217+
if annotations == nil {
218+
annotations = make(map[string]string, 2)
219+
}
220+
annotations[helmReleaseNameAnnotation] = rls.Name
221+
annotations[helmReleaseNamespaceAnnotation] = rls.Namespace
222+
obj.SetAnnotations(annotations)
223+
}
224+
225+
// objectToChangeSetEntry returns a ssa.ChangeSetEntry for the given object and
226+
// action.
227+
func objectToChangeSetEntry(obj client.Object, action ssa.Action) ssa.ChangeSetEntry {
228+
return ssa.ChangeSetEntry{
229+
ObjMetadata: object.ObjMetadata{
230+
GroupKind: obj.GetObjectKind().GroupVersionKind().GroupKind(),
231+
Name: obj.GetName(),
232+
Namespace: obj.GetNamespace(),
233+
},
234+
GroupVersion: obj.GetObjectKind().GroupVersionKind().Version,
235+
Subject: diff.ResourceName(obj),
236+
Action: action,
237+
}
238+
}
239+
240+
// maskSensitiveErrData masks potentially sensitive data from the error message
241+
// returned by the Kubernetes API server.
242+
// This avoids leaking any sensitive data in logs or other output when a patch
243+
// operation fails.
244+
func maskSensitiveErrData(err error) error {
245+
if apierrors.IsInvalid(err) {
246+
// The last part of the error message is the reason for the error.
247+
if i := strings.LastIndex(err.Error(), `:`); i != -1 {
248+
err = errors.New(strings.TrimSpace(err.Error()[i+1:]))
249+
}
250+
}
251+
return err
252+
}
253+
254+
// sortableDiffs is a sortable slice of jsondiff.Diffs.
255+
type sortableDiffs []*jsondiff.Diff
256+
257+
// Len returns the length of the slice.
258+
func (s sortableDiffs) Len() int { return len(s) }
259+
260+
// Swap swaps the elements with indexes i and j.
261+
func (s sortableDiffs) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
262+
263+
// Less returns true if the element with index i should sort before the element
264+
// with index j.
265+
// The elements are sorted by GroupKind, Namespace and Name.
266+
func (s sortableDiffs) Less(i, j int) bool {
267+
iDiff, jDiff := s[i], s[j]
268+
269+
if !ssa.Equals(iDiff.GroupVersionKind().GroupKind(), jDiff.GroupVersionKind().GroupKind()) {
270+
return ssa.IsLessThan(iDiff.GroupVersionKind().GroupKind(), jDiff.GroupVersionKind().GroupKind())
271+
}
272+
273+
if iDiff.GetNamespace() != jDiff.GetNamespace() {
274+
return iDiff.GetNamespace() < jDiff.GetNamespace()
275+
}
276+
277+
return iDiff.GetName() < jDiff.GetName()
123278
}

0 commit comments

Comments
 (0)