...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/kcclite/templating.go

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

     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 kcclite
    16  
    17  import (
    18  	"fmt"
    19  	"path"
    20  	"regexp"
    21  	"strings"
    22  
    23  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl"
    25  	dclextension "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
    26  	dclmetadata "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader"
    28  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    29  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf"
    30  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    31  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    32  
    33  	"github.com/nasa9084/go-openapi"
    34  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  )
    37  
    38  var fieldRegex = regexp.MustCompile("{{([0-9A-Za-z]+)}}")
    39  
    40  func CanonicalizeReferencedResourceName(name string, nameValueTemplate string, refResource *k8s.Resource,
    41  	smLoader dclmetadata.ServiceMetadataLoader, schemaLoader dclschemaloader.DCLSchemaLoader,
    42  	serviceMappingLoader *servicemappingloader.ServiceMappingLoader, kubeClient client.Client) (string, error) {
    43  	ret := strings.ReplaceAll(nameValueTemplate, "{{name}}", name)
    44  	var resolutionError error
    45  	resolveFunc := func(s string) string {
    46  		field := fieldRegex.FindStringSubmatch(s)[1]
    47  		if dcl.IsParentReferenceField([]string{field}) {
    48  			val, err := resolveParentReferenceFieldValue(field, refResource, smLoader, schemaLoader, serviceMappingLoader, kubeClient)
    49  			if err != nil {
    50  				resolutionError = decorateErrorForResolvingReferenceField(err, field, refResource)
    51  				return ""
    52  			}
    53  			return val
    54  		}
    55  		val, exists, err := unstructured.NestedString(refResource.Spec, field)
    56  		if err != nil {
    57  			resolutionError = fmt.Errorf("error getting value for DCL field %v in spec of referenced resource %v with GroupVersionKind %v: %w",
    58  				field, k8s.GetNamespacedName(refResource), refResource.GroupVersionKind(), err)
    59  			return ""
    60  		}
    61  		if exists {
    62  			return val
    63  		}
    64  		// Value not found in spec, so check status.
    65  		val, exists, err = unstructured.NestedString(refResource.Status, field)
    66  		if err != nil {
    67  			resolutionError = fmt.Errorf("error getting value for DCL field %v in status of referenced resource %v with GroupVersionKind %v: %w",
    68  				field, k8s.GetNamespacedName(refResource), refResource.GroupVersionKind(), err)
    69  			return ""
    70  		}
    71  		if exists {
    72  			return val
    73  		}
    74  		resolutionError = fmt.Errorf("no value found for DCL field %v in referenced resource %v with GroupVersionKind %v",
    75  			field, k8s.GetNamespacedName(refResource), refResource.GroupVersionKind())
    76  		return ""
    77  	}
    78  	return fieldRegex.ReplaceAllStringFunc(ret, resolveFunc), resolutionError
    79  }
    80  
    81  func resolveParentReferenceFieldValue(field string, resource *k8s.Resource, smLoader dclmetadata.ServiceMetadataLoader,
    82  	schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader, kubeClient client.Client) (string, error) {
    83  	if dclmetadata.IsDCLBasedResourceKind(resource.GroupVersionKind(), smLoader) {
    84  		return resolveParentReferenceFieldValueForDCLResource(field, resource, smLoader, schemaLoader, kubeClient)
    85  	}
    86  	// If the resource is not a DCL-based resource, then assume it is TF-based.
    87  	return resolveParentReferenceFieldValueForTFResource(field, resource, serviceMappingLoader, kubeClient)
    88  }
    89  
    90  func resolveParentReferenceFieldValueForDCLResource(field string, resource *k8s.Resource, smLoader dclmetadata.ServiceMetadataLoader,
    91  	schemaLoader dclschemaloader.DCLSchemaLoader, kubeClient client.Client) (string, error) {
    92  	r, found := smLoader.GetResourceWithGVK(resource.GroupVersionKind())
    93  	if !found {
    94  		return "", fmt.Errorf("ServiceMetadata for resource with GroupVersionKind %v not found", resource.GroupVersionKind())
    95  	}
    96  	if !r.Releasable {
    97  		return "", fmt.Errorf("expected resource with GroupVersionKind %v to be supported via DCL, but it is not", resource.GroupVersionKind())
    98  	}
    99  
   100  	// TODO(b/186159460): Delete this if-block once all DCL-based resources
   101  	// support hierarchical references.
   102  	if !r.SupportsHierarchicalReferences {
   103  		return resolveParentFieldFromContainerAnnotation(field, resource)
   104  	}
   105  
   106  	schema, err := dclschemaloader.GetDCLSchemaForGVK(resource.GroupVersionKind(), smLoader, schemaLoader)
   107  	if err != nil {
   108  		return "", fmt.Errorf("error getting DCL schema for GroupVersionKind %v: %w", resource.GroupVersionKind(), err)
   109  	}
   110  	fieldSchema, ok := schema.Properties[field]
   111  	if !ok {
   112  		return "", fmt.Errorf("could not find schema for DCL field '%v' for GroupVersionKind %v", field, resource.GroupVersionKind())
   113  	}
   114  	if dcl.IsMultiTypeParentReferenceField([]string{field}) {
   115  		return resolveMultiTypeParentReferenceFieldValueForDCLResource(field, fieldSchema, resource, smLoader, kubeClient)
   116  	}
   117  	return resolveSingleTypeParentReferenceFieldValueForDCLResource(field, fieldSchema, resource, smLoader, kubeClient)
   118  }
   119  
   120  func resolveMultiTypeParentReferenceFieldValueForDCLResource(field string, schema *openapi.Schema, resource *k8s.Resource,
   121  	smLoader dclmetadata.ServiceMetadataLoader, kubeClient client.Client) (string, error) {
   122  	rawVal, tc, err := dcl.GetHierarchicalRefFromConfigForMultiParentResource(resource.Spec, schema, smLoader)
   123  	if err != nil {
   124  		return "", fmt.Errorf("error getting hierarchical reference from config for multi-parent resource: %w", err)
   125  	}
   126  	if rawVal == nil {
   127  		return "", fmt.Errorf("no hierarchical reference found for multi-parent resource")
   128  	}
   129  	refField := tc.Key
   130  	refObj, ok := rawVal.(map[string]interface{})
   131  	if !ok {
   132  		return "", fmt.Errorf("expected the value to be map[string]interface{} for reference field %v but was actually %T", refField, rawVal)
   133  	}
   134  	val, err := resolveReferenceObject(refObj, tc, resource.GetNamespace(), kubeClient)
   135  	if err != nil {
   136  		return "", err
   137  	}
   138  	name := path.Base(val) // In case the resolved value is a path, use the last element of the path only.
   139  	return fmt.Sprintf("%v%v", dcl.ParentPrefixForKind(tc.GVK.Kind), name), nil
   140  }
   141  
   142  func resolveSingleTypeParentReferenceFieldValueForDCLResource(field string, schema *openapi.Schema, resource *k8s.Resource,
   143  	smLoader dclmetadata.ServiceMetadataLoader, kubeClient client.Client) (string, error) {
   144  	refField, err := dclextension.GetReferenceFieldName([]string{field}, schema)
   145  	if err != nil {
   146  		return "", fmt.Errorf("error getting the reference field name for DCL field '%v': %w", field, err)
   147  	}
   148  	rawVal, ok := resource.Spec[refField]
   149  	if !ok || rawVal == nil {
   150  		return "", fmt.Errorf("no hierarchical reference found for single-parent resource")
   151  	}
   152  	refObj, ok := rawVal.(map[string]interface{})
   153  	if !ok {
   154  		return "", fmt.Errorf("expected the value to be map[string]interface{} for reference field %v but was actually %T", refField, rawVal)
   155  	}
   156  	tcs, err := dcl.GetReferenceTypeConfigs(schema, smLoader)
   157  	if err != nil {
   158  		return "", fmt.Errorf("error getting type configs for DCL field '%v': %w", field, err)
   159  	}
   160  	if len(tcs) > 1 {
   161  		return "", fmt.Errorf("unexpectedly got more than one type config for DCL field '%v' which is supposed to be a single-type parent reference field", field)
   162  	}
   163  	val, err := resolveReferenceObject(refObj, &tcs[0], resource.GetNamespace(), kubeClient)
   164  	if err != nil {
   165  		return "", err
   166  	}
   167  	name := path.Base(val) // In case the resolved value is a path, use the last element of the path only.
   168  	return name, nil
   169  }
   170  
   171  func resolveParentReferenceFieldValueForTFResource(field string, resource *k8s.Resource,
   172  	serviceMappingLoader *servicemappingloader.ServiceMappingLoader, kubeClient client.Client) (string, error) {
   173  	u, err := resource.MarshalAsUnstructured()
   174  	if err != nil {
   175  		return "", fmt.Errorf("error marshalling resource to unstructured: %w", err)
   176  	}
   177  	rc, err := serviceMappingLoader.GetResourceConfig(u)
   178  	if err != nil {
   179  		return "", fmt.Errorf("error getting resource config for resource: %w", err)
   180  	}
   181  	krmResource := &krmtotf.Resource{
   182  		Resource:       *resource,
   183  		ResourceConfig: *rc,
   184  	}
   185  	// TODO(b/193177782): Delete this if-block once all TF-based resources
   186  	// support hierarchical references.
   187  	if !krmtotf.SupportsHierarchicalReferences(rc) {
   188  		return resolveParentFieldFromContainerAnnotation(field, resource)
   189  	}
   190  	ref, hierarchicalRef, err := k8s.GetHierarchicalReference(resource, rc.HierarchicalReferences)
   191  	if err != nil {
   192  		return "", fmt.Errorf("error getting hierarchical reference: %w", err)
   193  	}
   194  	refConfig, err := krmtotf.GetReferenceConfigForHierarchicalReference(hierarchicalRef, rc)
   195  	if err != nil {
   196  		return "", fmt.Errorf("error getting reference config for hierarchical reference: %w", err)
   197  	}
   198  	var refObj map[string]interface{}
   199  	if err := util.Marshal(ref, &refObj); err != nil {
   200  		return "", fmt.Errorf("error marshalling hierarchical reference to map[string]interface{}: %w", err)
   201  	}
   202  	rawVal, err := krmtotf.ResolveReferenceObject(refObj, *refConfig, krmResource, kubeClient, serviceMappingLoader)
   203  	if err != nil {
   204  		return "", fmt.Errorf("error resolving resource reference object representing resource's hierarchical reference: %w", err)
   205  	}
   206  	val, ok := rawVal.(string)
   207  	if !ok {
   208  		return "", fmt.Errorf("expected the resolved value of the resource reference object to be string, but was actually %T", rawVal)
   209  	}
   210  	name := path.Base(val) // In case the resolved value is a path, use the last element of the path only.
   211  	if dcl.IsMultiTypeParentReferenceField([]string{field}) {
   212  		return fmt.Sprintf("%v%v", dcl.ParentPrefixForKind(refConfig.GVK.Kind), name), nil
   213  	}
   214  	return name, nil
   215  }
   216  
   217  func resolveParentFieldFromContainerAnnotation(field string, resource *k8s.Resource) (string, error) {
   218  	annotation := k8s.GetAnnotationForContainerType(corekccv1alpha1.ContainerType(field))
   219  	val, ok := k8s.GetAnnotation(annotation, resource)
   220  	if !ok || val == "" {
   221  		return "", fmt.Errorf("no value found for annotation %v in resource %v",
   222  			annotation, k8s.GetNamespacedName(resource))
   223  	}
   224  	return val, nil
   225  }
   226  
   227  func decorateErrorForResolvingReferenceField(err error, field string, refResource *k8s.Resource) error {
   228  	if unwrappedErr, ok := k8s.AsReferenceNotFoundError(err); ok {
   229  		return k8s.NewTransitiveDependencyNotFoundError(unwrappedErr.RefResourceGVK, unwrappedErr.RefResource)
   230  	}
   231  	if unwrappedErr, ok := k8s.AsReferenceNotReadyError(err); ok {
   232  		return k8s.NewTransitiveDependencyNotReadyError(unwrappedErr.RefResourceGVK, unwrappedErr.RefResource)
   233  	}
   234  	return fmt.Errorf("error getting value for DCL field %v in referenced resource %v with GroupVersionKind %v: %w",
   235  		field, k8s.GetNamespacedName(refResource), refResource.GroupVersionKind(), err)
   236  }
   237  

View as plain text