@@ -18,20 +18,27 @@ package action
18
18
19
19
import (
20
20
"context"
21
+ "encoding/json"
22
+ "errors"
21
23
"fmt"
24
+ "sort"
22
25
"strings"
23
26
24
27
helmaction "helm.sh/helm/v3/pkg/action"
25
28
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"
27
32
"k8s.io/utils/ptr"
28
33
"sigs.k8s.io/controller-runtime/pkg/client"
29
34
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
30
35
36
+ "github.com/fluxcd/cli-utils/pkg/object"
31
37
"github.com/fluxcd/pkg/ssa"
32
38
"github.com/fluxcd/pkg/ssa/jsondiff"
33
39
34
40
v2 "github.com/fluxcd/helm-controller/api/v2beta2"
41
+ "github.com/fluxcd/helm-controller/internal/diff"
35
42
)
36
43
37
44
// 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
61
68
errs []error
62
69
)
63
70
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.
64
76
if obj .GetNamespace () == "" {
65
77
// Manifest does not contain the namespace of the release.
66
78
// 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
86
98
diffOpts := []jsondiff.ListOption {
87
99
jsondiff .FieldOwner (fieldOwner ),
88
100
jsondiff.ExclusionSelector {v2 .DriftDetectionMetadataKey : v2 .DriftDetectionDisabledValue },
89
- jsondiff .MaskSecrets (true ),
90
101
jsondiff .Rationalize (true ),
91
102
jsondiff .Graceful (true ),
92
103
}
@@ -119,5 +130,149 @@ func Diff(ctx context.Context, config *helmaction.Configuration, rls *helmreleas
119
130
if err != nil {
120
131
errs = append (errs , err )
121
132
}
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 ()
123
278
}
0 commit comments