...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf/resource.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  	"reflect"
    20  	"strings"
    21  
    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  
    27  	tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    28  	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  )
    33  
    34  // Resource is a wrapper around k8s.Resource and adds information regarding its
    35  // corresponding Terraform resource and maintains an original copy of the
    36  // k8s.Resource.
    37  type Resource struct {
    38  	k8s.Resource `json:",inline"`
    39  
    40  	Original *k8s.Resource `json:"-"`
    41  
    42  	// Fields related to TF provider processing
    43  	TFInfo         *terraform.InstanceInfo        `json:"-"`
    44  	ResourceConfig corekccv1alpha1.ResourceConfig `json:"-"`
    45  	TFResource     *tfschema.Resource             `json:"-"`
    46  }
    47  
    48  // NewResource returns a Resource, populating the Resource information from u.Kind,
    49  // using the structs found in sm and p.
    50  func NewResource(u *unstructured.Unstructured, sm *corekccv1alpha1.ServiceMapping, p *tfschema.Provider) (*Resource, error) {
    51  	rc, err := servicemappingloader.GetResourceConfig(sm, u)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	resource, err := NewResourceFromResourceConfig(rc, p)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  
    60  	r, err := k8s.NewResource(u)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  	resource.Resource = *r
    65  
    66  	// Intentionally re-create the K8s resource to create a separate copy.
    67  	resource.Original, err = k8s.NewResource(u)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	if err := resource.ValidateResourceIDIfSupported(); err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	return resource, nil
    77  }
    78  
    79  func NewResourceFromResourceConfig(rc *corekccv1alpha1.ResourceConfig, p *tfschema.Provider) (*Resource, error) {
    80  	tfResource, ok := p.ResourcesMap[rc.Name]
    81  	if !ok {
    82  		return nil, fmt.Errorf("error getting TF resource: unknown resource %v", rc.Name)
    83  	}
    84  	resource := &Resource{
    85  		TFInfo: &terraform.InstanceInfo{
    86  			Type: rc.Name,
    87  		},
    88  		ResourceConfig: *rc,
    89  		TFResource:     tfResource,
    90  	}
    91  	return resource, nil
    92  }
    93  
    94  func getServerGeneratedIDFromStatus(rc *corekccv1alpha1.ResourceConfig, status map[string]interface{}) (string, bool, error) {
    95  	splitPath := text.SnakeCaseStrsToLowerCamelCaseStrs(
    96  		strings.Split(rc.ServerGeneratedIDField, "."))
    97  
    98  	return unstructured.NestedString(status, splitPath...)
    99  }
   100  
   101  func (r *Resource) ValidateResourceIDIfSupported() error {
   102  	if !SupportsResourceIDField(&r.ResourceConfig) {
   103  		return nil
   104  	}
   105  
   106  	_, err := r.IsResourceIDConfigured()
   107  	if err != nil {
   108  		return fmt.Errorf("error validating '%s' field: %v", k8s.ResourceIDFieldPath, err)
   109  	}
   110  	return nil
   111  }
   112  
   113  func (r *Resource) ConstructServerGeneratedIDInStatusFromResourceID(c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (string, error) {
   114  	resourceID, foundInSpec, err := unstructured.NestedString(r.Spec, k8s.ResourceIDFieldName)
   115  	if err != nil {
   116  		return "", fmt.Errorf("error getting '%s': %w",
   117  			k8s.ResourceIDFieldPath, err)
   118  	}
   119  
   120  	if !foundInSpec {
   121  		return "", nil
   122  	}
   123  
   124  	if foundInSpec && resourceID == "" {
   125  		return "", fmt.Errorf("the value of '%s' is invalid: '' (empty "+
   126  			"string)", k8s.ResourceIDFieldPath)
   127  	}
   128  
   129  	resourceID, err = ResolveValueTemplate(
   130  		r.ResourceConfig.ResourceID.ValueTemplate, resourceID, r, c, smLoader)
   131  	if err != nil {
   132  		return "", fmt.Errorf("error expanding resource ID: %w", err)
   133  	}
   134  
   135  	return resourceID, nil
   136  }
   137  
   138  func (r *Resource) SelfLinkAsID() (string, error) {
   139  	selfLink, found, err := unstructured.NestedString(r.Status, k8s.SelfLinkFieldName)
   140  	if err != nil {
   141  		return "", fmt.Errorf("error getting '%s': %w",
   142  			k8s.SelfLinkFieldName, err)
   143  	}
   144  	if !found {
   145  		return "", fmt.Errorf("resource %s doesn't have a '%s' field", r.Name, k8s.SelfLinkFieldName)
   146  	}
   147  	return selfLink, nil
   148  }
   149  
   150  // GetImportID returns the Terraform import ID for the resource.
   151  // TODO(kcc-eng): Require ID templates for all resources and remove all implicit defaults.
   152  func (r *Resource) GetImportID(c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (string, error) {
   153  	template := r.ResourceConfig.IDTemplate
   154  	if r.HasServerGeneratedIDField() {
   155  		// when using a server generated id for import, ensure it is there before importing to get a more specific
   156  		// error of type ServerGeneratedIDNotFoundError
   157  		if template == "" {
   158  			template = r.serverGeneratedIdToTemplate()
   159  			if _, err := r.GetServerGeneratedID(); err != nil {
   160  				return "", err
   161  			}
   162  		} else if r.serverGeneratedIdInIdTemplate() {
   163  			if _, err := r.GetServerGeneratedID(); err != nil {
   164  				return "", err
   165  			}
   166  		}
   167  	} else {
   168  		if template == "" {
   169  			template = fmt.Sprintf("{{project?}}/{{%v}}", r.ResourceConfig.MetadataMapping.Name)
   170  		}
   171  	}
   172  	value, err := expandTemplate(template, r, c, smLoader)
   173  	if err != nil {
   174  		// Some resources, e.g. Project, have (1) a server-generated ID and (2)
   175  		// an ID template that doesn't contain the server-generated ID in it.
   176  		// And they can be imported by either (1) or (2). The following if block
   177  		// is to get import ID via (1) after failing to resolve (2).
   178  		if r.shouldFallBackToServerGeneratedIdIfImportIdFails() {
   179  			template = r.serverGeneratedIdToTemplate()
   180  			return expandTemplate(template, r, c, smLoader)
   181  		}
   182  		return "", err
   183  	}
   184  	return value, nil
   185  }
   186  
   187  func (r *Resource) HasIDTemplate() bool {
   188  	return r.ResourceConfig.IDTemplate != ""
   189  }
   190  
   191  func (r *Resource) HasServerGeneratedIDField() bool {
   192  	return r.ResourceConfig.ServerGeneratedIDField != ""
   193  }
   194  
   195  func (r *Resource) serverGeneratedIdToTemplate() string {
   196  	return ServerGeneratedIdToTemplate(&r.ResourceConfig)
   197  }
   198  
   199  func (r *Resource) shouldFallBackToServerGeneratedIdIfImportIdFails() bool {
   200  	return r.HasServerGeneratedIDField() && !r.serverGeneratedIdInIdTemplate()
   201  }
   202  
   203  func (r *Resource) serverGeneratedIdInIdTemplate() bool {
   204  	if !r.HasIDTemplate() || !r.HasServerGeneratedIDField() {
   205  		return false
   206  	}
   207  	idTemplateFormOfServerGeneratedId := fmt.Sprintf("{{%v}}", r.ResourceConfig.ServerGeneratedIDField)
   208  	return strings.Contains(r.ResourceConfig.IDTemplate, idTemplateFormOfServerGeneratedId)
   209  }
   210  
   211  // GetServerGeneratedID gets the value of the resource's server-generated ID.
   212  // There are two cases:
   213  // (1) If the resource supports a server-generated `spec.resourceID`, return
   214  //
   215  //	its value if specified.  If unspecified, continue to case (2) but
   216  //	extract out the resource ID segment from the server-generated ID field
   217  //	using the value template of the resource ID field.
   218  //
   219  // (2) If the resource doesn't support a server-generated `spec.resourceID`
   220  //
   221  //	field, then look up the field defined in ResourceConfig.ServerGeneratedIDField
   222  //	in `status` and return its value. Note: this value is not a resource ID,
   223  //	but a raw value in the status field.
   224  func (r *Resource) GetServerGeneratedID() (string, error) {
   225  	if SupportsResourceIDField(&r.ResourceConfig) && IsResourceIDFieldServerGenerated(&r.ResourceConfig) {
   226  		id, exists, err := unstructured.NestedString(r.Spec, k8s.ResourceIDFieldName)
   227  		if err != nil {
   228  			return "", fmt.Errorf("error getting server-generated resource ID: %w", err)
   229  		}
   230  		if exists {
   231  			if id == "" {
   232  				return "", fmt.Errorf("invalid empty value for \"spec.%s\"",
   233  					k8s.ResourceIDFieldName)
   234  			}
   235  			return id, nil
   236  		}
   237  	}
   238  
   239  	// If the resource doesn't support a server-generated `spec.resourceID` or
   240  	// if the field is not specified, fallback to resolve it from status.
   241  	idInStatus, exists, err := getServerGeneratedIDFromStatus(&r.ResourceConfig, r.Status)
   242  	if err != nil {
   243  		return "", fmt.Errorf("error getting server-generated ID: %v", err)
   244  	}
   245  	if !exists {
   246  		return "", k8s.NewServerGeneratedIDNotFoundError(r.GroupVersionKind(),
   247  			k8s.GetNamespacedName(r))
   248  	}
   249  
   250  	if idInStatus == "" {
   251  		return "", fmt.Errorf("invalid empty value for \"status.%s\"",
   252  			text.SnakeCaseToLowerCamelCase(r.ResourceConfig.ServerGeneratedIDField))
   253  	}
   254  
   255  	if SupportsResourceIDField(&r.ResourceConfig) && IsResourceIDFieldServerGenerated(&r.ResourceConfig) {
   256  		id, err := extractValueSegmentFromIDInStatus(idInStatus,
   257  			r.ResourceConfig.ResourceID.ValueTemplate)
   258  		if err != nil {
   259  			return "", fmt.Errorf("error getting server-generated "+
   260  				"resource ID from the value of '%s': %w", fmt.Sprintf("status.%s",
   261  				text.SnakeCaseToLowerCamelCase(r.ResourceConfig.ServerGeneratedIDField)), err)
   262  		}
   263  
   264  		if id == "" {
   265  			return "", fmt.Errorf("invalid empty value for server-generated resource ID")
   266  		}
   267  		return id, nil
   268  	}
   269  	return idInStatus, nil
   270  }
   271  
   272  // GetResourceID gets the resource's resource ID. The assumption is
   273  // that the resource supports the `spec.resourceID` field.
   274  // There are two cases:
   275  // (1) If `spec.resourceID` is specified, return its value.
   276  // (2) Otherwise, (happens during KCC upgrade or resource creation), fall back to:
   277  //   - Value of `metadata.name` if the resource ID is user-specified.
   278  //   - Value of the server generated ID field in status if the resource ID is
   279  //     server-generated.
   280  func (r *Resource) GetResourceID() (string, error) {
   281  	resourceID, exists, err := unstructured.NestedString(r.Spec, k8s.ResourceIDFieldName)
   282  	if err != nil {
   283  		return "", fmt.Errorf("error getting the value of "+
   284  			"\"spec.%s\": %w", k8s.ResourceIDFieldName, err)
   285  	}
   286  
   287  	if !exists {
   288  		if !IsResourceIDFieldServerGenerated(&r.ResourceConfig) {
   289  			resourceID = r.GetName()
   290  		} else {
   291  			resourceID, err = r.GetServerGeneratedID()
   292  			if err != nil {
   293  				return "", err
   294  			}
   295  		}
   296  	}
   297  
   298  	if resourceID == "" {
   299  		return "", fmt.Errorf("invalid empty value for resource ID")
   300  	}
   301  	return resourceID, nil
   302  }
   303  
   304  func (r *Resource) Unreadable() bool {
   305  	return r.ResourceConfig.Unreadable != nil && *r.ResourceConfig.Unreadable
   306  }
   307  
   308  // AllTopLevelFieldsAreImmutableOrComputed returns true if the resource schema only
   309  // contains top level fields that are immutable and/or computed.
   310  func (r *Resource) AllTopLevelFieldsAreImmutableOrComputed() bool {
   311  	for _, schema := range r.TFResource.Schema {
   312  		if !schema.Computed && !schema.ForceNew {
   313  			return false
   314  		}
   315  	}
   316  	return true
   317  }
   318  
   319  func SupportsResourceIDField(rc *corekccv1alpha1.ResourceConfig) bool {
   320  	return rc.ResourceID.TargetField != ""
   321  }
   322  
   323  func IsResourceIDFieldServerGenerated(rc *corekccv1alpha1.ResourceConfig) bool {
   324  	return rc.ResourceID.TargetField == rc.ServerGeneratedIDField
   325  }
   326  
   327  func SupportsServerGeneratedIDField(rc *corekccv1alpha1.ResourceConfig) bool {
   328  	return rc.ServerGeneratedIDField != ""
   329  }
   330  
   331  func SupportsHierarchicalReferences(rc *corekccv1alpha1.ResourceConfig) bool {
   332  	return len(rc.HierarchicalReferences) > 0
   333  }
   334  
   335  func SupportsIAM(rc *corekccv1alpha1.ResourceConfig) bool {
   336  	emptyIAMConfig := corekccv1alpha1.IAMConfig{}
   337  	return !reflect.DeepEqual(rc.IAMConfig, emptyIAMConfig)
   338  }
   339  
   340  func GVKForResource(sm *corekccv1alpha1.ServiceMapping, rc *corekccv1alpha1.ResourceConfig) schema.GroupVersionKind {
   341  	return schema.GroupVersionKind{
   342  		Group:   sm.Name,
   343  		Version: sm.GetVersionFor(rc),
   344  		Kind:    rc.Kind,
   345  	}
   346  }
   347  
   348  func ServerGeneratedIdToTemplate(rc *corekccv1alpha1.ResourceConfig) string {
   349  	return fmt.Sprintf("{{%v}}", rc.ServerGeneratedIDField)
   350  }
   351  

View as plain text