Skip to content

Commit c32b334

Browse files
Merge pull request #1823 from rexagod/regression-1575
fix: Set resource version for CRs
2 parents b3a7a46 + fc123b2 commit c32b334

File tree

6 files changed

+292
-84
lines changed

6 files changed

+292
-84
lines changed

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,9 @@ verify-podnetworkconnectivitychecks:
2626
test-e2e-encryption: GO_TEST_PACKAGES :=./test/e2e-encryption/...
2727
.PHONY: test-e2e-encryption
2828

29-
test-e2e-monitoring:
29+
test-e2e-monitoring: GO_TEST_PACKAGES :=./test/e2e-monitoring/...
30+
test-e2e-monitoring: GO_TEST_FLAGS += -v
31+
test-e2e-monitoring: GO_TEST_FLAGS += -timeout 5m
32+
test-e2e-monitoring: GO_TEST_FLAGS += -p 1
33+
test-e2e-monitoring: test-unit
3034
.PHONY: test-e2e-monitoring

pkg/operator/resource/resourceapply/monitoring.go

Lines changed: 25 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,17 @@ package resourceapply
22

33
import (
44
"context"
5-
errorsstdlib "errors"
6-
"fmt"
75

6+
"github.com/openshift/library-go/pkg/operator/events"
7+
"github.com/openshift/library-go/pkg/operator/resource/resourcehelper"
8+
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
89
"k8s.io/apimachinery/pkg/api/equality"
910
"k8s.io/apimachinery/pkg/api/errors"
1011
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1112
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12-
"k8s.io/apimachinery/pkg/runtime"
1313
"k8s.io/apimachinery/pkg/runtime/schema"
1414
"k8s.io/client-go/dynamic"
1515
"k8s.io/klog/v2"
16-
"k8s.io/utils/ptr"
17-
18-
"github.com/openshift/library-go/pkg/operator/events"
19-
"github.com/openshift/library-go/pkg/operator/resource/resourcehelper"
20-
21-
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
2216
)
2317

