...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf/templating.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  	"regexp"
    20  	"strings"
    21  
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcp"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    24  
    25  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    28  
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  )
    32  
    33  var fieldRegex = regexp.MustCompile("{{([0-9A-Za-z_.?]+)}}")
    34  
    35  func ResolveValueTemplate(template string, val string, r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (string, error) {
    36  	if template == "" {
    37  		// An empty template is defined as no expansion necessary.
    38  		return val, nil
    39  	}
    40  	ret := strings.ReplaceAll(template, "{{value}}", val)
    41  	// Check to see if there are any Terraform fields to expand. If so, send it through field
    42  	// expansion.
    43  	if fieldRegex.MatchString(ret) {
    44  		return expandTemplate(ret, r, c, smLoader)
    45  	}
    46  	return ret, nil
    47  }
    48  
    49  func resolveValueTemplateFromInterface(template string, val interface{}, r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (interface{}, error) {
    50  	if template == "" {
    51  		// An empty template is defined as no expansion necessary.
    52  		return val, nil
    53  	}
    54  	switch valAsType := val.(type) {
    55  	case string:
    56  		return ResolveValueTemplate(template, valAsType, r, c, smLoader)
    57  	default:
    58  		return nil, fmt.Errorf("cannot resolve value template for non-string type")
    59  	}
    60  }
    61  
    62  // returns true if the value could have been obtained from the template, i.e. if 'template' is "folders/{{value}}"
    63  //   - if 'value' is "folders/1234567" the result will be true
    64  //   - if 'value' is "organizations/1234567" the result will be false
    65  func valueMatchesTemplate(template string, value string) bool {
    66  	if template == "" {
    67  		return true
    68  	}
    69  	template = strings.ReplaceAll(template, "{{value}}", ".*")
    70  	r := regexp.MustCompile(template)
    71  	return r.MatchString(value)
    72  }
    73  
    74  func expandTemplate(template string, r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (string, error) {
    75  	leftBracketIdx := strings.Index(template, "[")
    76  	if leftBracketIdx == -1 {
    77  		if isORTemplate(template) {
    78  			return expandOrTemplate(template, r, c, smLoader)
    79  		}
    80  		return expandFieldTemplate(template, r, c, smLoader)
    81  	}
    82  	rightBracketIdx := strings.LastIndex(template, "]")
    83  	if rightBracketIdx == -1 {
    84  		return "", fmt.Errorf("template '%v' has a left bracket but is missing corresponding right bracket", template)
    85  	}
    86  	subTemplate := template[leftBracketIdx+1 : rightBracketIdx]
    87  	result, err := expandTemplate(subTemplate, r, c, smLoader)
    88  	if err != nil {
    89  		return "", fmt.Errorf("error resolving sub-template: %w", err)
    90  	}
    91  	resolvedTemplate := fmt.Sprintf("%v%v%v", template[:leftBracketIdx], result, template[rightBracketIdx+1:])
    92  	return expandTemplate(resolvedTemplate, r, c, smLoader)
    93  }
    94  
    95  func isORTemplate(template string) bool {
    96  	return strings.Contains(template, "|")
    97  }
    98  
    99  // expandFieldTemplate expands the given template by replacing any occurrence of {{field_name}} with
   100  // the value of that field. Note that this may trigger further recursive field expansions, if the
   101  // field's value depends on further template expansion.
   102  func expandFieldTemplate(template string, r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (string, error) {
   103  	var resolutionError error
   104  	resolveFunc := func(s string) string {
   105  		field := fieldRegex.FindStringSubmatch(s)[1]
   106  		isRequired := true
   107  		if strings.HasSuffix(field, "?") {
   108  			isRequired = false
   109  			field = strings.TrimSuffix(field, "?")
   110  		}
   111  		if isRef, refConfig := IsReferenceField(field, &r.ResourceConfig); isRef {
   112  			val, found, err := getValueFromReference(refConfig, r, c, smLoader)
   113  			if err != nil {
   114  				resolutionError = fmt.Errorf("error getting value from reference: %w", err)
   115  				return ""
   116  			}
   117  			if !found {
   118  				if isRequired {
   119  					resolutionError = fmt.Errorf("required reference '%v' could not be found in spec", field)
   120  				}
   121  				return ""
   122  			}
   123  			return val
   124  		}
   125  		if field == r.ResourceConfig.ResourceID.TargetField {
   126  			val, err := resolveResourceID(r, c, smLoader)
   127  			if err != nil {
   128  				resolutionError = fmt.Errorf("error resolving resource ID: %w", err)
   129  				return ""
   130  			}
   131  			return val
   132  		}
   133  		if field == r.ResourceConfig.MetadataMapping.Name {
   134  			val, err := resolveNameMetadataMapping(r, c, smLoader)
   135  			if err != nil {
   136  				resolutionError = fmt.Errorf("error resolving metadata name mapping value: %w", err)
   137  				return ""
   138  			}
   139  			return val
   140  		}
   141  
   142  		if field == r.ResourceConfig.MetadataMapping.Labels {
   143  			resolutionError = fmt.Errorf("cannot map labels (map[string]string) to string field '%v'", field)
   144  			return ""
   145  		}
   146  		if field == "region" && r.ResourceConfig.Locationality == gcp.Regional ||
   147  			field == "zone" && r.ResourceConfig.Locationality == gcp.Zonal {
   148  			if val, exists, _ := unstructured.NestedString(r.Spec, "location"); exists {
   149  				return val
   150  			}
   151  		}
   152  		if !SupportsHierarchicalReferences(&r.ResourceConfig) {
   153  			// TODO(b/193177782): Delete this if-block once all resources
   154  			// support hierarchical references.
   155  			for _, c := range r.ResourceConfig.Containers {
   156  				if field == c.TFField {
   157  					annotation := k8s.GetAnnotationForContainerType(c.Type)
   158  					val, ok := k8s.GetAnnotation(annotation, r)
   159  					if (!ok || val == "") && isRequired {
   160  						resolutionError = fmt.Errorf("no value found for annotation %v", annotation)
   161  						return ""
   162  					}
   163  					return val
   164  				}
   165  			}
   166  		}
   167  		for _, d := range r.ResourceConfig.Directives {
   168  			if field == d {
   169  				annotation := k8s.FormatAnnotation(text.SnakeCaseToKebabCase(d))
   170  				val, ok := k8s.GetAnnotation(annotation, r)
   171  				if (!ok || val == "") && isRequired {
   172  					resolutionError = fmt.Errorf("no value found for annotation %v", annotation)
   173  					return ""
   174  				}
   175  				return val
   176  			}
   177  		}
   178  		path := text.SnakeCaseToLowerCamelCase(field)
   179  		if val, exists, _ := unstructured.NestedString(r.Spec, strings.Split(path, ".")...); exists {
   180  			return val
   181  		}
   182  		if val, exists, _ := unstructured.NestedString(r.Status, strings.Split(path, ".")...); exists {
   183  			return val
   184  		}
   185  		if isRequired {
   186  			resolutionError = fmt.Errorf("unable to resolve missing value: %v", field)
   187  		}
   188  		return ""
   189  	}
   190  	return fieldRegex.ReplaceAllStringFunc(template, resolveFunc), resolutionError
   191  }
   192  
   193  func expandOrTemplate(template string, r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (string, error) {
   194  	templates := strings.Split(template, "|")
   195  	expectedCount := 2
   196  	if len(templates) != expectedCount {
   197  		return "", fmt.Errorf("unexpected template format: after splitting on '|' there are '%v' templates when %v were expected",
   198  			len(templates), expectedCount)
   199  	}
   200  	t1 := templates[0]
   201  	t2 := templates[1]
   202  	result, err := expandTemplate(t1, r, c, smLoader)
   203  	if err == nil {
   204  		return result, nil
   205  	}
   206  	result, err = expandTemplate(t2, r, c, smLoader)
   207  	if err != nil {
   208  		return "", fmt.Errorf("error resolving both sides of an '|' template: %w", err)
   209  	}
   210  	return result, nil
   211  }
   212  
   213  func getValueFromReference(refConfig *corekccv1alpha1.ReferenceConfig, r *Resource, c client.Client,
   214  	smLoader *servicemappingloader.ServiceMappingLoader) (val string, found bool, err error) {
   215  	pathToRef := getPathToReferenceKey(refConfig)
   216  	refObj, ok, err := unstructured.NestedMap(r.Spec, pathToRef...)
   217  	if err != nil {
   218  		return "", false, fmt.Errorf("error getting reference object '%v': %v", strings.Join(pathToRef, "."), err)
   219  	}
   220  	if !ok {
   221  		return "", false, nil
   222  	}
   223  	retRaw, err := ResolveReferenceObject(refObj, *refConfig, r, c, smLoader)
   224  	if err != nil {
   225  		return "", false, fmt.Errorf("error resolving reference field: %w", err)
   226  	}
   227  	ret, ok := retRaw.(string)
   228  	if !ok {
   229  		return "", false, fmt.Errorf("could not parse reference resolution value '%+v' as string", retRaw)
   230  	}
   231  	return ret, true, nil
   232  }
   233  
   234  func extractValueSegmentFromIDInStatus(idInStatus, template string) (string, error) {
   235  	if template == "" {
   236  		return idInStatus, nil
   237  	}
   238  
   239  	// Convert the template to be a compilable regular expression.
   240  	template = strings.ReplaceAll(template, "{{value}}", "([^/]+)")
   241  
   242  	// Template starting with the parent field value may contain additional
   243  	// slashes that are not in the template.
   244  	// E.g. DataCatalogPolicyTag has resource ID template:
   245  	// "{{taxonomy}}/policyTags/{{value}}", however the value of the parent
   246  	// field, 'spec.taxonomy', is "projects/test-project/locations/us/taxonomies/tid",
   247  	// which contains additional slashes that are not captured in the template.
   248  	if strings.HasPrefix(template, "{{") {
   249  		re := regexp.MustCompile(`^({{[a-z]([a-z_]*[a-z])*}})/.*$`)
   250  		matched := re.FindStringSubmatch(template)
   251  		if len(matched) == 0 {
   252  			return "", fmt.Errorf("error extracting the parent field name from resource ID template %v", template)
   253  		}
   254  		parentField := matched[1]
   255  		template = strings.ReplaceAll(template, parentField, "[^/](.*[^/])*")
   256  	}
   257  
   258  	template = fieldRegex.ReplaceAllString(template, "[^/]+")
   259  	template = fmt.Sprintf("%s%s%s", "^", template, "$")
   260  
   261  	// Extract out the resourceID from the value.
   262  	templateRegex := regexp.MustCompile(template)
   263  	subMatches := templateRegex.FindStringSubmatch(idInStatus)
   264  	if len(subMatches) < 2 {
   265  		return "", fmt.Errorf("error extracting out the value segment "+
   266  			"from the idInStatus template '%s' using template '%s'", idInStatus,
   267  			template)
   268  	}
   269  
   270  	return subMatches[len(subMatches)-1], nil
   271  }
   272  

View as plain text