1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package webhook
16
17 import (
18 "container/list"
19 "context"
20 "fmt"
21 "net/http"
22 "reflect"
23 "regexp"
24 "strings"
25
26 corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
27 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl"
28 dclextension "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
29 dclcontainer "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension/container"
30 dclmetadata "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
31 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader"
32 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
33 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf"
34 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
35 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
36 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/pathslice"
37 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/typeutil"
38
39 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
40 "github.com/hashicorp/terraform-provider-google-beta/google-beta"
41 "github.com/nasa9084/go-openapi"
42 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
43 "k8s.io/apimachinery/pkg/runtime"
44 "k8s.io/apimachinery/pkg/runtime/serializer"
45 "k8s.io/klog/v2"
46 "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
47 )
48
49 var (
50 scheme = runtime.NewScheme()
51 codecs = serializer.NewCodecFactory(scheme)
52 TFSchemaNotFound = fmt.Errorf("schema does not exist")
53 )
54
55 type immutableFieldsValidatorHandler struct {
56 smLoader *servicemappingloader.ServiceMappingLoader
57 tfResourceMap map[string]*schema.Resource
58 dclSchemaLoader dclschemaloader.DCLSchemaLoader
59 serviceMetadataLoader dclmetadata.ServiceMetadataLoader
60 }
61
62 var (
63 allowedResponse = admission.ValidationResponse(true, "admission controller passed")
64 )
65
66 func NewImmutableFieldsValidatorHandler(smLoader *servicemappingloader.ServiceMappingLoader, dclSchemaLoader dclschemaloader.DCLSchemaLoader, serviceMetadataLoader dclmetadata.ServiceMetadataLoader) *immutableFieldsValidatorHandler {
67 return &immutableFieldsValidatorHandler{
68 smLoader: smLoader,
69 tfResourceMap: google.ResourceMap(),
70 dclSchemaLoader: dclSchemaLoader,
71 serviceMetadataLoader: serviceMetadataLoader,
72 }
73 }
74
75 func (a *immutableFieldsValidatorHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
76 if regexp.MustCompile(ControllerManagerServiceAccountRegex).MatchString(req.AdmissionRequest.UserInfo.Username) {
77 return admission.ValidationResponse(true, "ignore non-user requests")
78 }
79
80
81 deserializer := codecs.UniversalDeserializer()
82 obj := &unstructured.Unstructured{}
83 if _, _, err := deserializer.Decode(req.AdmissionRequest.Object.Raw, nil, obj); err != nil {
84 klog.Error(err)
85 return admission.Errored(http.StatusBadRequest,
86 fmt.Errorf("error decoding object: %v", err))
87 }
88 oldObj := &unstructured.Unstructured{}
89 if _, _, err := deserializer.Decode(req.AdmissionRequest.OldObject.Raw, nil, oldObj); err != nil {
90 klog.Error(err)
91 return admission.Errored(http.StatusBadRequest,
92 fmt.Errorf("error decoding old object: %v", err))
93 }
94
95 spec, ok := obj.Object["spec"].(map[string]interface{})
96 if obj.Object["spec"] != nil && !ok {
97 return admission.Errored(http.StatusBadRequest,
98 fmt.Errorf("the type of spec field is not map[string]interface{}"))
99 }
100 oldSpec, ok := oldObj.Object["spec"].(map[string]interface{})
101 if oldObj.Object["spec"] != nil && !ok {
102 return admission.Errored(http.StatusBadRequest,
103 fmt.Errorf("the type of spec field is not map[string]interface{}"))
104 }
105
106 if isIAMResource(oldObj) {
107 return validateImmutableFieldsForIAMResource(oldObj, oldSpec, spec)
108 }
109
110 if err := validateImmutableStateIntoSpecAnnotation(obj, oldObj); err != nil {
111 return admission.Errored(http.StatusForbidden, err)
112 }
113
114 if dclmetadata.IsDCLBasedResourceKind(obj.GroupVersionKind(), a.serviceMetadataLoader) {
115 return validateImmutableFieldsForDCLBasedResource(obj, oldObj, spec, oldSpec, a.dclSchemaLoader, a.serviceMetadataLoader)
116 }
117 return validateImmutableFieldsForTFBasedResource(obj, oldObj, spec, oldSpec, a.smLoader, a.tfResourceMap)
118 }
119
120 func validateImmutableStateIntoSpecAnnotation(obj, oldObj *unstructured.Unstructured) error {
121 val, found := k8s.GetAnnotation(k8s.StateIntoSpecAnnotation, obj)
122 prevVal, prevFound := k8s.GetAnnotation(k8s.StateIntoSpecAnnotation, oldObj)
123 if found != prevFound || val != prevVal {
124 return fmt.Errorf("annotation %v is immutable", k8s.StateIntoSpecAnnotation)
125 }
126 return nil
127 }
128
129 func validateImmutableFieldsForDCLBasedResource(obj, oldObj *unstructured.Unstructured, spec, oldSpec map[string]interface{}, dclSchemaLoader dclschemaloader.DCLSchemaLoader, serviceMetadataLoader dclmetadata.ServiceMetadataLoader) admission.Response {
130 gvk := obj.GroupVersionKind()
131 schema, err := dclschemaloader.GetDCLSchemaForGVK(gvk, serviceMetadataLoader, dclSchemaLoader)
132 if err != nil {
133 return admission.Errored(http.StatusInternalServerError,
134 fmt.Errorf("error getting the DCL Schema for GroupVersionKind %v: %w", gvk, err))
135 }
136 containers, err := dclcontainer.GetContainersForGVK(gvk, serviceMetadataLoader, dclSchemaLoader)
137 if err != nil {
138 return admission.Errored(http.StatusInternalServerError,
139 fmt.Errorf("error getting containers supported by GroupVersionKind %v: %v", gvk, err))
140 }
141 hierarchicalRefs, err := dcl.GetHierarchicalReferencesForGVK(gvk, serviceMetadataLoader, dclSchemaLoader)
142 if err != nil {
143 return admission.Errored(http.StatusInternalServerError,
144 fmt.Errorf("error getting hierarchical references supported by GroupVersionKind %v: %v", gvk, err))
145 }
146 if err := validateContainerAnnotationsForResource(gvk.Kind, obj.GetAnnotations(), oldObj.GetAnnotations(), containers, hierarchicalRefs); err != nil {
147 return admission.Errored(http.StatusBadRequest,
148 fmt.Errorf("error validating container annotations: %v", err))
149 }
150 if isResourceIDModified(spec, oldSpec) {
151 return admission.Errored(http.StatusForbidden,
152 k8s.NewImmutableFieldsMutationError([]string{k8s.ResourceIDFieldPath}))
153 }
154 res, err := getChangesOnImmutableFields(spec, oldSpec, []string{"spec"}, []string{}, schema, hierarchicalRefs)
155 if err != nil {
156 return admission.Errored(http.StatusInternalServerError,
157 fmt.Errorf("unexpected error: %w", err))
158 }
159 if len(res) != 0 {
160 return admission.Errored(http.StatusForbidden,
161 k8s.NewImmutableFieldsMutationError(res))
162 }
163 return allowedResponse
164 }
165
166 func getChangesOnImmutableFields(spec, oldSpec map[string]interface{}, krmPath, dclPath []string, schema *openapi.Schema, hierarchicalRefs []corekccv1alpha1.HierarchicalReference) ([]string, error) {
167 if schema.Type != "object" {
168 return nil, fmt.Errorf("expect the schame type to be 'object', but got %v", schema.Type)
169 }
170 ret := make([]string, 0)
171 for f, s := range schema.Properties {
172 if s.ReadOnly {
173 continue
174 }
175 isImmutable, err := dclextension.IsImmutableField(s)
176 if err != nil {
177 return nil, fmt.Errorf("error determining if field %v is immutable", f)
178 }
179 if dclextension.IsReferenceField(s) {
180 if !isImmutable {
181 continue
182 }
183
184
185
186 if dcl.IsMultiTypeParentReferenceField(append(dclPath, f)) {
187 for _, h := range hierarchicalRefs {
188 if !reflect.DeepEqual(oldSpec[h.Key], spec[h.Key]) {
189 krmPathToField := pathslice.ToString(append(krmPath, h.Key))
190 ret = append(ret, krmPathToField)
191 }
192 }
193 continue
194 }
195 refField, err := dclextension.GetReferenceFieldName(append(dclPath, f), s)
196 if err != nil {
197 return nil, err
198 }
199 if !reflect.DeepEqual(oldSpec[refField], spec[refField]) {
200 krmPathToField := pathslice.ToString(append(krmPath, refField))
201 ret = append(ret, krmPathToField)
202 }
203 continue
204 }
205 krmPathToField := pathslice.ToString(append(krmPath, f))
206 oldVal := oldSpec[f]
207 newVal := spec[f]
208 if oldVal == nil && newVal == nil {
209 continue
210 }
211 if isImmutable && (oldVal == nil || newVal == nil) {
212 ret = append(ret, krmPathToField)
213 continue
214 }
215 switch s.Type {
216 case "object":
217 var v1 map[string]interface{}
218 var v2 map[string]interface{}
219 if oldVal != nil {
220 v1 = oldVal.(map[string]interface{})
221 }
222 if newVal != nil {
223 v2 = newVal.(map[string]interface{})
224 }
225
226 if s.AdditionalProperties != nil {
227
228 if isImmutable {
229 if !reflect.DeepEqual(v1, v2) {
230 ret = append(ret, krmPathToField)
231 }
232 continue
233 }
234 if typeutil.IsPrimitiveType(s.AdditionalProperties.Type) {
235 continue
236 }
237
238
239
240
241
242 continue
243 }
244
245 nestedFields, err := getChangesOnImmutableFields(v1, v2, append(krmPath, f), append(dclPath, f), s, hierarchicalRefs)
246 if err != nil {
247 return nil, err
248 }
249 ret = append(ret, nestedFields...)
250 case "array":
251 if typeutil.IsPrimitiveType(s.Items.Type) {
252 if isImmutable && !reflect.DeepEqual(oldVal, newVal) {
253 ret = append(ret, krmPathToField)
254 }
255 continue
256 }
257
258
259 case "string", "boolean", "number", "integer":
260 if isImmutable && !reflect.DeepEqual(oldSpec[f], spec[f]) {
261 ret = append(ret, krmPathToField)
262 }
263 default:
264 return nil, fmt.Errorf("unknown schema type %v", s.Type)
265 }
266 }
267 return ret, nil
268 }
269
270 func getQualifiedFieldName(prefix string, fieldName string) string {
271 qualifiedName := fieldName
272 if prefix != "" {
273 qualifiedName = prefix + "." + fieldName
274 }
275 return qualifiedName
276 }
277
278 func validateImmutableFieldsForTFBasedResource(obj, oldObj *unstructured.Unstructured, spec, oldSpec map[string]interface{}, smLoader *servicemappingloader.ServiceMappingLoader, tfResourceMap map[string]*schema.Resource) admission.Response {
279 rc, err := smLoader.GetResourceConfig(obj)
280 if err != nil {
281 return admission.Errored(http.StatusBadRequest,
282 fmt.Errorf("couldn't get ResourceConfig for kind %v: %v", obj.GetKind(), err))
283 }
284
285 if err := validateContainerAnnotationsForResource(obj.GetKind(), obj.GetAnnotations(), oldObj.GetAnnotations(), rc.Containers, rc.HierarchicalReferences); err != nil {
286 return admission.Errored(http.StatusBadRequest,
287 fmt.Errorf("error validating container annotations: %v", err))
288 }
289
290 r, ok := tfResourceMap[rc.Name]
291 if !ok {
292 return admission.Errored(http.StatusInternalServerError,
293 fmt.Errorf("unknown resource %v", rc.Name))
294 }
295
296 if findChangesOnImmutableResourceIDField(spec, oldSpec, rc) {
297 return admission.Errored(http.StatusForbidden,
298 k8s.NewImmutableFieldsMutationError([]string{k8s.ResourceIDFieldPath}))
299 }
300
301 if findChangesOnImmutableLocationField(spec, oldSpec, rc) {
302 return admission.Errored(http.StatusForbidden,
303 k8s.NewImmutableFieldsMutationError([]string{"spec.location"}))
304 }
305
306 fields := list.New()
307 compareAndFindChangesOnImmutableFields(spec, oldSpec, r.Schema, "", rc, getIgnoredFields(rc), fields)
308 if fields.Len() != 0 {
309 res := make([]string, 0)
310 for e := fields.Front(); e != nil; e = e.Next() {
311 res = append(res, constructCamelCasePath(e.Value.(string)))
312 }
313 return admission.Errored(http.StatusBadRequest,
314 k8s.NewImmutableFieldsMutationError(res))
315 }
316
317 return allowedResponse
318 }
319
320 func validateContainerAnnotationsForResource(kind string, annotations, oldAnnotations map[string]string, containers []corekccv1alpha1.Container, hierarchicalRefs []corekccv1alpha1.HierarchicalReference) error {
321
322
323 if len(hierarchicalRefs) == 0 {
324 return validateContainerAnnotations(kind, annotations, oldAnnotations, containers)
325 }
326 return validateDeprecatedContainerAnnotations(annotations, oldAnnotations, containers, hierarchicalRefs)
327 }
328
329 func validateContainerAnnotations(kind string, annotations, oldAnnotations map[string]string, containers []corekccv1alpha1.Container) error {
330 for _, c := range containers {
331 a := k8s.GetAnnotationForContainerType(c.Type)
332
333
334 if oldAnnotations[a] == annotations[a] {
335 continue
336 }
337
338
339
340
341 if kind != "Project" && kind != "Folder" {
342 return fmt.Errorf("cannot make changes to container annotation %v", a)
343 }
344
345
346 for _, otherC := range containers {
347 if c == otherC {
348 continue
349 }
350 otherA := k8s.GetAnnotationForContainerType(otherC.Type)
351 _, ok := oldAnnotations[a]
352 _, otherOk := annotations[otherA]
353 if ok && otherOk {
354 return fmt.Errorf("cannot change from container annotation %v to container annotation %v", a, otherA)
355 }
356 }
357 }
358 return nil
359 }
360
361 func validateDeprecatedContainerAnnotations(annotations, oldAnnotations map[string]string, containers []corekccv1alpha1.Container, hierarchicalRefs []corekccv1alpha1.HierarchicalReference) error {
362 for _, c := range containers {
363 a := k8s.GetAnnotationForContainerType(c.Type)
364
365
366 if oldAnnotations[a] == annotations[a] {
367 continue
368 }
369
370
371
372
373 if annotations[a] == "" {
374 continue
375 }
376
377
378 possibleFields := k8s.HierarchicalReferencesToFields(hierarchicalRefs)
379 return fmt.Errorf("cannot add/change container annotation %v as it is no longer supported by the resource; set one of [%v] instead.", a, strings.Join(possibleFields, ", "))
380 }
381 return nil
382 }
383
384 func validateImmutableFieldsForIAMResource(oldObj *unstructured.Unstructured, oldSpec, newSpec map[string]interface{}) admission.Response {
385 if isIAMPolicy(oldObj) {
386 return handleIAMPolicy(oldSpec, newSpec)
387 }
388 if isIAMPartialPolicy(oldObj) {
389 return handleIAMPartialPolicy(oldSpec, newSpec)
390 }
391 if isIAMPolicyMember(oldObj) {
392 return handleIAMPolicyMember(oldSpec, newSpec)
393 }
394 if isIAMAuditConfig(oldObj) {
395 return handleIAMAuditConfig(oldSpec, newSpec)
396 }
397 return admission.ValidationResponse(false, fmt.Sprintf("unknown IAM resource type: %v", oldObj.GroupVersionKind()))
398 }
399
400 func handleIAMPolicy(oldSpec, newSpec map[string]interface{}) admission.Response {
401 if isIAMResourceReferenceModified(oldSpec, newSpec) {
402 msg := fmt.Sprintf("the IAMPolicy's spec.resourceRef is immutable")
403 return admission.ValidationResponse(false, msg)
404 }
405 return allowedResponse
406 }
407
408 func handleIAMPartialPolicy(oldSpec, newSpec map[string]interface{}) admission.Response {
409 if isIAMResourceReferenceModified(oldSpec, newSpec) {
410 msg := fmt.Sprintf("the IAMPartialPolicy's spec.resourceRef is immutable")
411 return admission.ValidationResponse(false, msg)
412 }
413 return allowedResponse
414 }
415
416 func handleIAMPolicyMember(oldSpec, newSpec map[string]interface{}) admission.Response {
417 if isIAMSpecModified(oldSpec, newSpec) {
418 msg := fmt.Sprintf("the IAMPolicyMember's spec is immutable")
419 return admission.ValidationResponse(false, msg)
420 }
421 return allowedResponse
422 }
423
424 func handleIAMAuditConfig(oldSpec, newSpec map[string]interface{}) admission.Response {
425 if isIAMResourceReferenceModified(oldSpec, newSpec) {
426 msg := fmt.Sprintf("the IAMAuditConfig's spec.resourceRef is immutable")
427 return admission.ValidationResponse(false, msg)
428 }
429 if isIAMAuditConfigServiceModified(oldSpec, newSpec) {
430 msg := fmt.Sprintf("the IAMAuditConfig's spec.service is immutable")
431 return admission.ValidationResponse(false, msg)
432 }
433 return allowedResponse
434 }
435
436 func findChangesOnImmutableResourceIDField(spec, oldSpec map[string]interface{}, rc *corekccv1alpha1.ResourceConfig) bool {
437 if rc.ResourceID.TargetField == "" {
438 return false
439 }
440
441 return isResourceIDModified(spec, oldSpec)
442 }
443
444 func isResourceIDModified(spec, oldSpec map[string]interface{}) bool {
445 return !reflect.DeepEqual(spec[k8s.ResourceIDFieldName], oldSpec[k8s.ResourceIDFieldName])
446 }
447
448 func findChangesOnImmutableLocationField(obj map[string]interface{}, oldObj map[string]interface{}, rc *corekccv1alpha1.ResourceConfig) bool {
449 if rc.Locationality == "" {
450 return false
451 }
452
453 return !reflect.DeepEqual(obj["location"], oldObj["location"])
454 }
455
456
457 func compareAndFindChangesOnImmutableFields(obj map[string]interface{}, oldObj map[string]interface{}, schemaMap map[string]*schema.Schema, prefix string, resourceConfig *corekccv1alpha1.ResourceConfig, ignoredFields map[string]bool, fields *list.List) {
458 for k, s := range schemaMap {
459 qualifiedName := getQualifiedFieldName(prefix, k)
460 if ignoredFields[qualifiedName] {
461 continue
462 }
463
464 if ok, refConfig := krmtotf.IsReferenceField(qualifiedName, resourceConfig); ok {
465 if !s.ForceNew {
466 continue
467 }
468 modified, refKey := isReferenceValRawModified(obj, oldObj, refConfig)
469 if modified {
470 refKey = getQualifiedFieldName(prefix, refKey)
471 fields.PushBack(refKey)
472 }
473 continue
474 }
475
476 camelCaseKey := text.SnakeCaseToLowerCamelCase(k)
477 v1 := obj[camelCaseKey]
478 v2 := oldObj[camelCaseKey]
479 if v1 == nil && v2 == nil {
480 continue
481 }
482 if (v1 == nil || v2 == nil) && s.ForceNew {
483 fields.PushBack(qualifiedName)
484 continue
485 }
486
487 switch s.Type {
488
489
490 case schema.TypeBool, schema.TypeFloat, schema.TypeString, schema.TypeInt, schema.TypeMap:
491 if s.ForceNew && !reflect.DeepEqual(v1, v2) {
492 fields.PushBack(qualifiedName)
493 }
494 case schema.TypeList, schema.TypeSet:
495 switch s.Elem.(type) {
496 case *schema.Schema:
497
498 if s.ForceNew && !reflect.DeepEqual(v1, v2) {
499 fields.PushBack(qualifiedName)
500 }
501 case *schema.Resource:
502 if s.MaxItems == 1 {
503
504 tfObjSchemaMap := s.Elem.(*schema.Resource).Schema
505 var o1 map[string]interface{}
506 var o2 map[string]interface{}
507 if v1 != nil {
508 o1 = v1.(map[string]interface{})
509 }
510 if v2 != nil {
511 o2 = v2.(map[string]interface{})
512 }
513 compareAndFindChangesOnImmutableFields(o1, o2, tfObjSchemaMap, qualifiedName, resourceConfig, ignoredFields, fields)
514 } else {
515
516
517
518
519 }
520 }
521 }
522 }
523 }
524
525 func isReferenceValRawModified(obj map[string]interface{}, oldObj map[string]interface{}, refConfig *corekccv1alpha1.ReferenceConfig) (bool, string) {
526
527 referenceFieldKey := krmtotf.GetKeyForReferenceField(refConfig)
528 return !reflect.DeepEqual(obj[referenceFieldKey], oldObj[referenceFieldKey]), referenceFieldKey
529 }
530
531 func constructCamelCasePath(path string) string {
532 segs := make([]string, 0)
533 for _, f := range strings.Split(path, ".") {
534 segs = append(segs, text.SnakeCaseToLowerCamelCase(f))
535 }
536 return strings.Join(segs, ".")
537 }
538
539 func getIgnoredFields(rc *corekccv1alpha1.ResourceConfig) map[string]bool {
540 ignoredFields := make(map[string]bool)
541 for _, f := range rc.IgnoredFields {
542 ignoredFields[f] = true
543 }
544
545
546
547 ignoredFields[rc.MetadataMapping.Name] = true
548 ignoredFields[rc.MetadataMapping.Labels] = true
549 return ignoredFields
550 }
551
View as plain text