2418
var alertmanagerGVR = schema.GroupVersionResource{Group: "monitoring.coreos.com", Version: "v1", Resource: "alertmanagers"}
@@ -88,10 +82,10 @@ func ApplyUnstructuredResourceImproved(
8882
}
8983
existing, err := client.Resource(resourceGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
9084
if errors.IsNotFound(err) {
91-
want, err := client.Resource(resourceGVR).Namespace(namespace).Create(ctx, required, metav1.CreateOptions{})
92-
resourcehelper.ReportCreateEvent(recorder, required, err)
85+
want, errCreate := client.Resource(resourceGVR).Namespace(namespace).Create(ctx, required, metav1.CreateOptions{})
86+
resourcehelper.ReportCreateEvent(recorder, required, errCreate)
9387
cache.UpdateCachedResourceMetadata(required, want)
94-
return want, true, err
88+
return want, true, errCreate
9589
}
9690
if err != nil {
9791
return nil, false, err
@@ -102,71 +96,42 @@ func ApplyUnstructuredResourceImproved(
10296
return existing, false, nil
10397
}
10498

105-
// Ensure metadata field is present on the object.
10699
existingCopy := existing.DeepCopy()
107-
existingObjectMeta, found, err := unstructured.NestedMap(existingCopy.Object, "metadata")
108-
if err != nil {
109-
return nil, false, err
110-
}
111-
if !found {
112-
return nil, false, errorsstdlib.New(fmt.Sprintf("metadata not found in the existing object: %s/%s", existing.GetNamespace(), existingCopy.GetName()))
113-
}
114-
requiredObjectMeta, found, err := unstructured.NestedMap(required.Object, "metadata")
115-
if err != nil {
116-
return nil, false, err
117-
}
118-
if !found {
119-
return nil, false, errorsstdlib.New(fmt.Sprintf("metadata not found in the required object: %s/%s", required.GetNamespace(), required.GetName()))
120-
}
121100

122-
// Cast the metadata to the correct type.
123-
var existingObjectMetaTyped, requiredObjectMetaTyped metav1.ObjectMeta
124-
err = runtime.DefaultUnstructuredConverter.FromUnstructured(existingObjectMeta, &existingObjectMetaTyped)
125-
if err != nil {
126-
return nil, false, err
127-
}
128-
err = runtime.DefaultUnstructuredConverter.FromUnstructured(requiredObjectMeta, &requiredObjectMetaTyped)
101+
// Replace and/or merge certain metadata fields.
102+
didMetadataModify := false
103+
err = resourcemerge.EnsureObjectMetaForUnstructured(&didMetadataModify, existingCopy, required)
129104
if err != nil {
130105
return nil, false, err
131106
}
132107

133-
// Fail-fast if the resource versions differ.
134-
if requiredObjectMetaTyped.ResourceVersion != "" && existingObjectMetaTyped.ResourceVersion != requiredObjectMetaTyped.ResourceVersion {
135-
err = errors.NewConflict(resourceGVR.GroupResource(), name, fmt.Errorf("rejected to update %s %s because the object has been modified: desired/actual ResourceVersion: %v/%v", existing.GetKind(), existing.GetName(), requiredObjectMetaTyped.ResourceVersion, existingObjectMetaTyped.ResourceVersion))
136-
return nil, false, err
137-
}
138-
139-
// Check if the metadata objects differ.
140-
didMetadataModify := ptr.To(false)
141-
resourcemerge.EnsureObjectMeta(didMetadataModify, &existingObjectMetaTyped, requiredObjectMetaTyped)
142-
143108
// Deep-check the spec objects for equality, and update the cache in either case.
144109
if defaultingFunc == nil {
145110
defaultingFunc = noDefaulting
146111
}
147112
if equalityChecker == nil {
148113
equalityChecker = equality.Semantic
149114
}
150-
existingCopy, didSpecModify, err := ensureGenericSpec(required, existingCopy, defaultingFunc, equalityChecker)
115+
didSpecModify := false
116+
err = ensureGenericSpec(&didSpecModify, required, existingCopy, defaultingFunc, equalityChecker)
151117
if err != nil {
152118
return nil, false, err
153119
}
154-
if !didSpecModify && !*didMetadataModify {
120+
if !didSpecModify && !didMetadataModify {
155121
// Update cache even if certain fields are not modified, in order to maintain a consistent cache based on the
156122
// resource hash. The resource hash depends on the entire metadata, not just the fields that were checked above,
157123
cache.UpdateCachedResourceMetadata(required, existingCopy)
158124
return existingCopy, false, nil
159125
}
160126

127+
// Perform update if resource exists but different from the required (desired) one.
161128
if klog.V(4).Enabled() {
162129
klog.Infof("%s %q changes: %v", resourceGVR.String(), namespace+"/"+name, JSONPatchNoError(existing, existingCopy))
163130
}
164-
165-
// Perform update if resource exists but different from the required (desired) one.
166-
actual, err := client.Resource(resourceGVR).Namespace(namespace).Update(ctx, required, metav1.UpdateOptions{})
167-
resourcehelper.ReportUpdateEvent(recorder, required, err)
168-
cache.UpdateCachedResourceMetadata(required, actual)
169-
return actual, true, err
131+
actual, errUpdate := client.Resource(resourceGVR).Namespace(namespace).Update(ctx, existingCopy, metav1.UpdateOptions{})
132+
resourcehelper.ReportUpdateEvent(recorder, existingCopy, errUpdate)
133+
cache.UpdateCachedResourceMetadata(existingCopy, actual)
134+
return actual, true, errUpdate
170135
}
171136

172137
// DeleteUnstructuredResource deletes the unstructured resource.
@@ -182,27 +147,27 @@ func DeleteUnstructuredResource(ctx context.Context, client dynamic.Interface, r
182147
return nil, true, nil
183148
}
184149

185-
func ensureGenericSpec(required, existing *unstructured.Unstructured, mimicDefaultingFn mimicDefaultingFunc, equalityChecker equalityChecker) (*unstructured.Unstructured, bool, error) {
150+
func ensureGenericSpec(didSpecModify *bool, required, existing *unstructured.Unstructured, mimicDefaultingFn mimicDefaultingFunc, equalityChecker equalityChecker) error {
186151
mimicDefaultingFn(required)
187152
requiredSpec, _, err := unstructured.NestedMap(required.UnstructuredContent(), "spec")
188153
if err != nil {
189-
return nil, false, err
154+
return err
190155
}
191156
existingSpec, _, err := unstructured.NestedMap(existing.UnstructuredContent(), "spec")
192157
if err != nil {
193-
return nil, false, err
158+
return err
194159
}
195160

196161
if equalityChecker.DeepEqual(existingSpec, requiredSpec) {
197-
return existing, false, nil
162+
return nil
198163
}
199164

200-
existingCopy := existing.DeepCopy()
201-
if err := unstructured.SetNestedMap(existingCopy.UnstructuredContent(), requiredSpec, "spec"); err != nil {
202-
return nil, true, err
165+
if err = unstructured.SetNestedMap(existing.UnstructuredContent(), requiredSpec, "spec"); err != nil {
166+
return err
203167
}
168+
*didSpecModify = true
204169

205-
return existingCopy, true, nil
170+
return nil
206171
}
207172

208173
// mimicDefaultingFunc is used to set fields that are defaulted. This allows for sparse manifests to apply correctly.

pkg/operator/resource/resourceapply/monitoring_test.go

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ package resourceapply
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57
"testing"
68

9+
"github.com/google/go-cmp/cmp"
710
"k8s.io/apimachinery/pkg/api/equality"
811
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
912
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1013
"k8s.io/apimachinery/pkg/runtime"
1114
"k8s.io/apimachinery/pkg/runtime/schema"
12-
"k8s.io/apimachinery/pkg/util/diff"
1315
"k8s.io/apimachinery/pkg/util/json"
1416
dynamicfake "k8s.io/client-go/dynamic/fake"
1517
clienttesting "k8s.io/client-go/testing"
@@ -19,31 +21,48 @@ import (
1921
"github.com/openshift/library-go/pkg/operator/events"
2022
)
2123

24+
var structuredServiceMonitor = pov1api.ServiceMonitor{
25+
TypeMeta: metav1.TypeMeta{
26+
APIVersion: "monitoring.coreos.com/v1",
27+
Kind: "ServiceMonitor",
28+
},
29+
ObjectMeta: metav1.ObjectMeta{
30+
Name: "test-sm",
31+
Namespace: "test-ns",
32+
Labels: map[string]string{"app": "test-app"},
33+
},
34+
Spec: pov1api.ServiceMonitorSpec{
35+
NamespaceSelector: pov1api.NamespaceSelector{
36+
MatchNames: []string{"test-ns"},
37+
},
38+
},
39+
}
40+
2241
func TestApplyServiceMonitor(t *testing.T) {
2342
dynamicScheme := runtime.NewScheme()
2443
dynamicScheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "monitoring.coreos.com", Version: "v1", Kind: "ServiceMonitor"}, &unstructured.Unstructured{})
2544

26-
structuredServiceMonitor := pov1api.ServiceMonitor{
27-
TypeMeta: metav1.TypeMeta{
28-
APIVersion: "monitoring.coreos.com/v1",
29-
Kind: "ServiceMonitor",
30-
},
31-
ObjectMeta: metav1.ObjectMeta{
32-
Name: "test-sm",
33-
Namespace: "test-ns",
34-
Labels: map[string]string{"app": "test-app"},
35-
},
36-
Spec: pov1api.ServiceMonitorSpec{
37-
NamespaceSelector: pov1api.NamespaceSelector{
38-
MatchNames: []string{"test-ns"},
39-
},
40-
},
41-
}
4245
unstructuredServiceMonitor := structuredToUnstructuredServiceMonitor(&structuredServiceMonitor)
4346

4447
structuredServiceMonitorDifferentLabels := structuredServiceMonitor.DeepCopy()
45-
structuredServiceMonitorDifferentLabels.Labels = map[string]string{"app": "different-test-app"}
48+
structuredServiceMonitorDifferentLabels.Labels = map[string]string{"fake-app": "fake-test-app"}
4649
unstructuredServiceMonitorDifferentLabels := structuredToUnstructuredServiceMonitor(structuredServiceMonitorDifferentLabels)
50+
serviceMonitorDifferentLabelsValidationFunc := func(oI interface{}) error {
51+
o, ok := oI.(*unstructured.Unstructured)
52+
if !ok {
53+
return errors.New("unexpected object type")
54+
}
55+
gotLabels := o.GetLabels()
56+
wantLabels := structuredServiceMonitor.Labels
57+
for k, v := range structuredServiceMonitorDifferentLabels.Labels {
58+
wantLabels[k] = v
59+
}
60+
if !equality.Semantic.DeepEqual(gotLabels, wantLabels) {
61+
return errors.New(fmt.Sprintf("service monitor labels were not merged correctly (+got, -want): %s", cmp.Diff(gotLabels, wantLabels)))
62+
}
63+
64+
return nil
65+
}
4766

4867
structuredServiceMonitorDifferentSpec := structuredServiceMonitor.DeepCopy()
4968
structuredServiceMonitorDifferentSpec.Spec.NamespaceSelector.MatchNames = []string{"different-test-ns"}
@@ -58,6 +77,7 @@ func TestApplyServiceMonitor(t *testing.T) {
5877
existing *unstructured.Unstructured
5978
expectExistingResourceToBeModified bool
6079
expectActionsDuringModification []string
80+
delegateValidation func(interface{}) error
6181
}{
6282
{
6383
name: "same label, same spec",
@@ -69,6 +89,7 @@ func TestApplyServiceMonitor(t *testing.T) {
6989
existing: unstructuredServiceMonitorDifferentLabels,
7090
expectExistingResourceToBeModified: true,
7191
expectActionsDuringModification: []string{"get", "update"},
92+
delegateValidation: serviceMonitorDifferentLabelsValidationFunc,
7293
},
7394
{
7495
name: "same label, different spec",
@@ -85,7 +106,7 @@ func TestApplyServiceMonitor(t *testing.T) {
85106
} {
86107
t.Run(tc.name, func(t *testing.T) {
87108
dynamicClient := dynamicfake.NewSimpleDynamicClient(dynamicScheme, tc.existing)
88-
_, modified, err := ApplyServiceMonitor(context.TODO(), dynamicClient, events.NewInMemoryRecorder("monitor-test"), unstructuredServiceMonitor)
109+
got, modified, err := ApplyServiceMonitor(context.TODO(), dynamicClient, events.NewInMemoryRecorder("monitor-test"), unstructuredServiceMonitor)
89110
if err != nil {
90111
t.Fatalf("unexpected error: %v", err)
91112
}
@@ -103,26 +124,30 @@ func TestApplyServiceMonitor(t *testing.T) {
103124
}
104125
}
105126

106-
if len(tc.expectActionsDuringModification) > 1 &&
127+
if tc.delegateValidation != nil {
128+
if err = tc.delegateValidation(got); err != nil {
129+
t.Fatalf("delegated validation failed: %v", err)
130+
}
131+
} else if len(tc.expectActionsDuringModification) > 1 &&
107132
tc.expectActionsDuringModification[1] == "update" {
108133
updateAction, isUpdate := actions[1].(clienttesting.UpdateAction)
109134
if !isUpdate {
110135
t.Fatalf("expected second action to be update, got %+v", actions[1])
111136
}
112137
updatedMonitorObj := updateAction.GetObject().(*unstructured.Unstructured)
113138

114-
// Verify `metadata`.
139+
// Verify `metadata`. Note that the `metadata` is merged in some cases.
115140
requiredMonitorMetadata, _, _ := unstructured.NestedMap(unstructuredServiceMonitor.UnstructuredContent(), "metadata")
116141
existingMonitorMetadata, _, _ := unstructured.NestedMap(updatedMonitorObj.UnstructuredContent(), "metadata")
117142
if !equality.Semantic.DeepEqual(requiredMonitorMetadata, existingMonitorMetadata) {
118-
t.Fatalf("expected resulting service monitor metadata to match required metadata: %s", diff.ObjectDiff(requiredMonitorMetadata, existingMonitorMetadata))
143+
t.Fatalf("expected resulting service monitor metadata to match required metadata: %s", cmp.Diff(requiredMonitorMetadata, existingMonitorMetadata))
119144
}
120145

121146
// Verify `spec`.
122147
requiredMonitorSpec, _, _ := unstructured.NestedMap(unstructuredServiceMonitor.UnstructuredContent(), "spec")
123148
existingMonitorSpec, _, _ := unstructured.NestedMap(updatedMonitorObj.UnstructuredContent(), "spec")
124149
if !equality.Semantic.DeepEqual(requiredMonitorSpec, existingMonitorSpec) {
125-
t.Fatalf("expected resulting service monitor spec to match required spec: %s", diff.ObjectDiff(requiredMonitorMetadata, existingMonitorMetadata))
150+
t.Fatalf("expected resulting service monitor spec to match required spec: %s", cmp.Diff(requiredMonitorSpec, existingMonitorSpec))
126151
}
127152
}
128153
})

pkg/operator/resource/resourcemerge/object_merger.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package resourcemerge
22

33
import (
4+
errorsstdlib "errors"
5+
"fmt"
46
"reflect"
57
"strings"
68

79
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
"k8s.io/apimachinery/pkg/runtime"
812
"k8s.io/apimachinery/pkg/runtime/schema"
913
)
1014

@@ -18,6 +22,48 @@ func EnsureObjectMeta(modified *bool, existing *metav1.ObjectMeta, required meta
1822
MergeOwnerRefs(modified, &existing.OwnerReferences, required.OwnerReferences)
1923
}
2024

25+
func EnsureObjectMetaForUnstructured(modified *bool, existing *unstructured.Unstructured, required *unstructured.Unstructured) error {
26+
27+
// Ensure metadata field is present on the object.
28+
existingObjectMeta, found, err := unstructured.NestedMap(existing.Object, "metadata")
29+
if err != nil {
30+
return err
31+
}
32+
if !found {
33+
return errorsstdlib.New(fmt.Sprintf("metadata not found in the existing object: %s/%s", existing.GetNamespace(), existing.GetName()))
34+
}
35+
var requiredObjectMeta map[string]interface{}
36+
requiredObjectMeta, found, err = unstructured.NestedMap(required.Object, "metadata")
37+
if err != nil {
38+
return err
39+
}
40+
if !found {
41+
return errorsstdlib.New(fmt.Sprintf("metadata not found in the required object: %s/%s", required.GetNamespace(), required.GetName()))
42+
}
43+
44+
// Cast the metadata to the correct type.
45+
var existingObjectMetaTyped, requiredObjectMetaTyped metav1.ObjectMeta
46+
err = runtime.DefaultUnstructuredConverter.FromUnstructured(existingObjectMeta, &existingObjectMetaTyped)
47+
if err != nil {
48+
return err
49+
}
50+
err = runtime.DefaultUnstructuredConverter.FromUnstructured(requiredObjectMeta, &requiredObjectMetaTyped)
51+
if err != nil {
52+
return err
53+
}
54+
55+
// Check if the metadata objects differ. This only checks for selective fields (excluding the resource version, among others).
56+
EnsureObjectMeta(modified, &existingObjectMetaTyped, requiredObjectMetaTyped)
57+
if *modified {
58+
existing.Object["metadata"], err = runtime.DefaultUnstructuredConverter.ToUnstructured(&existingObjectMetaTyped)
59+
if err != nil {
60+
return err
61+
}
62+
}
63+
64+
return nil
65+
}
66+
2167
// WithCleanLabelsAndAnnotations cleans the metadata off the removal annotations/labels/ownerrefs
2268
// (those that end with trailing "-")
2369
func WithCleanLabelsAndAnnotations(obj metav1.Object) metav1.Object {

test/e2e-monitoring/OWNERS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
reviewers:
2+
- dgrisonnet
3+
- p0lyn0mial
4+
- tkashem
5+
- rexagod
6+
approvers:
7+
- dgrisonnet
8+
- p0lyn0mial
9+
- tkashem

0 commit comments

Comments
 (0)