istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/helmreconciler/apply_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package helmreconciler 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "os" 22 "reflect" 23 "sync" 24 "testing" 25 26 kerrors "k8s.io/apimachinery/pkg/api/errors" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime/schema" 30 "k8s.io/apimachinery/pkg/types" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 "sigs.k8s.io/controller-runtime/pkg/client/fake" 33 "sigs.k8s.io/controller-runtime/pkg/client/interceptor" 34 35 v1alpha12 "istio.io/api/operator/v1alpha1" 36 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" 37 "istio.io/istio/operator/pkg/object" 38 ) 39 40 var interceptorFunc = interceptor.Funcs{Patch: func( 41 ctx context.Context, 42 clnt client.WithWatch, 43 obj client.Object, 44 patch client.Patch, 45 opts ...client.PatchOption, 46 ) error { 47 // Apply patches are supposed to upsert, but fake client fails if the object doesn't exist, 48 // if an apply patch occurs for an object that doesn't yet exist, create it. 49 if patch.Type() != types.ApplyPatchType { 50 return clnt.Patch(ctx, obj, patch, opts...) 51 } 52 check, ok := obj.DeepCopyObject().(client.Object) 53 if !ok { 54 return errors.New("could not check for object in fake client") 55 } 56 if err := clnt.Get(ctx, client.ObjectKeyFromObject(obj), check); kerrors.IsNotFound(err) { 57 if err := clnt.Create(ctx, check); err != nil { 58 return fmt.Errorf("could not inject object creation for fake: %w", err) 59 } 60 } else if err != nil { 61 return err 62 } 63 obj.SetResourceVersion(check.GetResourceVersion()) 64 return clnt.Update(ctx, obj) 65 }} 66 67 func TestHelmReconciler_ApplyObject(t *testing.T) { 68 tests := []struct { 69 name string 70 currentState string 71 input string 72 want string 73 wantErr bool 74 }{ 75 { 76 name: "creates if not present", 77 input: "testdata/configmap.yaml", 78 want: "testdata/configmap.yaml", 79 }, 80 { 81 name: "updates if present", 82 currentState: "testdata/configmap.yaml", 83 input: "testdata/configmap-changed.yaml", 84 want: "testdata/configmap-changed.yaml", 85 }, 86 // Test IstioOperator field removals 87 { 88 name: "creates if not present", 89 input: "testdata/iop-test-gw-1.yaml", 90 want: "testdata/iop-test-gw-1.yaml", 91 }, 92 { 93 name: "updates if present", 94 currentState: "testdata/iop-test-gw-1.yaml", 95 input: "testdata/iop-changed.yaml", 96 want: "testdata/iop-changed.yaml", 97 }, 98 } 99 for _, tt := range tests { 100 t.Run(tt.name, func(t *testing.T) { 101 obj := loadData(t, tt.input) 102 var k8sClient client.Client 103 if tt.currentState != "" { 104 k8sClient = fake.NewClientBuilder(). 105 WithRuntimeObjects(loadData(t, tt.currentState). 106 UnstructuredObject()).WithInterceptorFuncs(interceptorFunc).Build() 107 } else { 108 // no current state provided, initialize fake client without runtime object 109 k8sClient = fake.NewClientBuilder().WithInterceptorFuncs(interceptorFunc).Build() 110 } 111 112 cl := k8sClient 113 h := &HelmReconciler{ 114 client: cl, 115 opts: &Options{}, 116 iop: &v1alpha1.IstioOperator{ 117 ObjectMeta: metav1.ObjectMeta{ 118 Name: "test-operator", 119 Namespace: "istio-operator-test", 120 }, 121 Spec: &v1alpha12.IstioOperatorSpec{}, 122 }, 123 countLock: &sync.Mutex{}, 124 prunedKindSet: map[schema.GroupKind]struct{}{}, 125 } 126 if err := h.ApplyObject(obj.UnstructuredObject()); (err != nil) != tt.wantErr { 127 t.Errorf("HelmReconciler.ApplyObject() error = %v, wantErr %v", err, tt.wantErr) 128 } 129 130 manifest := loadData(t, tt.want) 131 key := client.ObjectKeyFromObject(manifest.UnstructuredObject()) 132 got, want := obj.UnstructuredObject(), manifest.UnstructuredObject() 133 134 if err := cl.Get(context.Background(), key, got); err != nil { 135 t.Errorf("error validating manifest %v: %v", manifest.Hash(), err) 136 } 137 // remove resource version and annotations (last applied config) when we compare as we don't care 138 unstructured.RemoveNestedField(got.Object, "metadata", "resourceVersion") 139 unstructured.RemoveNestedField(got.Object, "metadata", "annotations") 140 141 if !reflect.DeepEqual(want, got) { 142 t.Errorf("wanted:\n%v\ngot:\n%v", 143 object.NewK8sObject(want, nil, nil).YAMLDebugString(), 144 object.NewK8sObject(got, nil, nil).YAMLDebugString(), 145 ) 146 } 147 }) 148 } 149 } 150 151 func loadData(t *testing.T, file string) *object.K8sObject { 152 contents, err := os.ReadFile(file) 153 if err != nil { 154 t.Fatal(err) 155 } 156 obj, err := object.ParseYAMLToK8sObject(contents) 157 if err != nil { 158 t.Fatal(err) 159 } 160 return obj 161 }