Skip to content

Commit b79df2f

Browse files
committed
reconcile: determine drift in cluster
This allows `DetermineReleaseState` to determine if the cluster state has drifted from the manifest defined in the Helm storage. This allows the atomic reconciler to determine if an upgrade should happen based on the configuration of the `HelmRelease`. If drift detection is `enabled` (or set to `warn`), it will report drift via the controller logs and a Kubernetes Event. In addition, when correction is enabled, it will instruct to perform a Helm upgrade to correct the drift. To summarize the detected drift in a compact message, summarize utilities have been introduced to the `diff` package. Signed-off-by: Hidde Beydals <hidde@hhh.computer>
1 parent 98c4118 commit b79df2f

File tree

6 files changed

+670
-14
lines changed

6 files changed

+670
-14
lines changed

internal/diff/summarize.go

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
Copyright 2023 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package diff
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
extjsondiff "github.com/wI2L/jsondiff"
24+
25+
"github.com/fluxcd/pkg/ssa/jsondiff"
26+
)
27+
28+
// DefaultDiffTypes is the default set of jsondiff.DiffType types to include in
29+
// summaries.
30+
var DefaultDiffTypes = []jsondiff.DiffType{
31+
jsondiff.DiffTypeCreate,
32+
jsondiff.DiffTypeUpdate,
33+
jsondiff.DiffTypeExclude,
34+
}
35+
36+
// SummarizeDiffSet returns a summary of the given DiffSet, including only
37+
// the given jsondiff.DiffType types. If no types are given, the
38+
// DefaultDiffTypes set is used.
39+
//
40+
// The summary is a string with one line per Diff, in the format:
41+
// `Kind/namespace/name: <summary>`
42+
//
43+
// Where summary is one of:
44+
//
45+
// - unchanged
46+
// - removed
47+
// - excluded
48+
// - changed (x added, y changed, z removed)
49+
//
50+
// For example:
51+
//
52+
// Deployment/default/hello-world: changed (1 added, 1 changed, 1 removed)
53+
// Deployment/default/hello-world2: removed
54+
// Deployment/default/hello-world3: excluded
55+
// Deployment/default/hello-world4: unchanged
56+
func SummarizeDiffSet(set jsondiff.DiffSet, include ...jsondiff.DiffType) string {
57+
if include == nil {
58+
include = DefaultDiffTypes
59+
}
60+
61+
var summary strings.Builder
62+
for _, diff := range set {
63+
if diff == nil || !typeInSlice(diff.Type, include) {
64+
continue
65+
}
66+
67+
switch diff.Type {
68+
case jsondiff.DiffTypeNone:
69+
writeResourceName(diff, &summary)
70+
summary.WriteString(" unchanged\n")
71+
case jsondiff.DiffTypeCreate:
72+
writeResourceName(diff, &summary)
73+
summary.WriteString(" removed\n")
74+
case jsondiff.DiffTypeExclude:
75+
writeResourceName(diff, &summary)
76+
summary.WriteString(" excluded\n")
77+
case jsondiff.DiffTypeUpdate:
78+
writeResourceName(diff, &summary)
79+
added, changed, removed := summarizeUpdate(diff)
80+
summary.WriteString(fmt.Sprintf(" changed (%d additions, %d changes, %d removals)\n", added, changed, removed))
81+
}
82+
}
83+
return strings.TrimSpace(summary.String())
84+
}
85+
86+
// SummarizeDiffSetBrief returns a brief summary of the given DiffSet.
87+
//
88+
// The summary is a string in the format:
89+
//
90+
// removed: x, changed: y, excluded: z, unchanged: w
91+
//
92+
// For example:
93+
//
94+
// removed: 1, changed: 3, excluded: 1, unchanged: 2
95+
func SummarizeDiffSetBrief(set jsondiff.DiffSet, include ...jsondiff.DiffType) string {
96+
var removed, changed, excluded, unchanged int
97+
for _, diff := range set {
98+
switch diff.Type {
99+
case jsondiff.DiffTypeCreate:
100+
removed++
101+
case jsondiff.DiffTypeUpdate:
102+
changed++
103+
case jsondiff.DiffTypeExclude:
104+
excluded++
105+
case jsondiff.DiffTypeNone:
106+
unchanged++
107+
}
108+
}
109+
110+
if include == nil {
111+
include = DefaultDiffTypes
112+
}
113+
114+
var summary strings.Builder
115+
for _, t := range include {
116+
switch t {
117+
case jsondiff.DiffTypeCreate:
118+
summary.WriteString(fmt.Sprintf("removed: %d, ", removed))
119+
case jsondiff.DiffTypeUpdate:
120+
summary.WriteString(fmt.Sprintf("changed: %d, ", changed))
121+
case jsondiff.DiffTypeExclude:
122+
summary.WriteString(fmt.Sprintf("excluded: %d, ", excluded))
123+
case jsondiff.DiffTypeNone:
124+
summary.WriteString(fmt.Sprintf("unchanged: %d, ", unchanged))
125+
}
126+
}
127+
return strings.TrimSuffix(summary.String(), ", ")
128+
}
129+
130+
// writeResourceName writes the resource name in the format
131+
// `kind/namespace/name` to the given strings.Builder.
132+
func writeResourceName(diff *jsondiff.Diff, summary *strings.Builder) {
133+
summary.WriteString(diff.GroupVersionKind.Kind)
134+
summary.WriteString("/")
135+
summary.WriteString(diff.Namespace)
136+
summary.WriteString("/")
137+
summary.WriteString(diff.Name)
138+
}
139+
140+
// SummarizeUpdate returns the number of added, changed and removed fields
141+
// in the given update patch.
142+
func summarizeUpdate(diff *jsondiff.Diff) (added, changed, removed int) {
143+
for _, p := range diff.Patch {
144+
switch p.Type {
145+
case extjsondiff.OperationAdd:
146+
added++
147+
case extjsondiff.OperationReplace:
148+
changed++
149+
case extjsondiff.OperationRemove:
150+
removed++
151+
}
152+
}
153+
return
154+
}
155+
156+
// typeInSlice returns true if the given jsondiff.DiffType is in the slice.
157+
func typeInSlice(t jsondiff.DiffType, slice []jsondiff.DiffType) bool {
158+
for _, s := range slice {
159+
if t == s {
160+
return true
161+
}
162+
}
163+
return false
164+
}

