Skip to content

Commit 8d9886f

Browse files
committed
ssa: avoid potential cyclic import
This moves things into separate packages to avoid a potential cyclic import as soon as we would like to utilize `jsondiff` in `ssa` itself. Signed-off-by: Hidde Beydals <[email protected]>
1 parent 1ad0559 commit 8d9886f

26 files changed

+742
-553
lines changed

ssa/errors.go ssa/errors/errors.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
package ssa
17+
package errors
1818

1919
import (
2020
"fmt"
2121

2222
apierrors "k8s.io/apimachinery/pkg/api/errors"
2323
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2424
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25+
26+
"github.com/fluxcd/pkg/ssa/utils"
2527
)
2628

2729
// DryRunErr is an error that occurs during a server-side dry-run apply.
@@ -51,9 +53,9 @@ func (e *DryRunErr) Error() string {
5153

5254
if apierrors.IsNotFound(e.Unwrap()) {
5355
if e.involvedObject.GetNamespace() != "" {
54-
return fmt.Sprintf("%s namespace not specified: %s", FmtUnstructured(e.involvedObject), e.Unwrap().Error())
56+
return fmt.Sprintf("%s namespace not specified: %s", utils.Unstructured(e.involvedObject), e.Unwrap().Error())
5557
}
56-
return fmt.Sprintf("%s not found: %s", FmtUnstructured(e.involvedObject), e.Unwrap().Error())
58+
return fmt.Sprintf("%s not found: %s", utils.Unstructured(e.involvedObject), e.Unwrap().Error())
5759
}
5860

5961
reason := string(apierrors.ReasonForError(e.Unwrap()))
@@ -67,7 +69,7 @@ func (e *DryRunErr) Error() string {
6769
reason = fmt.Sprintf(" (%s)", reason)
6870
}
6971

70-
return fmt.Sprintf("%s dry-run failed%s: %s", FmtUnstructured(e.involvedObject), reason, e.underlyingErr.Error())
72+
return fmt.Sprintf("%s dry-run failed%s: %s", utils.Unstructured(e.involvedObject), reason, e.underlyingErr.Error())
7173
}
7274

7375
// Unwrap returns the underlying error.

ssa/errors/immutable.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 errors
18+
19+
import (
20+
"regexp"
21+
22+
"k8s.io/apimachinery/pkg/api/errors"
23+
)
24+
25+
// Match CEL immutable error variants.
26+
var matchImmutableFieldErrors = []*regexp.Regexp{
27+
regexp.MustCompile(`.*is\simmutable.*`),
28+
regexp.MustCompile(`.*immutable\sfield.*`),
29+
}
30+
31+
// IsImmutableError checks if the given error is an immutable error.
32+
func IsImmutableError(err error) bool {
33+
// Detect immutability like kubectl does
34+
// https://github.com/kubernetes/kubectl/blob/8165f83007/pkg/cmd/apply/patcher.go#L201
35+
if errors.IsConflict(err) || errors.IsInvalid(err) {
36+
return true
37+
}
38+
39+
// Detect immutable errors returned by custom admission webhooks and Kubernetes CEL
40+
// https://kubernetes.io/blog/2022/09/29/enforce-immutability-using-cel/#immutablility-after-first-modification
41+
for _, fieldError := range matchImmutableFieldErrors {
42+
if fieldError.MatchString(err.Error()) {
43+
return true
44+
}
45+
}
46+
47+
return false
48+
}

ssa/errors/immutable_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 errors
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestIsImmutableError(t *testing.T) {
27+
testCases := []struct {
28+
name string
29+
err error
30+
match bool
31+
}{
32+
{
33+
name: "CEL immutable error",
34+
err: fmt.Errorf(`the ImmutableSinceFirstWrite "test1" is invalid: value: Invalid value: "string": Value is immutable`),
35+
match: true,
36+
},
37+
{
38+
name: "Custom admission immutable error",
39+
err: fmt.Errorf(`the IAMPolicyMember's spec is immutable: admission webhook "deny-immutable-field-updates.cnrm.cloud.google.com" denied the request: the IAMPolicyMember's spec is immutable`),
40+
match: true,
41+
},
42+
{
43+
name: "Not immutable error",
44+
err: fmt.Errorf(`is not immutable`),
45+
match: false,
46+
},
47+
}
48+
49+
for _, tc := range testCases {
50+
t.Run(tc.name, func(t *testing.T) {
51+
g := NewWithT(t)
52+
53+
g.Expect(IsImmutableError(tc.err)).To(BeIdenticalTo(tc.match))
54+
})
55+
}
56+
}

