...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s/meta_test.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s

     1  // Copyright 2022 Google LLC
     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 k8s_test
    16  
    17  import (
    18  	"fmt"
    19  	"testing"
    20  
    21  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test"
    24  	testmain "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/main"
    25  	"github.com/appscode/jsonpatch"
    26  	tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    27  	"github.com/nasa9084/go-openapi"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	runtimeschema "k8s.io/apimachinery/pkg/runtime/schema"
    33  	"sigs.k8s.io/controller-runtime/pkg/manager"
    34  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    35  )
    36  
    37  var (
    38  	mgr manager.Manager
    39  )
    40  
    41  func TestIsDeleted(t *testing.T) {
    42  	nowTime := metav1.Now()
    43  	testCases := []struct {
    44  		Name           string
    45  		Time           *metav1.Time
    46  		ExpectedResult bool
    47  	}{
    48  		{"Nil time", nil, false},
    49  		{"Now time", &nowTime, true},
    50  	}
    51  	for _, tc := range testCases {
    52  		t.Run(tc.Name, func(t *testing.T) {
    53  			meta := metav1.ObjectMeta{
    54  				DeletionTimestamp: tc.Time,
    55  			}
    56  			result := k8s.IsDeleted(&meta)
    57  			if result != tc.ExpectedResult {
    58  				t.Errorf("result mismatch: got '%v', want '%v'", result, tc.ExpectedResult)
    59  			}
    60  		})
    61  	}
    62  }
    63  
    64  func TestGVKToGVR(t *testing.T) {
    65  	tests := []struct {
    66  		gvk         runtimeschema.GroupVersionKind
    67  		expectedGVR runtimeschema.GroupVersionResource
    68  	}{
    69  		{
    70  			gvk:         runtimeschema.GroupVersionKind{Kind: "ComputeVPNGateway"},
    71  			expectedGVR: runtimeschema.GroupVersionResource{Resource: "computevpngateways"},
    72  		},
    73  		{
    74  			gvk:         runtimeschema.GroupVersionKind{Kind: "KMSCryptoKey"},
    75  			expectedGVR: runtimeschema.GroupVersionResource{Resource: "kmscryptokeys"},
    76  		},
    77  		{
    78  			gvk:         runtimeschema.GroupVersionKind{Kind: "IAMPolicy"},
    79  			expectedGVR: runtimeschema.GroupVersionResource{Resource: "iampolicies"},
    80  		},
    81  		{
    82  			gvk:         runtimeschema.GroupVersionKind{Kind: "ComputeAddress"},
    83  			expectedGVR: runtimeschema.GroupVersionResource{Resource: "computeaddresses"},
    84  		},
    85  		{
    86  			gvk:         runtimeschema.GroupVersionKind{Kind: "FirestoreIndex"},
    87  			expectedGVR: runtimeschema.GroupVersionResource{Resource: "firestoreindexes"},
    88  		},
    89  		{
    90  			gvk:         runtimeschema.GroupVersionKind{Kind: "NetworkServicesMesh"},
    91  			expectedGVR: runtimeschema.GroupVersionResource{Resource: "networkservicesmeshes"},
    92  		},
    93  		{
    94  			gvk:         runtimeschema.GroupVersionKind{Kind: "PubSubTopic"},
    95  			expectedGVR: runtimeschema.GroupVersionResource{Resource: "pubsubtopics"},
    96  		},
    97  	}
    98  	for _, tc := range tests {
    99  		if got, want := k8s.ToGVR(tc.gvk), tc.expectedGVR; got != want {
   100  			t.Errorf("result mismatch: got '%v', want '%v'", got, want)
   101  		}
   102  	}
   103  }
   104  
   105  func TestHasAbandonAnnotation(t *testing.T) {
   106  	tests := []struct {
   107  		name                 string
   108  		annotations          map[string]string
   109  		hasAbandonAnnotation bool
   110  	}{
   111  		{
   112  			name: "has deletion policy annotation set as abandon",
   113  			annotations: map[string]string{
   114  				k8s.DeletionPolicyAnnotation: k8s.DeletionPolicyAbandon,
   115  			},
   116  			hasAbandonAnnotation: true,
   117  		},
   118  		{
   119  			name: "has deletion policy annotation set as delete",
   120  			annotations: map[string]string{
   121  				k8s.DeletionPolicyAnnotation: k8s.DeletionPolicyDelete,
   122  			},
   123  			hasAbandonAnnotation: false,
   124  		},
   125  		{
   126  			name: "has deletion policy annotation set to empty string",
   127  			annotations: map[string]string{
   128  				k8s.DeletionPolicyAnnotation: "",
   129  			},
   130  			hasAbandonAnnotation: false,
   131  		},
   132  		{
   133  			name:                 "has no deletion policy annotation",
   134  			annotations:          map[string]string{},
   135  			hasAbandonAnnotation: false,
   136  		},
   137  		{
   138  			name:                 "has nil annotations map",
   139  			hasAbandonAnnotation: false,
   140  		},
   141  	}
   142  	for _, tc := range tests {
   143  		tc := tc
   144  		t.Run(tc.name, func(t *testing.T) {
   145  			t.Parallel()
   146  			obj := &unstructured.Unstructured{}
   147  			obj.SetAnnotations(tc.annotations)
   148  			actual := k8s.HasAbandonAnnotation(obj)
   149  			if actual != tc.hasAbandonAnnotation {
   150  				t.Errorf("incorrect value for HasAbandonAnnotation(): got %v, want %v", actual, tc.hasAbandonAnnotation)
   151  			}
   152  		})
   153  	}
   154  }
   155  
   156  func TestSetDefaultContainerAnnotation(t *testing.T) {
   157  	const (
   158  		nsName    = "namespace-1"
   159  		projectID = "project-1"
   160  		folderID  = "1234567890"
   161  		orgID     = "0987654321"
   162  	)
   163  	tests := []struct {
   164  		name            string
   165  		objAnnotations  map[string]string
   166  		nsAnnotations   map[string]string
   167  		containers      []corekccv1alpha1.Container
   168  		expectedPatches []jsonpatch.JsonPatchOperation
   169  		shouldErr       bool
   170  	}{
   171  		{
   172  			name:          "no defaulting if containers list is empty",
   173  			nsAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
   174  			containers:    []corekccv1alpha1.Container{},
   175  		},
   176  		{
   177  			name:           "prefer resource-level to namespace-level annotation for same type",
   178  			objAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
   179  			nsAnnotations:  map[string]string{k8s.ProjectIDAnnotation: "other-project-id"},
   180  			containers: []corekccv1alpha1.Container{
   181  				{Type: corekccv1alpha1.ContainerTypeProject},
   182  			},
   183  		},
   184  		{
   185  			name:           "prefer resource-level to namespace-level annotation for different types",
   186  			objAnnotations: map[string]string{k8s.FolderIDAnnotation: folderID},
   187  			nsAnnotations:  map[string]string{k8s.ProjectIDAnnotation: projectID},
   188  			containers: []corekccv1alpha1.Container{
   189  				{Type: corekccv1alpha1.ContainerTypeProject},
   190  				{Type: corekccv1alpha1.ContainerTypeFolder},
   191  			},
   192  		},
   193  		{
   194  			name:           "prefer resource-level annotation to namespace name",
   195  			objAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
   196  			containers: []corekccv1alpha1.Container{
   197  				{Type: corekccv1alpha1.ContainerTypeProject},
   198  			},
   199  		},
   200  		{
   201  			name:           "add annotation from namespace-level when no resource-level annotation present",
   202  			objAnnotations: map[string]string{"key": "value"},
   203  			nsAnnotations:  map[string]string{k8s.ProjectIDAnnotation: projectID},
   204  			containers: []corekccv1alpha1.Container{
   205  				{Type: corekccv1alpha1.ContainerTypeProject},
   206  			},
   207  			expectedPatches: []jsonpatch.JsonPatchOperation{{
   208  				Operation: "add",
   209  				Path:      fmt.Sprintf("/metadata/annotations/%v~1%v", k8s.AnnotationPrefix, "project-id"),
   210  				Value:     projectID,
   211  			}},
   212  		},
   213  		{
   214  			name:          "defaulting creates a new annotations map when none present",
   215  			nsAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
   216  			containers: []corekccv1alpha1.Container{
   217  				{Type: corekccv1alpha1.ContainerTypeProject},
   218  			},
   219  			expectedPatches: []jsonpatch.JsonPatchOperation{{
   220  				Operation: "add",
   221  				Path:      "/metadata/annotations",
   222  				Value:     map[string]interface{}{k8s.ProjectIDAnnotation: projectID},
   223  			}},
   224  		},
   225  		{
   226  			name:           "project-scoped resources use namespace name as project ID when no override present",
   227  			objAnnotations: map[string]string{"key": "value"},
   228  			containers: []corekccv1alpha1.Container{
   229  				{Type: corekccv1alpha1.ContainerTypeProject},
   230  			},
   231  			expectedPatches: []jsonpatch.JsonPatchOperation{{
   232  				Operation: "add",
   233  				Path:      fmt.Sprintf("/metadata/annotations/%v~1%v", k8s.AnnotationPrefix, "project-id"),
   234  				Value:     nsName,
   235  			}},
   236  		},
   237  		{
   238  			name:           "folder-scoped resources use folder ID annotation",
   239  			objAnnotations: map[string]string{"key": "value"},
   240  			nsAnnotations:  map[string]string{k8s.FolderIDAnnotation: folderID},
   241  			containers: []corekccv1alpha1.Container{
   242  				{Type: corekccv1alpha1.ContainerTypeFolder},
   243  			},
   244  			expectedPatches: []jsonpatch.JsonPatchOperation{{
   245  				Operation: "add",
   246  				Path:      fmt.Sprintf("/metadata/annotations/%v~1%v", k8s.AnnotationPrefix, "folder-id"),
   247  				Value:     folderID,
   248  			}},
   249  		},
   250  		{
   251  			name:           "org-scoped resources use org ID annotation",
   252  			objAnnotations: map[string]string{"key": "value"},
   253  			nsAnnotations:  map[string]string{k8s.OrgIDAnnotation: orgID},
   254  			containers: []corekccv1alpha1.Container{
   255  				{Type: corekccv1alpha1.ContainerTypeOrganization},
   256  			},
   257  			expectedPatches: []jsonpatch.JsonPatchOperation{{
   258  				Operation: "add",
   259  				Path:      fmt.Sprintf("/metadata/annotations/%v~1%v", k8s.AnnotationPrefix, "organization-id"),
   260  				Value:     orgID,
   261  			}},
   262  		},
   263  		{
   264  			name: "fail if no default can be determined for non-project-scoped resources",
   265  			containers: []corekccv1alpha1.Container{
   266  				{Type: corekccv1alpha1.ContainerTypeOrganization},
   267  			},
   268  			shouldErr: true,
   269  		},
   270  		{
   271  			name: "fail if ambiguous resource-level container annotation",
   272  			objAnnotations: map[string]string{
   273  				k8s.FolderIDAnnotation: folderID,
   274  				k8s.OrgIDAnnotation:    orgID,
   275  			},
   276  			containers: []corekccv1alpha1.Container{
   277  				{Type: corekccv1alpha1.ContainerTypeFolder},
   278  				{Type: corekccv1alpha1.ContainerTypeOrganization},
   279  			},
   280  			shouldErr: true,
   281  		},
   282  		{
   283  			name: "fail if ambiguous resource-level container annotation (with one being set to empty string)",
   284  			objAnnotations: map[string]string{
   285  				k8s.FolderIDAnnotation: "",
   286  				k8s.OrgIDAnnotation:    orgID,
   287  			},
   288  			containers: []corekccv1alpha1.Container{
   289  				{Type: corekccv1alpha1.ContainerTypeFolder},
   290  				{Type: corekccv1alpha1.ContainerTypeOrganization},
   291  			},
   292  			shouldErr: true,
   293  		},
   294  		{
   295  			name: "fail if ambiguous resource-level container annotation (with both being set to empty string)",
   296  			objAnnotations: map[string]string{
   297  				k8s.FolderIDAnnotation: "",
   298  				k8s.OrgIDAnnotation:    "",
   299  			},
   300  			containers: []corekccv1alpha1.Container{
   301  				{Type: corekccv1alpha1.ContainerTypeFolder},
   302  				{Type: corekccv1alpha1.ContainerTypeOrganization},
   303  			},
   304  			shouldErr: true,
   305  		},
   306  		{
   307  			name: "fail if ambiguous namespace-level container annotation",
   308  			nsAnnotations: map[string]string{
   309  				k8s.FolderIDAnnotation: folderID,
   310  				k8s.OrgIDAnnotation:    orgID,
   311  			},
   312  			containers: []corekccv1alpha1.Container{
   313  				{Type: corekccv1alpha1.ContainerTypeFolder},
   314  				{Type: corekccv1alpha1.ContainerTypeOrganization},
   315  			},
   316  			shouldErr: true,
   317  		},
   318  		{
   319  			name: "fail if ambiguous namespace-level container annotation (with one being set to empty string)",
   320  			nsAnnotations: map[string]string{
   321  				k8s.FolderIDAnnotation: "",
   322  				k8s.OrgIDAnnotation:    orgID,
   323  			},
   324  			containers: []corekccv1alpha1.Container{
   325  				{Type: corekccv1alpha1.ContainerTypeFolder},
   326  				{Type: corekccv1alpha1.ContainerTypeOrganization},
   327  			},
   328  			shouldErr: true,
   329  		},
   330  		{
   331  			name: "fail if ambiguous namespace-level container annotation (with both being set to empty string)",
   332  			nsAnnotations: map[string]string{
   333  				k8s.FolderIDAnnotation: "",
   334  				k8s.OrgIDAnnotation:    "",
   335  			},
   336  			containers: []corekccv1alpha1.Container{
   337  				{Type: corekccv1alpha1.ContainerTypeFolder},
   338  				{Type: corekccv1alpha1.ContainerTypeOrganization},
   339  			},
   340  			shouldErr: true,
   341  		},
   342  	}
   343  	for _, tc := range tests {
   344  		tc := tc
   345  		t.Run(tc.name, func(t *testing.T) {
   346  			ns := &corev1.Namespace{}
   347  			ns.SetName(nsName)
   348  			ns.SetAnnotations(tc.nsAnnotations)
   349  
   350  			obj := &unstructured.Unstructured{}
   351  			obj.SetNamespace(nsName)
   352  			obj.SetAnnotations(tc.objAnnotations)
   353  
   354  			newObj := obj.DeepCopy()
   355  			err := k8s.SetDefaultContainerAnnotation(newObj, ns, tc.containers)
   356  			if tc.shouldErr {
   357  				if err == nil {
   358  					t.Errorf("expected error but there was none")
   359  				}
   360  				return
   361  			} else {
   362  				if err != nil {
   363  					t.Errorf("error setting default container annotation: %v", err)
   364  					return
   365  				}
   366  			}
   367  			objRaw, err := obj.MarshalJSON()
   368  			if err != nil {
   369  				t.Fatalf("error marshaling old object as JSON: %v", err)
   370  			}
   371  			newObjRaw, err := newObj.MarshalJSON()
   372  			if err != nil {
   373  				t.Fatalf("error marshaling new object as JSON: %v", err)
   374  			}
   375  			patches := admission.PatchResponseFromRaw(objRaw, newObjRaw).Patches
   376  			if len(patches) != len(tc.expectedPatches) {
   377  				t.Errorf("expected %v patch(es), but got %v; expected: %+v, actual: %+v",
   378  					len(tc.expectedPatches), len(patches), tc.expectedPatches, patches)
   379  				return
   380  			}
   381  			// Should only have either 0 or 1 patches, so ordering is unimportant
   382  			for i, p := range patches {
   383  				if !test.Equals(t, tc.expectedPatches[i], p) {
   384  					t.Errorf("expected patch: %+v, actual patch: %+v", tc.expectedPatches[i], p)
   385  				}
   386  			}
   387  		})
   388  	}
   389  }
   390  
   391  func TestValidateOrDefaultManagementConflictPreventionAnnotationForTFBasedResource(t *testing.T) {
   392  	tests := []struct {
   393  		Name                                  string
   394  		ManagementConflictNamespaceAnnotation string
   395  		ManagementConflictObjectAnnotation    string
   396  		MetadataMappingLabels                 string
   397  		LabelsFieldIsMutable                  bool
   398  		ExpectedObjectAnnotation              string
   399  		ShouldSucceed                         bool
   400  	}{
   401  		{
   402  			Name:                                  "none policy on namespace, empty on object",
   403  			ManagementConflictNamespaceAnnotation: "none",
   404  			ManagementConflictObjectAnnotation:    "",
   405  			MetadataMappingLabels:                 "",
   406  			ExpectedObjectAnnotation:              "none",
   407  			ShouldSucceed:                         true,
   408  		},
   409  		{
   410  			Name:                                  "none policy on namespace, resource on object",
   411  			ManagementConflictNamespaceAnnotation: "none",
   412  			ManagementConflictObjectAnnotation:    "resource",
   413  			MetadataMappingLabels:                 "labels_field",
   414  			LabelsFieldIsMutable:                  true,
   415  			ExpectedObjectAnnotation:              "resource",
   416  			ShouldSucceed:                         true,
   417  		},
   418  		{
   419  			Name:                                  "none policy on namespace, none on object",
   420  			ManagementConflictNamespaceAnnotation: "none",
   421  			ManagementConflictObjectAnnotation:    "none",
   422  			MetadataMappingLabels:                 "",
   423  			ExpectedObjectAnnotation:              "none",
   424  			ShouldSucceed:                         true,
   425  		},
   426  		{
   427  			Name:                                  "resource policy on namespace, empty on object",
   428  			ManagementConflictNamespaceAnnotation: "resource",
   429  			ManagementConflictObjectAnnotation:    "",
   430  			MetadataMappingLabels:                 "labels_field",
   431  			LabelsFieldIsMutable:                  true,
   432  			ExpectedObjectAnnotation:              "resource",
   433  			ShouldSucceed:                         true,
   434  		},
   435  		{
   436  			Name:                                  "resource policy on namespace, resource on object",
   437  			ManagementConflictNamespaceAnnotation: "resource",
   438  			ManagementConflictObjectAnnotation:    "resource",
   439  			MetadataMappingLabels:                 "labels_field",
   440  			LabelsFieldIsMutable:                  true,
   441  			ExpectedObjectAnnotation:              "resource",
   442  			ShouldSucceed:                         true,
   443  		},
   444  		{
   445  			Name:                                  "resource policy on namespace, none on object",
   446  			ManagementConflictNamespaceAnnotation: "resource",
   447  			ManagementConflictObjectAnnotation:    "none",
   448  			MetadataMappingLabels:                 "labels_field",
   449  			LabelsFieldIsMutable:                  true,
   450  			ExpectedObjectAnnotation:              "none",
   451  			ShouldSucceed:                         true,
   452  		},
   453  		{
   454  			Name:                                  "resource policy on namespace with no labels support should default to none",
   455  			ManagementConflictNamespaceAnnotation: "resource",
   456  			ManagementConflictObjectAnnotation:    "",
   457  			MetadataMappingLabels:                 "",
   458  			ExpectedObjectAnnotation:              "none",
   459  			ShouldSucceed:                         true,
   460  		},
   461  		{
   462  			Name:                                  "resource policy on namespace with immutable labels should default to none",
   463  			ManagementConflictNamespaceAnnotation: "resource",
   464  			ManagementConflictObjectAnnotation:    "",
   465  			MetadataMappingLabels:                 "labels_field",
   466  			LabelsFieldIsMutable:                  false,
   467  			ExpectedObjectAnnotation:              "none",
   468  			ShouldSucceed:                         true,
   469  		},
   470  		{
   471  			Name:                                  "resource policy on object should require labels support",
   472  			ManagementConflictNamespaceAnnotation: "",
   473  			ManagementConflictObjectAnnotation:    "resource",
   474  			MetadataMappingLabels:                 "",
   475  			ExpectedObjectAnnotation:              "resource",
   476  			ShouldSucceed:                         false,
   477  		},
   478  		{
   479  			Name:                                  "resource policy on object should require mutable labels",
   480  			ManagementConflictNamespaceAnnotation: "",
   481  			ManagementConflictObjectAnnotation:    "resource",
   482  			MetadataMappingLabels:                 "labels_field",
   483  			LabelsFieldIsMutable:                  false,
   484  			ExpectedObjectAnnotation:              "resource",
   485  			ShouldSucceed:                         false,
   486  		},
   487  		{
   488  			Name:                                  "invalid policy on namespace",
   489  			ManagementConflictNamespaceAnnotation: "invalid",
   490  			ManagementConflictObjectAnnotation:    "",
   491  			MetadataMappingLabels:                 "",
   492  			ExpectedObjectAnnotation:              "",
   493  			ShouldSucceed:                         false,
   494  		},
   495  		{
   496  			Name:                                  "invalid policy on object",
   497  			ManagementConflictNamespaceAnnotation: "resource",
   498  			ManagementConflictObjectAnnotation:    "invalid",
   499  			MetadataMappingLabels:                 "",
   500  			ExpectedObjectAnnotation:              "invalid",
   501  			ShouldSucceed:                         false,
   502  		},
   503  		{
   504  			Name:                                  "no value on namespace or resource with no labels support (i.e. default behavior when the resource doesn't support labels)",
   505  			ManagementConflictNamespaceAnnotation: "",
   506  			ManagementConflictObjectAnnotation:    "",
   507  			MetadataMappingLabels:                 "",
   508  			ExpectedObjectAnnotation:              "none",
   509  			ShouldSucceed:                         true,
   510  		},
   511  		{
   512  			Name:                                  "no value on namespace or resource with immutable labels (i.e. default behavior when the resource doesn't support mutable labels)",
   513  			ManagementConflictNamespaceAnnotation: "",
   514  			ManagementConflictObjectAnnotation:    "",
   515  			MetadataMappingLabels:                 "",
   516  			LabelsFieldIsMutable:                  false,
   517  			ExpectedObjectAnnotation:              "none",
   518  			ShouldSucceed:                         true,
   519  		},
   520  		{
   521  			Name:                                  "no value on namespace or resource with mutable labels (i.e. default behavior when the resource supports mutable labels)",
   522  			ManagementConflictNamespaceAnnotation: "",
   523  			ManagementConflictObjectAnnotation:    "",
   524  			MetadataMappingLabels:                 "labels_value",
   525  			LabelsFieldIsMutable:                  true,
   526  			ExpectedObjectAnnotation:              "none",
   527  			ShouldSucceed:                         true,
   528  		},
   529  	}
   530  	for _, tc := range tests {
   531  		t.Run(tc.Name, func(t *testing.T) {
   532  			ns := corev1.Namespace{}
   533  			ns.SetName("my-namespace")
   534  			ns.SetAnnotations(newManagementConflictAnnotations(tc.ManagementConflictNamespaceAnnotation))
   535  			obj := unstructured.Unstructured{}
   536  			obj.SetAnnotations(newManagementConflictAnnotations(tc.ManagementConflictObjectAnnotation))
   537  
   538  			fakeTFResourceName := "google_fake_resource"
   539  			fakeTFResource := &tfschema.Resource{
   540  				Schema: map[string]*tfschema.Schema{},
   541  			}
   542  			fakeTFLabelsField := tc.MetadataMappingLabels
   543  			if fakeTFLabelsField != "" {
   544  				fakeTFResource.Schema[fakeTFLabelsField] = &tfschema.Schema{
   545  					ForceNew: !tc.LabelsFieldIsMutable,
   546  				}
   547  			}
   548  			fakeTFProvider := &tfschema.Provider{
   549  				ResourcesMap: map[string]*tfschema.Resource{
   550  					fakeTFResourceName: fakeTFResource,
   551  				},
   552  			}
   553  			rc := corekccv1alpha1.ResourceConfig{
   554  				Name: fakeTFResourceName,
   555  				MetadataMapping: corekccv1alpha1.MetadataMapping{
   556  					Labels: fakeTFLabelsField,
   557  				},
   558  			}
   559  
   560  			err := k8s.ValidateOrDefaultManagementConflictPreventionAnnotationForTFBasedResource(&obj, &ns, &rc, fakeTFProvider.ResourcesMap)
   561  			if tc.ShouldSucceed != (err == nil) {
   562  				t.Fatalf("expected success to be '%v', instead got error mismsatch: %v", tc.ShouldSucceed, err)
   563  			}
   564  			value, ok := k8s.GetAnnotation(k8s.ManagementConflictPreventionPolicyFullyQualifiedAnnotation, &obj)
   565  			if ok || tc.ExpectedObjectAnnotation != "" {
   566  				if value != tc.ExpectedObjectAnnotation {
   567  					t.Fatalf("unexpected management conflict annotation value: got '%v', want '%v'", value, tc.ExpectedObjectAnnotation)
   568  				}
   569  			}
   570  		})
   571  	}
   572  }
   573  
   574  func TestValidateOrDefaultManagementConflictPreventionAnnotationForDCLBasedResource(t *testing.T) {
   575  	tests := []struct {
   576  		Name                                  string
   577  		ManagementConflictNamespaceAnnotation string
   578  		ManagementConflictObjectAnnotation    string
   579  		Schema                                *openapi.Schema
   580  		ExpectedObjectAnnotation              string
   581  		ShouldSucceed                         bool
   582  	}{
   583  		{
   584  			Name:                                  "none policy on namespace, empty on object",
   585  			ManagementConflictNamespaceAnnotation: "none",
   586  			ManagementConflictObjectAnnotation:    "",
   587  			Schema: &openapi.Schema{
   588  				Type: "object",
   589  				Properties: map[string]*openapi.Schema{
   590  					"labels": &openapi.Schema{
   591  						Type: "string",
   592  					},
   593  				},
   594  				Extension: map[string]interface{}{
   595  					"x-dcl-labels": "labels",
   596  				},
   597  			},
   598  			ExpectedObjectAnnotation: "none",
   599  			ShouldSucceed:            true,
   600  		},
   601  		{
   602  			Name:                                  "none policy on namespace, resource on object",
   603  			ManagementConflictNamespaceAnnotation: "none",
   604  			ManagementConflictObjectAnnotation:    "resource",
   605  			Schema: &openapi.Schema{
   606  				Type: "object",
   607  				Properties: map[string]*openapi.Schema{
   608  					"labels": &openapi.Schema{
   609  						Type: "string",
   610  					},
   611  				},
   612  				Extension: map[string]interface{}{
   613  					"x-dcl-labels": "labels",
   614  				},
   615  			},
   616  			ExpectedObjectAnnotation: "resource",
   617  			ShouldSucceed:            true,
   618  		},
   619  		{
   620  			Name:                                  "none policy on namespace, none on object",
   621  			ManagementConflictNamespaceAnnotation: "none",
   622  			ManagementConflictObjectAnnotation:    "none",
   623  			Schema: &openapi.Schema{
   624  				Type: "object",
   625  			},
   626  			ExpectedObjectAnnotation: "none",
   627  			ShouldSucceed:            true,
   628  		},
   629  		{
   630  			Name:                                  "resource policy on namespace, empty on object",
   631  			ManagementConflictNamespaceAnnotation: "resource",
   632  			ManagementConflictObjectAnnotation:    "",
   633  			Schema: &openapi.Schema{
   634  				Type: "object",
   635  				Properties: map[string]*openapi.Schema{
   636  					"labels": &openapi.Schema{
   637  						Type: "string",
   638  					},
   639  				},
   640  				Extension: map[string]interface{}{
   641  					"x-dcl-labels": "labels",
   642  				},
   643  			},
   644  			ExpectedObjectAnnotation: "resource",
   645  			ShouldSucceed:            true,
   646  		},
   647  		{
   648  			Name:                                  "resource policy on namespace, resource on object",
   649  			ManagementConflictNamespaceAnnotation: "resource",
   650  			ManagementConflictObjectAnnotation:    "resource",
   651  			Schema: &openapi.Schema{
   652  				Type: "object",
   653  				Properties: map[string]*openapi.Schema{
   654  					"labels": &openapi.Schema{
   655  						Type: "string",
   656  					},
   657  				},
   658  				Extension: map[string]interface{}{
   659  					"x-dcl-labels": "labels",
   660  				},
   661  			},
   662  			ExpectedObjectAnnotation: "resource",
   663  			ShouldSucceed:            true,
   664  		},
   665  		{
   666  			Name:                                  "resource policy on namespace, none on object",
   667  			ManagementConflictNamespaceAnnotation: "resource",
   668  			ManagementConflictObjectAnnotation:    "none",
   669  			Schema: &openapi.Schema{
   670  				Type: "object",
   671  				Properties: map[string]*openapi.Schema{
   672  					"labels": &openapi.Schema{
   673  						Type: "string",
   674  					},
   675  				},
   676  				Extension: map[string]interface{}{
   677  					"x-dcl-labels": "labels",
   678  				},
   679  			},
   680  			ExpectedObjectAnnotation: "none",
   681  			ShouldSucceed:            true,
   682  		},
   683  		{
   684  			Name:                                  "resource policy on namespace with no labels support should default to none",
   685  			ManagementConflictNamespaceAnnotation: "resource",
   686  			Schema: &openapi.Schema{
   687  				Type: "object",
   688  			},
   689  			ExpectedObjectAnnotation: "none",
   690  			ShouldSucceed:            true,
   691  		},
   692  		{
   693  			Name:                                  "resource policy on namespace with immutable labels should default to none",
   694  			ManagementConflictNamespaceAnnotation: "resource",
   695  			ManagementConflictObjectAnnotation:    "",
   696  			Schema: &openapi.Schema{
   697  				Type: "object",
   698  				Properties: map[string]*openapi.Schema{
   699  					"labels": &openapi.Schema{
   700  						Type: "string",
   701  						Extension: map[string]interface{}{
   702  							"x-kubernetes-immutable": true,
   703  						},
   704  					},
   705  				},
   706  				Extension: map[string]interface{}{
   707  					"x-dcl-labels": "labels",
   708  				},
   709  			},
   710  			ExpectedObjectAnnotation: "none",
   711  			ShouldSucceed:            true,
   712  		},
   713  		{
   714  			Name:                                  "resource policy on object should require labels support",
   715  			ManagementConflictNamespaceAnnotation: "",
   716  			ManagementConflictObjectAnnotation:    "resource",
   717  			Schema: &openapi.Schema{
   718  				Type: "object",
   719  			},
   720  			ExpectedObjectAnnotation: "resource",
   721  			ShouldSucceed:            false,
   722  		},
   723  		{
   724  			Name:                                  "resource policy on object should require mutable labels",
   725  			ManagementConflictNamespaceAnnotation: "",
   726  			ManagementConflictObjectAnnotation:    "resource",
   727  			Schema: &openapi.Schema{
   728  				Type: "object",
   729  				Properties: map[string]*openapi.Schema{
   730  					"labels": &openapi.Schema{
   731  						Type: "string",
   732  						Extension: map[string]interface{}{
   733  							"x-kubernetes-immutable": true,
   734  						},
   735  					},
   736  				},
   737  				Extension: map[string]interface{}{
   738  					"x-dcl-labels": "labels",
   739  				},
   740  			},
   741  			ExpectedObjectAnnotation: "resource",
   742  			ShouldSucceed:            false,
   743  		},
   744  		{
   745  			Name:                                  "invalid policy on namespace",
   746  			ManagementConflictNamespaceAnnotation: "invalid",
   747  			ManagementConflictObjectAnnotation:    "",
   748  			Schema: &openapi.Schema{
   749  				Type: "object",
   750  			},
   751  			ExpectedObjectAnnotation: "",
   752  			ShouldSucceed:            false,
   753  		},
   754  		{
   755  			Name:                                  "invalid policy on object",
   756  			ManagementConflictNamespaceAnnotation: "resource",
   757  			ManagementConflictObjectAnnotation:    "invalid",
   758  			Schema: &openapi.Schema{
   759  				Type: "object",
   760  			},
   761  			ExpectedObjectAnnotation: "invalid",
   762  			ShouldSucceed:            false,
   763  		},
   764  		{
   765  			Name:                                  "no value on namespace or resource with no labels support (i.e. default behavior when the resource doesn't support labels)",
   766  			ManagementConflictNamespaceAnnotation: "",
   767  			ManagementConflictObjectAnnotation:    "",
   768  			Schema: &openapi.Schema{
   769  				Type: "object",
   770  			},
   771  			ExpectedObjectAnnotation: "none",
   772  			ShouldSucceed:            true,
   773  		},
   774  		{
   775  			Name:                                  "no value on namespace or resource with immutable labels (i.e. default behavior when the resource doesn't support mutable labels)",
   776  			ManagementConflictNamespaceAnnotation: "",
   777  			ManagementConflictObjectAnnotation:    "",
   778  			Schema: &openapi.Schema{
   779  				Type: "object",
   780  				Properties: map[string]*openapi.Schema{
   781  					"labels": &openapi.Schema{
   782  						Type: "string",
   783  						Extension: map[string]interface{}{
   784  							"x-kubernetes-immutable": true,
   785  						},
   786  					},
   787  				},
   788  				Extension: map[string]interface{}{
   789  					"x-dcl-labels": "labels",
   790  				},
   791  			},
   792  			ExpectedObjectAnnotation: "none",
   793  			ShouldSucceed:            true,
   794  		},
   795  		{
   796  			Name:                                  "no value on namespace or resource with mutable labels (i.e. default behavior when the resource supports mutable labels)",
   797  			ManagementConflictNamespaceAnnotation: "",
   798  			ManagementConflictObjectAnnotation:    "",
   799  			Schema: &openapi.Schema{
   800  				Type: "object",
   801  				Properties: map[string]*openapi.Schema{
   802  					"labels": &openapi.Schema{
   803  						Type: "string",
   804  					},
   805  				},
   806  				Extension: map[string]interface{}{
   807  					"x-dcl-labels": "labels",
   808  				},
   809  			},
   810  			ExpectedObjectAnnotation: "none",
   811  			ShouldSucceed:            true,
   812  		},
   813  	}
   814  	for _, tc := range tests {
   815  		t.Run(tc.Name, func(t *testing.T) {
   816  			ns := corev1.Namespace{}
   817  			ns.SetName("my-namespace")
   818  			ns.SetAnnotations(newManagementConflictAnnotations(tc.ManagementConflictNamespaceAnnotation))
   819  			obj := unstructured.Unstructured{}
   820  			obj.SetAnnotations(newManagementConflictAnnotations(tc.ManagementConflictObjectAnnotation))
   821  
   822  			err := k8s.ValidateOrDefaultManagementConflictPreventionAnnotationForDCLBasedResource(&obj, &ns, tc.Schema)
   823  			if tc.ShouldSucceed != (err == nil) {
   824  				t.Fatalf("expected success to be '%v', instead got error mismsatch: %v", tc.ShouldSucceed, err)
   825  			}
   826  			value, ok := k8s.GetAnnotation(k8s.ManagementConflictPreventionPolicyFullyQualifiedAnnotation, &obj)
   827  			if ok || tc.ExpectedObjectAnnotation != "" {
   828  				if value != tc.ExpectedObjectAnnotation {
   829  					t.Fatalf("unexpected management conflict annotation value: got '%v', want '%v'", value, tc.ExpectedObjectAnnotation)
   830  				}
   831  			}
   832  		})
   833  	}
   834  }
   835  
   836  func TestGetManagementConflictPreventionAnnotationValue(t *testing.T) {
   837  	testCases := []struct {
   838  		Name           string
   839  		Annotations    map[string]string
   840  		ExpectedPolicy k8s.ManagementConflictPreventionPolicy
   841  		ShouldSucceed  bool
   842  	}{
   843  		{
   844  			Name:           "nil annotations should error",
   845  			Annotations:    nil,
   846  			ExpectedPolicy: k8s.ManagementConflictPreventionPolicyNone,
   847  			ShouldSucceed:  false,
   848  		},
   849  		{
   850  			Name:           "missing annotation should error",
   851  			Annotations:    make(map[string]string),
   852  			ExpectedPolicy: k8s.ManagementConflictPreventionPolicyNone,
   853  			ShouldSucceed:  false,
   854  		},
   855  		{
   856  			Name:           "invalid annotation should error",
   857  			Annotations:    newManagementConflictAnnotations("my invalid policy name"),
   858  			ExpectedPolicy: k8s.ManagementConflictPreventionPolicyNone,
   859  			ShouldSucceed:  false,
   860  		},
   861  		{
   862  			Name:           "valid value should succeed",
   863  			Annotations:    newManagementConflictAnnotations(k8s.ManagementConflictPreventionPolicyResource),
   864  			ExpectedPolicy: k8s.ManagementConflictPreventionPolicyResource,
   865  			ShouldSucceed:  true,
   866  		},
   867  	}
   868  	for _, tc := range testCases {
   869  		t.Run(tc.Name, func(t *testing.T) {
   870  			obj := unstructured.Unstructured{}
   871  			obj.SetAnnotations(tc.Annotations)
   872  			policy, err := k8s.GetManagementConflictPreventionAnnotationValue(&obj)
   873  			if tc.ShouldSucceed != (err == nil) {
   874  				t.Fatalf("expected success to be '%v', instead got error mismatch: %v", tc.ShouldSucceed, err)
   875  			}
   876  			if policy != tc.ExpectedPolicy {
   877  				t.Fatalf("policy mismatch: got '%v', want '%v'", policy, tc.ExpectedPolicy)
   878  			}
   879  		})
   880  	}
   881  }
   882  
   883  func newManagementConflictAnnotations(policy string) map[string]string {
   884  	annotations := make(map[string]string)
   885  	if policy != "" {
   886  		annotations[k8s.ManagementConflictPreventionPolicyFullyQualifiedAnnotation] = policy
   887  	}
   888  	return annotations
   889  }
   890  
   891  func TestMain(m *testing.M) {
   892  	testmain.TestMainForUnitTests(m, &mgr)
   893  }
   894  

View as plain text