...

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

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

     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 krmtotf
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    22  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    27  
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"sigs.k8s.io/controller-runtime/pkg/client"
    30  )
    31  
    32  func GetReferencedResource(r *Resource, typeConfig corekccv1alpha1.TypeConfig,
    33  	resourceRef *v1alpha1.ResourceReference, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (rsrc *Resource, err error) {
    34  	if resourceRef.External != "" {
    35  		return nil, fmt.Errorf("reference is external: %v", resourceRef.External)
    36  	}
    37  	u, err := k8s.GetReferencedResourceAsUnstruct(resourceRef, typeConfig.GVK, r.GetNamespace(), kubeClient)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	rsrc = &Resource{}
    42  	if err := util.Marshal(u, rsrc); err != nil {
    43  		return nil, fmt.Errorf("error parsing %v", u.GetName())
    44  	}
    45  	if typeConfig.DCLBasedResource {
    46  		return rsrc, nil
    47  	}
    48  
    49  	rc, err := smLoader.GetResourceConfig(u)
    50  	if err != nil {
    51  		return nil, fmt.Errorf("error getting ResourceConfig for referenced resource %v: %w", r.GetName(), err)
    52  	}
    53  	rsrc.ResourceConfig = *rc
    54  	return rsrc, nil
    55  }
    56  
    57  func handleResourceReference(config map[string]interface{}, refConfig v1alpha1.ReferenceConfig, r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) error {
    58  	path := strings.Split(refConfig.TFField, ".")
    59  	return ResolveResourceReference(path, config, refConfig, r, c, smLoader)
    60  }
    61  
    62  func ResolveResourceReference(path []string, obj interface{}, refConfig v1alpha1.ReferenceConfig,
    63  	r *Resource, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) error {
    64  	if obj == nil {
    65  		return nil
    66  	}
    67  
    68  	// If the object is a list, resolve the reference for each list item
    69  	var config map[string]interface{}
    70  	switch objAsType := obj.(type) {
    71  	case []interface{}:
    72  		for _, item := range objAsType {
    73  			if err := ResolveResourceReference(path, item, refConfig, r, kubeClient, smLoader); err != nil {
    74  				return err
    75  			}
    76  		}
    77  		return nil
    78  	case map[string]interface{}:
    79  		config = objAsType
    80  	default:
    81  		return fmt.Errorf("error resolving reference: cannot iterate through type that is not object or list of objects")
    82  	}
    83  
    84  	field := text.SnakeCaseToLowerCamelCase(path[0])
    85  
    86  	// Iterate down the chain of fields
    87  	if len(path) > 1 {
    88  		return ResolveResourceReference(path[1:], config[field], refConfig, r, kubeClient, smLoader)
    89  	}
    90  
    91  	// Base case. We have found the object that holds the field that is the reference. Resolve its value.
    92  	key := field
    93  	if refConfig.Key != "" {
    94  		key = refConfig.Key
    95  	}
    96  	ref := config[key]
    97  	if ref == nil {
    98  		return nil
    99  	}
   100  
   101  	var resolvedVal interface{}
   102  	switch refAsType := ref.(type) {
   103  	case map[string]interface{}:
   104  		var err error
   105  		resolvedVal, err = ResolveReferenceObject(refAsType, refConfig, r, kubeClient, smLoader)
   106  		if err != nil {
   107  			return err
   108  		}
   109  	case []interface{}:
   110  		resolvedList := make([]interface{}, 0)
   111  		for _, item := range refAsType {
   112  			itemAsMap, ok := item.(map[string]interface{})
   113  			if !ok {
   114  				return fmt.Errorf("expected reference %v to be object but was not", key)
   115  			}
   116  			resolvedVal, err := ResolveReferenceObject(itemAsMap, refConfig, r, kubeClient, smLoader)
   117  			if err != nil {
   118  				return err
   119  			}
   120  			resolvedList = append(resolvedList, resolvedVal)
   121  		}
   122  		resolvedVal = resolvedList
   123  	default:
   124  		return fmt.Errorf("unexpected type for reference field %v", path[0])
   125  	}
   126  	config[field] = resolvedVal
   127  	if field != key {
   128  		delete(config, key)
   129  	}
   130  	return nil
   131  }
   132  
   133  func ResolveReferenceObject(resourceRefValRaw map[string]interface{},
   134  	refConfig corekccv1alpha1.ReferenceConfig, r *Resource, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (interface{}, error) {
   135  	typeConfig := refConfig.TypeConfig
   136  	if len(refConfig.Types) > 0 {
   137  		var (
   138  			nestedRefValRaw interface{}
   139  			err             error
   140  			ok              bool
   141  			found           bool
   142  		)
   143  		for _, typeConfig = range refConfig.Types {
   144  			nestedRefValRaw, found, err = unstructured.NestedFieldNoCopy(resourceRefValRaw, typeConfig.Key)
   145  			if err != nil {
   146  				return nil, err
   147  			}
   148  			if found {
   149  				if typeConfig.JSONSchemaType != "" {
   150  					// This is not actually a reference, but an explicit value that should be used.
   151  					return resolveValueTemplateFromInterface(typeConfig.ValueTemplate, nestedRefValRaw, r, kubeClient, smLoader)
   152  				}
   153  				resourceRefValRaw, ok = nestedRefValRaw.(map[string]interface{})
   154  				if !ok {
   155  					return nil, fmt.Errorf("expected reference to be object")
   156  				}
   157  				break
   158  			}
   159  		}
   160  		if !found {
   161  			return nil, nil
   162  		}
   163  	}
   164  	resourceRef := &v1alpha1.ResourceReference{}
   165  	if err := util.Marshal(resourceRefValRaw, resourceRef); err != nil {
   166  		return nil, fmt.Errorf("field %v is a wrong format", typeConfig.Key)
   167  	}
   168  
   169  	// Resource references usually point to K8s resources except when the
   170  	// resource reference is an external resource reference. In the case of an
   171  	// external resource reference, the 'external' field is used to specify a
   172  	// string identifier for the referenced resource.
   173  	if resourceRef.External != "" {
   174  		return resourceRef.External, nil
   175  	}
   176  
   177  	// Deletions do some limited config expansion in order to preset immutable fields before the read
   178  	// from the underlying API. Do a best-effort setting of these fields, as any unresolvable references
   179  	// (due to, say, the reference having been deleted before) will be learned as a result of the read.
   180  	deleting := k8s.IsDeleted(&r.ObjectMeta)
   181  
   182  	refResource, err := GetReferencedResource(r, typeConfig, resourceRef, kubeClient, smLoader)
   183  	if err != nil {
   184  		if k8s.IsReferenceNotFoundError(err) {
   185  			if deleting {
   186  				return nil, nil
   187  			}
   188  			return nil, err
   189  		}
   190  		return nil, fmt.Errorf("error getting referenced resource from API server: %v", err)
   191  	}
   192  
   193  	if !deleting && !k8s.IsResourceReady(&refResource.Resource) {
   194  		return nil, k8s.NewReferenceNotReadyErrorForResource(&refResource.Resource)
   195  	}
   196  
   197  	resolvedVal, err := resolveTargetFieldValue(refResource, typeConfig)
   198  	if err != nil {
   199  		return nil, fmt.Errorf("error resolving value of target field of "+
   200  			"referenced resource %v %v: %v", refResource.GroupVersionKind(),
   201  			refResource.GetNamespacedName(), err)
   202  	}
   203  
   204  	if deleting && typeConfig.TargetField == "" && typeConfig.ValueTemplate == "" {
   205  		return resolvedVal, nil
   206  	}
   207  
   208  	return resolveValueTemplateFromInterface(typeConfig.ValueTemplate, resolvedVal, refResource, kubeClient, smLoader)
   209  }
   210  
   211  func resolveTargetFieldValue(r *Resource, tc corekccv1alpha1.TypeConfig) (interface{}, error) {
   212  	key := text.SnakeCaseToLowerCamelCase(tc.TargetField)
   213  	switch key {
   214  	case "":
   215  		return resolveDefaultTargetFieldValue(r, tc)
   216  	default:
   217  		if val, exists, _ := unstructured.NestedString(r.Spec, strings.Split(key, ".")...); exists {
   218  			return val, nil
   219  		}
   220  		if val, exists, _ := unstructured.NestedString(r.Status, strings.Split(key, ".")...); exists {
   221  			return val, nil
   222  		}
   223  		// For now, we do not support recursive target field resolution (i.e. targeting a field in
   224  		// the referenced resource that itself is a reference to a third resource, which would require
   225  		// its own target field resolution).
   226  		return nil, fmt.Errorf("referenced resource's target field %v is unsupported", tc.TargetField)
   227  	}
   228  }
   229  
   230  func resolveDefaultTargetFieldValue(r *Resource, tc corekccv1alpha1.TypeConfig) (interface{}, error) {
   231  	if !tc.DCLBasedResource && !SupportsResourceIDField(&r.ResourceConfig) {
   232  		return r.GetName(), nil
   233  	}
   234  
   235  	id, err := r.GetResourceID()
   236  	if err != nil {
   237  		return "", err
   238  	}
   239  
   240  	return id, nil
   241  }
   242  
   243  func IsReferenceField(qualifiedName string, rc *corekccv1alpha1.ResourceConfig) (bool, *corekccv1alpha1.ReferenceConfig) {
   244  	for _, refConfig := range rc.ResourceReferences {
   245  		if qualifiedName == refConfig.TFField {
   246  			return true, &refConfig
   247  		}
   248  	}
   249  	return false, nil
   250  }
   251  
   252  func containsReferenceField(qualifiedName string, rc *corekccv1alpha1.ResourceConfig) bool {
   253  	for _, refConfig := range rc.ResourceReferences {
   254  		if strings.HasPrefix(refConfig.TFField, qualifiedName) {
   255  			return true
   256  		}
   257  	}
   258  	return false
   259  }
   260  
   261  func GetKeyForReferenceField(refConfig *corekccv1alpha1.ReferenceConfig) string {
   262  	if refConfig.Key != "" {
   263  		return refConfig.Key
   264  	}
   265  	parts := strings.Split(refConfig.TFField, ".")
   266  	containerField := text.SnakeCaseToLowerCamelCase(parts[len(parts)-1])
   267  	return containerField
   268  }
   269  
   270  func getPathToReferenceKey(refConfig *corekccv1alpha1.ReferenceConfig) []string {
   271  	fieldCamelCase := text.SnakeCaseToLowerCamelCase(refConfig.TFField)
   272  	path := strings.Split(fieldCamelCase, ".")
   273  	if refConfig.Key != "" {
   274  		path[len(path)-1] = refConfig.Key
   275  	}
   276  	return path
   277  }
   278  
   279  func IsHierarchicalReference(ref corekccv1alpha1.ReferenceConfig, hierarchicalRefs []corekccv1alpha1.HierarchicalReference) bool {
   280  	// Hierarchical references can only be at the root level, but this
   281  	// reference is not a root-level field.
   282  	if strings.Contains(ref.TFField, ".") {
   283  		return false
   284  	}
   285  	key := GetKeyForReferenceField(&ref)
   286  	for _, h := range hierarchicalRefs {
   287  		if h.Key == key {
   288  			return true
   289  		}
   290  	}
   291  	return false
   292  }
   293  
   294  func IsRequiredParentReference(ref corekccv1alpha1.ReferenceConfig, resource *Resource) bool {
   295  	if ref.Parent {
   296  		return true
   297  	}
   298  	if !IsHierarchicalReference(ref, resource.ResourceConfig.HierarchicalReferences) {
   299  		return false
   300  	}
   301  	// For projects and folders, we shouldn't treat their hierarchical references as parent references
   302  	// because their URLs only contain their own project_id or folder_id, i.e. folders/{folder_id} and projects/{project_id}
   303  	if resource.Kind == "Project" || resource.Kind == "Folder" {
   304  		return false
   305  	}
   306  	return true
   307  }
   308  
   309  func GetReferenceConfigForHierarchicalReference(hierarchicalRef corekccv1alpha1.HierarchicalReference, rc *corekccv1alpha1.ResourceConfig) (*corekccv1alpha1.ReferenceConfig, error) {
   310  	for _, ref := range rc.ResourceReferences {
   311  		// Hierarchical references can only be at the root level, but this
   312  		// reference is not a root-level field.
   313  		if strings.Contains(ref.TFField, ".") {
   314  			continue
   315  		}
   316  		key := GetKeyForReferenceField(&ref)
   317  		if key == hierarchicalRef.Key {
   318  			return &ref, nil
   319  		}
   320  	}
   321  	return nil, fmt.Errorf("no reference config found for hierarchical reference field %v", hierarchicalRef.Key)
   322  }
   323  

View as plain text