ssa/jsondiff/suite_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828
"sigs.k8s.io/controller-runtime/pkg/client"
2929
"sigs.k8s.io/controller-runtime/pkg/envtest"
3030

31-
"github.com/fluxcd/pkg/ssa"
31+
"github.com/fluxcd/pkg/ssa/utils"
3232
)
3333

3434
var (
@@ -80,5 +80,5 @@ func LoadResource(p string) (*unstructured.Unstructured, error) {
8080
return nil, err
8181
}
8282
defer f.Close()
83-
return ssa.ReadObject(f)
83+
return utils.ReadObject(f)
8484
}

ssa/jsondiff/unstructured.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import (
2525
"k8s.io/apimachinery/pkg/util/errors"
2626
"sigs.k8s.io/controller-runtime/pkg/client"
2727

28-
"github.com/fluxcd/pkg/ssa"
28+
ssaerrors "github.com/fluxcd/pkg/ssa/errors"
29+
"github.com/fluxcd/pkg/ssa/normalize"
30+
"github.com/fluxcd/pkg/ssa/utils"
2931
)
3032

3133
// IgnorePathRoot ignores the root of a JSON document, i.e., the entire
@@ -118,7 +120,7 @@ func Unstructured(ctx context.Context, c client.Client, obj *unstructured.Unstru
118120
o.ApplyOptions(opts)
119121

120122
// Check if the object should be excluded based on metadata.
121-
if ssa.AnyInMetadata(obj, o.ExclusionSelector) {
123+
if utils.AnyInMetadata(obj, o.ExclusionSelector) {
122124
return NewDiffForUnstructured(obj, nil, DiffTypeExclude, nil), nil
123125
}
124126

@@ -142,14 +144,14 @@ func Unstructured(ctx context.Context, c client.Client, obj *unstructured.Unstru
142144
client.FieldOwner(o.FieldManager),
143145
}
144146
if err := c.Patch(ctx, dryRunObj, client.Apply, patchOpts...); err != nil {
145-
return nil, ssa.NewDryRunErr(err, obj)
147+
return nil, ssaerrors.NewDryRunErr(err, obj)
146148
}
147149

148150
if dryRunObj.GetResourceVersion() == "" {
149151
return NewDiffForUnstructured(obj, nil, DiffTypeCreate, nil), nil
150152
}
151153

152-
if err := ssa.NormalizeDryRunUnstructured(dryRunObj); err != nil {
154+
if err := normalize.DryRunUnstructured(dryRunObj); err != nil {
153155
return nil, err
154156
}
155157

ssa/jsondiff/unstructured_test.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2929
"sigs.k8s.io/controller-runtime/pkg/client"
3030

31-
"github.com/fluxcd/pkg/ssa"
31+
"github.com/fluxcd/pkg/ssa/normalize"
3232
)
3333

3434
const dummyFieldOwner = "dummy"
@@ -274,7 +274,7 @@ func TestUnstructuredList(t *testing.T) {
274274
},
275275
mutateDesired: func(obj *unstructured.Unstructured) {
276276
_ = unstructured.SetNestedField(obj.Object, "bar", "stringData", "foo")
277-
_ = ssa.NormalizeUnstructured(obj)
277+
_ = normalize.Unstructured(obj)
278278
},
279279
opts: []ListOption{
280280
MaskSecrets(true),
@@ -304,14 +304,14 @@ func TestUnstructuredList(t *testing.T) {
304304
"a": "2",
305305
"b": "1",
306306
}, "data")
307-
_ = ssa.NormalizeUnstructured(obj)
307+
_ = normalize.Unstructured(obj)
308308
},
309309
mutateDesired: func(obj *unstructured.Unstructured) {
310310
_ = unstructured.SetNestedMap(obj.Object, map[string]interface{}{
311311
"a": "1",
312312
"b": "2",
313313
}, "data")
314-
_ = ssa.NormalizeUnstructured(obj)
314+
_ = normalize.Unstructured(obj)
315315
},
316316
opts: []ListOption{
317317
Rationalize(true),
@@ -718,7 +718,7 @@ func TestUnstructured(t *testing.T) {
718718
path: "testdata/empty-secret.yaml",
719719
mutateDesired: func(obj *unstructured.Unstructured) {
720720
_ = unstructured.SetNestedField(obj.Object, "bar", "stringData", "foo")
721-
_ = ssa.NormalizeUnstructured(obj)
721+
_ = normalize.Unstructured(obj)
722722
},
723723
opts: []ResourceOption{
724724
MaskSecrets(false),
@@ -742,11 +742,11 @@ func TestUnstructured(t *testing.T) {
742742
mutateCluster: func(obj *unstructured.Unstructured) {
743743
_ = unstructured.SetNestedField(obj.Object, "bar", "stringData", "foo")
744744
_ = unstructured.SetNestedField(obj.Object, "bar", "stringData", "bar")
745-
_ = ssa.NormalizeUnstructured(obj)
745+
_ = normalize.Unstructured(obj)
746746
},
747747
mutateDesired: func(obj *unstructured.Unstructured) {
748748
_ = unstructured.SetNestedField(obj.Object, "baz", "stringData", "foo")
749-
_ = ssa.NormalizeUnstructured(obj)
749+
_ = normalize.Unstructured(obj)
750750
},
751751
opts: []ResourceOption{
752752
MaskSecrets(true),
@@ -769,11 +769,11 @@ func TestUnstructured(t *testing.T) {
769769
mutateCluster: func(obj *unstructured.Unstructured) {
770770
_ = unstructured.SetNestedField(obj.Object, "bar", "stringData", "foo")
771771
_ = unstructured.SetNestedField(obj.Object, "bar", "stringData", "bar")
772-
_ = ssa.NormalizeUnstructured(obj)
772+
_ = normalize.Unstructured(obj)
773773
},
774774
mutateDesired: func(obj *unstructured.Unstructured) {
775775
_ = unstructured.SetNestedField(obj.Object, "baz", "stringData", "foo")
776-
_ = ssa.NormalizeUnstructured(obj)
776+
_ = normalize.Unstructured(obj)
777777
},
778778
opts: []ResourceOption{
779779
MaskSecrets(true),

ssa/main_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"sigs.k8s.io/controller-runtime/pkg/envtest"
3232

3333
"github.com/fluxcd/cli-utils/pkg/kstatus/polling"
34+
35+
"github.com/fluxcd/pkg/ssa/utils"
3436
)
3537

3638
var manager *ResourceManager
@@ -85,7 +87,7 @@ func readManifest(manifest, namespace string) ([]*unstructured.Unstructured, err
8587
}
8688
yml := fmt.Sprintf(string(data), namespace)
8789

88-
objects, err := ReadObjects(strings.NewReader(yml))
90+
objects, err := utils.ReadObjects(strings.NewReader(yml))
8991
if err != nil {
9092
return nil, err
9193
}
@@ -103,7 +105,7 @@ func generateName(prefix string) string {
103105
func getFirstObject(objects []*unstructured.Unstructured, kind, name string) (string, *unstructured.Unstructured) {
104106
for _, object := range objects {
105107
if object.GetKind() == kind && object.GetName() == name {
106-
return FmtUnstructured(object), object
108+
return utils.Unstructured(object), object
107109
}
108110
}
109111
return "", nil

ssa/manager.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323

2424
"github.com/fluxcd/cli-utils/pkg/kstatus/polling"
2525
"github.com/fluxcd/cli-utils/pkg/object"
26+
27+
"github.com/fluxcd/pkg/ssa/utils"
2628
)
2729

2830
// ResourceManager reconciles Kubernetes resources onto the target cluster using server-side apply.
@@ -87,7 +89,7 @@ func (m *ResourceManager) changeSetEntry(o *unstructured.Unstructured, action Ac
8789
return &ChangeSetEntry{
8890
ObjMetadata: object.UnstructuredToObjMetadata(o),
8991
GroupVersion: o.GroupVersionKind().Version,
90-
Subject: FmtUnstructured(o),
92+
Subject: utils.Unstructured(o),
9193
Action: action,
9294
}
9395
}

0 commit comments

Comments
 (0)