internal/diff/summarize_test.go

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
Copyright 2023 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package diff
18+
19+
import (
20+
"testing"
21+
22+
extjsondiff "github.com/wI2L/jsondiff"
23+
"k8s.io/apimachinery/pkg/runtime/schema"
24+
25+
"github.com/fluxcd/pkg/ssa/jsondiff"
26+
)
27+
28+
func TestSummarizeDiffSet(t *testing.T) {
29+
diffSet := jsondiff.DiffSet{
30+
&jsondiff.Diff{
31+
GroupVersionKind: schema.GroupVersionKind{
32+
Kind: "ConfigMap",
33+
},
34+
Namespace: "namespace-1",
35+
Name: "config",
36+
Type: jsondiff.DiffTypeNone,
37+
},
38+
&jsondiff.Diff{
39+
GroupVersionKind: schema.GroupVersionKind{
40+
Kind: "Secret",
41+
},
42+
Namespace: "namespace-x",
43+
Name: "naughty",
44+
Type: jsondiff.DiffTypeCreate,
45+
},
46+
&jsondiff.Diff{
47+
GroupVersionKind: schema.GroupVersionKind{
48+
Kind: "StatefulSet",
49+
},
50+
Namespace: "default",
51+
Name: "hello-world",
52+
Type: jsondiff.DiffTypeExclude,
53+
},
54+
&jsondiff.Diff{
55+
GroupVersionKind: schema.GroupVersionKind{
56+
Kind: "Deployment",
57+
},
58+
Namespace: "tenant-y",
59+
Name: "touched-me",
60+
Type: jsondiff.DiffTypeUpdate,
61+
Patch: extjsondiff.Patch{
62+
{Type: extjsondiff.OperationAdd},
63+
{Type: extjsondiff.OperationReplace},
64+
{Type: extjsondiff.OperationReplace},
65+
{Type: extjsondiff.OperationReplace},
66+
{Type: extjsondiff.OperationRemove},
67+
{Type: extjsondiff.OperationRemove},
68+
},
69+
},
70+
}
71+
72+
tests := []struct {
73+
name string
74+
include []jsondiff.DiffType
75+
want string
76+
}{
77+
{
78+
name: "default",
79+
include: nil,
80+
want: `Secret/namespace-x/naughty removed
81+
StatefulSet/default/hello-world excluded
82+
Deployment/tenant-y/touched-me changed (1 additions, 3 changes, 2 removals)`,
83+
},
84+
{
85+
name: "include unchanged",
86+
include: []jsondiff.DiffType{
87+
jsondiff.DiffTypeNone,
88+
},
89+
want: "ConfigMap/namespace-1/config unchanged",
90+
},
91+
{
92+
name: "include removed",
93+
include: []jsondiff.DiffType{
94+
jsondiff.DiffTypeCreate,
95+
},
96+
want: "Secret/namespace-x/naughty removed",
97+
},
98+
{
99+
name: "include excluded",
100+
include: []jsondiff.DiffType{
101+
jsondiff.DiffTypeExclude,
102+
},
103+
want: "StatefulSet/default/hello-world excluded",
104+
},
105+
{
106+
name: "include changed",
107+
include: []jsondiff.DiffType{
108+
jsondiff.DiffTypeUpdate,
109+
},
110+
want: "Deployment/tenant-y/touched-me changed (1 additions, 3 changes, 2 removals)",
111+
},
112+
{
113+
name: "include multiple types",
114+
include: []jsondiff.DiffType{
115+
jsondiff.DiffTypeNone,
116+
jsondiff.DiffTypeUpdate,
117+
},
118+
want: `ConfigMap/namespace-1/config unchanged
119+
Deployment/tenant-y/touched-me changed (1 additions, 3 changes, 2 removals)`,
120+
},
121+
{
122+
name: "empty set",
123+
include: []jsondiff.DiffType{},
124+
want: "",
125+
},
126+
}
127+
128+
for _, tt := range tests {
129+
t.Run(tt.name, func(t *testing.T) {
130+
got := SummarizeDiffSet(diffSet, tt.include...)
131+
if got != tt.want {
132+
t.Errorf("SummarizeDiffSet() =\n\n%v\n\nwant\n\n%v", got, tt.want)
133+
}
134+
})
135+
}
136+
}
137+
138+
func TestSummarizeDiffSetBrief(t *testing.T) {
139+
diffSet := jsondiff.DiffSet{
140+
&jsondiff.Diff{Type: jsondiff.DiffTypeCreate},
141+
&jsondiff.Diff{Type: jsondiff.DiffTypeUpdate},
142+
&jsondiff.Diff{Type: jsondiff.DiffTypeExclude},
143+
&jsondiff.Diff{Type: jsondiff.DiffTypeNone},
144+
&jsondiff.Diff{Type: jsondiff.DiffTypeNone},
145+
}
146+
147+
tests := []struct {
148+
name string
149+
include []jsondiff.DiffType
150+
want string
151+
}{
152+
{
153+
name: "default include",
154+
include: nil,
155+
want: "removed: 1, changed: 1, excluded: 1",
156+
},
157+
{
158+
name: "include create and update",
159+
include: []jsondiff.DiffType{
160+
jsondiff.DiffTypeCreate,
161+
jsondiff.DiffTypeUpdate,
162+
},
163+
want: "removed: 1, changed: 1",
164+
},
165+
{
166+
name: "include all types",
167+
include: []jsondiff.DiffType{
168+
jsondiff.DiffTypeCreate,
169+
jsondiff.DiffTypeUpdate,
170+
jsondiff.DiffTypeExclude,
171+
jsondiff.DiffTypeNone,
172+
},
173+
want: "removed: 1, changed: 1, excluded: 1, unchanged: 2",
174+
},
175+
{
176+
name: "include none",
177+
include: []jsondiff.DiffType{},
178+
want: "",
179+
},
180+
}
181+
182+
for _, tt := range tests {
183+
t.Run(tt.name, func(t *testing.T) {
184+
got := SummarizeDiffSetBrief(diffSet, tt.include...)
185+
if got != tt.want {
186+
t.Errorf("SummarizeDiffSetBrief() = %v, want %v", got, tt.want)
187+
}
188+
})
189+
}
190+
}

0 commit comments

Comments
 (0)