sigs.k8s.io/cluster-api@v1.7.1/exp/runtime/topologymutation/walker_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes 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 topologymutation
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"testing"
    25  
    26  	. "github.com/onsi/gomega"
    27  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/runtime/serializer"
    31  	"k8s.io/apimachinery/pkg/types"
    32  
    33  	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
    34  	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
    35  	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
    36  )
    37  
    38  var (
    39  	testScheme = runtime.NewScheme()
    40  )
    41  
    42  func init() {
    43  	_ = controlplanev1.AddToScheme(testScheme)
    44  	_ = bootstrapv1.AddToScheme(testScheme)
    45  }
    46  
    47  func Test_WalkTemplates(t *testing.T) {
    48  	g := NewWithT(t)
    49  
    50  	decoder := serializer.NewCodecFactory(testScheme).UniversalDecoder(
    51  		controlplanev1.GroupVersion,
    52  		bootstrapv1.GroupVersion,
    53  	)
    54  	mutatingFunc := func(_ context.Context, obj runtime.Object, _ map[string]apiextensionsv1.JSON, _ runtimehooksv1.HolderReference) error {
    55  		switch obj := obj.(type) {
    56  		case *controlplanev1.KubeadmControlPlaneTemplate:
    57  			obj.Annotations = map[string]string{"a": "a"}
    58  		case *bootstrapv1.KubeadmConfigTemplate:
    59  			obj.Annotations = map[string]string{"b": "b"}
    60  		}
    61  		return nil
    62  	}
    63  	kubeadmControlPlaneTemplate := controlplanev1.KubeadmControlPlaneTemplate{
    64  		TypeMeta: metav1.TypeMeta{
    65  			Kind:       "KubeadmControlPlaneTemplate",
    66  			APIVersion: controlplanev1.GroupVersion.String(),
    67  		},
    68  	}
    69  	kubeadmConfigTemplate := bootstrapv1.KubeadmConfigTemplate{
    70  		TypeMeta: metav1.TypeMeta{
    71  			Kind:       "KubeadmConfigTemplate",
    72  			APIVersion: bootstrapv1.GroupVersion.String(),
    73  		},
    74  	}
    75  	tests := []struct {
    76  		name             string
    77  		globalVariables  []runtimehooksv1.Variable
    78  		requestItems     []runtimehooksv1.GeneratePatchesRequestItem
    79  		expectedResponse *runtimehooksv1.GeneratePatchesResponse
    80  		options          []WalkTemplatesOption
    81  	}{
    82  		{
    83  			name: "Fails for invalid builtin variables",
    84  			globalVariables: []runtimehooksv1.Variable{
    85  				newVariable(runtimehooksv1.BuiltinsName, runtimehooksv1.Builtins{
    86  					Cluster: &runtimehooksv1.ClusterBuiltins{
    87  						Name: "test",
    88  					},
    89  				}),
    90  			},
    91  			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
    92  				requestItem("1", kubeadmControlPlaneTemplate, []runtimehooksv1.Variable{
    93  					newVariable(runtimehooksv1.BuiltinsName, "{invalid-builtin-value}"),
    94  				}),
    95  			},
    96  			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
    97  				CommonResponse: runtimehooksv1.CommonResponse{
    98  					Status:  runtimehooksv1.ResponseStatusFailure,
    99  					Message: fmt.Sprintf("failed to merge builtin variables: failed to unmarshal builtin variable: json: cannot unmarshal string into Go value of type %s.Builtins", runtimehooksv1.GroupVersion.Version),
   100  				},
   101  			},
   102  		},
   103  		{
   104  			name: "Silently ignore unknown types if FailForUnknownTypes option is not set",
   105  			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
   106  				{
   107  					UID: types.UID("1"),
   108  					Object: runtime.RawExtension{
   109  						Raw: []byte("{\"kind\":\"Unknown\",\"apiVersion\":\"controlplane.cluster.x-k8s.io/v1beta1\"}"),
   110  					},
   111  				},
   112  			},
   113  			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
   114  				CommonResponse: runtimehooksv1.CommonResponse{
   115  					Status: runtimehooksv1.ResponseStatusSuccess,
   116  				},
   117  			},
   118  		},
   119  		{
   120  			name: "Fails for unknown types if FailForUnknownTypes option is set",
   121  			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
   122  				{
   123  					UID: types.UID("1"),
   124  					Object: runtime.RawExtension{
   125  						Raw: []byte("{\"kind\":\"Unknown\",\"apiVersion\":\"controlplane.cluster.x-k8s.io/v1beta1\"}"),
   126  					},
   127  				},
   128  			},
   129  			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
   130  				CommonResponse: runtimehooksv1.CommonResponse{
   131  					Status: runtimehooksv1.ResponseStatusFailure,
   132  					Message: "no kind \"Unknown\" is registered for version \"controlplane.cluster.x-k8s." +
   133  						"io/v1beta1\" in scheme",
   134  				},
   135  			},
   136  			options: []WalkTemplatesOption{
   137  				FailForUnknownTypes{},
   138  			},
   139  		},
   140  		{
   141  			name: "Fails for invalid holder ref",
   142  			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
   143  				{
   144  					UID: types.UID("1"),
   145  					Object: runtime.RawExtension{
   146  						Raw: toJSON(kubeadmConfigTemplate),
   147  					},
   148  					HolderReference: runtimehooksv1.HolderReference{
   149  						APIVersion: "invalid/cluster.x-k8s.io/v1beta1",
   150  					},
   151  				},
   152  			},
   153  			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
   154  				CommonResponse: runtimehooksv1.CommonResponse{
   155  					Status: runtimehooksv1.ResponseStatusFailure,
   156  					Message: "error generating patches - HolderReference apiVersion \"invalid/cluster.x-k8s." +
   157  						"io/v1beta1\" is not in valid format: unexpected GroupVersion string: invalid/cluster.x-k8s." +
   158  						"io/v1beta1",
   159  				},
   160  			},
   161  			options: []WalkTemplatesOption{
   162  				FailForUnknownTypes{},
   163  			},
   164  		},
   165  		{
   166  			name: "Creates Json patches by default",
   167  			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
   168  				requestItem("1", kubeadmControlPlaneTemplate, nil),
   169  				requestItem("2", kubeadmConfigTemplate, nil),
   170  			},
   171  			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
   172  				CommonResponse: runtimehooksv1.CommonResponse{
   173  					Status: runtimehooksv1.ResponseStatusSuccess,
   174  				},
   175  				Items: []runtimehooksv1.GeneratePatchesResponseItem{
   176  					responseItem("1", "[{\"op\":\"add\",\"path\":\"/metadata/annotations\",\"value\":{\"a\":\"a\"}}]", runtimehooksv1.JSONPatchType),
   177  					responseItem("2", "[{\"op\":\"add\",\"path\":\"/metadata/annotations\",\"value\":{\"b\":\"b\"}}]", runtimehooksv1.JSONPatchType),
   178  				},
   179  			},
   180  		},
   181  		{
   182  			name: "Creates Json patches if option PatchFormat is set to JSONPatchType",
   183  			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
   184  				requestItem("1", kubeadmControlPlaneTemplate, nil),
   185  				requestItem("2", kubeadmConfigTemplate, nil),
   186  			},
   187  			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
   188  				CommonResponse: runtimehooksv1.CommonResponse{
   189  					Status: runtimehooksv1.ResponseStatusSuccess,
   190  				},
   191  				Items: []runtimehooksv1.GeneratePatchesResponseItem{
   192  					responseItem("1", "[{\"op\":\"add\",\"path\":\"/metadata/annotations\",\"value\":{\"a\":\"a\"}}]", runtimehooksv1.JSONPatchType),
   193  					responseItem("2", "[{\"op\":\"add\",\"path\":\"/metadata/annotations\",\"value\":{\"b\":\"b\"}}]", runtimehooksv1.JSONPatchType),
   194  				},
   195  			},
   196  			options: []WalkTemplatesOption{
   197  				PatchFormat{Format: runtimehooksv1.JSONPatchType},
   198  			},
   199  		},
   200  		{
   201  			name: "Creates Json Merge patches if option PatchFormat is set to JSONMergePatchType",
   202  			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
   203  				requestItem("1", kubeadmControlPlaneTemplate, nil),
   204  				requestItem("2", kubeadmConfigTemplate, nil),
   205  			},
   206  			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
   207  				CommonResponse: runtimehooksv1.CommonResponse{
   208  					Status: runtimehooksv1.ResponseStatusSuccess,
   209  				},
   210  				Items: []runtimehooksv1.GeneratePatchesResponseItem{
   211  					responseItem("1", "{\"metadata\":{\"annotations\":{\"a\":\"a\"}}}", runtimehooksv1.JSONMergePatchType),
   212  					responseItem("2", "{\"metadata\":{\"annotations\":{\"b\":\"b\"}}}", runtimehooksv1.JSONMergePatchType),
   213  				},
   214  			},
   215  			options: []WalkTemplatesOption{
   216  				PatchFormat{Format: runtimehooksv1.JSONMergePatchType},
   217  			},
   218  		},
   219  	}
   220  	for _, tt := range tests {
   221  		t.Run(tt.name, func(*testing.T) {
   222  			response := &runtimehooksv1.GeneratePatchesResponse{}
   223  			request := &runtimehooksv1.GeneratePatchesRequest{Variables: tt.globalVariables, Items: tt.requestItems}
   224  
   225  			WalkTemplates(context.Background(), decoder, request, response, mutatingFunc, tt.options...)
   226  
   227  			g.Expect(response.Status).To(Equal(tt.expectedResponse.Status))
   228  			g.Expect(response.Message).To(ContainSubstring(tt.expectedResponse.Message))
   229  			for i, item := range response.Items {
   230  				expectedItem := tt.expectedResponse.Items[i]
   231  				g.Expect(item.PatchType).To(Equal(expectedItem.PatchType))
   232  				g.Expect(item.UID).To(Equal(expectedItem.UID))
   233  
   234  				switch item.PatchType {
   235  				case runtimehooksv1.JSONPatchType:
   236  					// Note; the order of the individual patch operations in Items[].Patch is not deterministic so we unmarshal
   237  					// to an array and check that the arrays hold equivalent items.
   238  					var actualPatchOps []map[string]interface{}
   239  					var expectedPatchOps []map[string]interface{}
   240  					g.Expect(json.Unmarshal(item.Patch, &actualPatchOps)).To(Succeed())
   241  					g.Expect(json.Unmarshal(expectedItem.Patch, &expectedPatchOps)).To(Succeed())
   242  					g.Expect(actualPatchOps).To(ConsistOf(expectedPatchOps))
   243  				case runtimehooksv1.JSONMergePatchType:
   244  					g.Expect(item.Patch).To(Equal(expectedItem.Patch))
   245  				}
   246  			}
   247  		})
   248  	}
   249  }
   250  
   251  // toJSONCompact is used to be able to write JSON values in a readable manner.
   252  func toJSONCompact(value string) []byte {
   253  	var compactValue bytes.Buffer
   254  	if err := json.Compact(&compactValue, []byte(value)); err != nil {
   255  		panic(err)
   256  	}
   257  	return compactValue.Bytes()
   258  }
   259  
   260  // toJSON marshals the object and returns a byte array. This function panics on any error.
   261  func toJSON(val interface{}) []byte {
   262  	jsonStr, err := json.Marshal(val)
   263  	if err != nil {
   264  		panic(err)
   265  	}
   266  	return jsonStr
   267  }
   268  
   269  // requestItem returns a GeneratePatchesRequestItem with the given uid, variables and object.
   270  func requestItem(uid string, object interface{}, variables []runtimehooksv1.Variable) runtimehooksv1.GeneratePatchesRequestItem {
   271  	return runtimehooksv1.GeneratePatchesRequestItem{
   272  		UID:       types.UID(uid),
   273  		Variables: variables,
   274  		Object: runtime.RawExtension{
   275  			Raw: toJSON(object),
   276  		},
   277  	}
   278  }
   279  
   280  // responseItem returns a GeneratePatchesResponseItem of PatchType JSONPatch with the passed uid and patch.
   281  func responseItem(uid, patch string, format runtimehooksv1.PatchType) runtimehooksv1.GeneratePatchesResponseItem {
   282  	return runtimehooksv1.GeneratePatchesResponseItem{
   283  		UID:       types.UID(uid),
   284  		PatchType: format,
   285  		Patch:     toJSONCompact(patch),
   286  	}
   287  }
   288  
   289  // newVariable returns a runtimehooksv1.Variable with the passed name and value.
   290  func newVariable(name string, value interface{}) runtimehooksv1.Variable {
   291  	return runtimehooksv1.Variable{
   292  		Name:  name,
   293  		Value: apiextensionsv1.JSON{Raw: toJSON(value)},
   294  	}
   295  }