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  }