github.com/crossplane/upjet@v1.3.0/pkg/types/builder.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package types
     6  
     7  import (
     8  	"fmt"
     9  	"go/token"
    10  	"go/types"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
    15  	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    16  	twtypes "github.com/muvaf/typewriter/pkg/types"
    17  	"github.com/pkg/errors"
    18  	"k8s.io/utils/ptr"
    19  
    20  	"github.com/crossplane/upjet/pkg/config"
    21  )
    22  
    23  const (
    24  	wildcard = "*"
    25  
    26  	emptyStruct = "struct{}"
    27  
    28  	// ref: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation-rules
    29  	celEscapeSequence = "__%s__"
    30  	// description for an injected list map key field in the context of the
    31  	// server-side apply object list merging
    32  	descriptionInjectedKey = "This is an injected field with a default value for being able to merge items of the parent object list."
    33  )
    34  
    35  var (
    36  	// ref: https://github.com/google/cel-spec/blob/v0.6.0/doc/langdef.md#syntax
    37  	celReservedKeywords = []string{"true", "false", "null", "in", "as", "break", "const", "continue",
    38  		"else", "for", "function", "if", "import", "let", "loop", "package", "namespace", "return", "var",
    39  		"void", "while"}
    40  )
    41  
    42  // Generated is a struct that holds generated types
    43  type Generated struct {
    44  	Types    []*types.Named
    45  	Comments twtypes.Comments
    46  
    47  	ForProviderType  *types.Named
    48  	InitProviderType *types.Named
    49  	AtProviderType   *types.Named
    50  
    51  	ValidationRules string
    52  }
    53  
    54  // Builder is used to generate Go type equivalence of given Terraform schema.
    55  type Builder struct {
    56  	Package *types.Package
    57  
    58  	genTypes        []*types.Named
    59  	comments        twtypes.Comments
    60  	validationRules string
    61  }
    62  
    63  // NewBuilder returns a new Builder.
    64  func NewBuilder(pkg *types.Package) *Builder {
    65  	return &Builder{
    66  		Package:  pkg,
    67  		comments: twtypes.Comments{},
    68  	}
    69  }
    70  
    71  // Build returns parameters and observation types built out of Terraform schema.
    72  func (g *Builder) Build(cfg *config.Resource) (Generated, error) {
    73  	if err := injectServerSideApplyListMergeKeys(cfg); err != nil {
    74  		return Generated{}, errors.Wrapf(err, "cannot inject server-side apply merge keys for resource %q", cfg.Name)
    75  	}
    76  
    77  	fp, ap, ip, err := g.buildResource(cfg.TerraformResource, cfg, nil, nil, false, cfg.Kind)
    78  	return Generated{
    79  		Types:            g.genTypes,
    80  		Comments:         g.comments,
    81  		ForProviderType:  fp,
    82  		InitProviderType: ip,
    83  		AtProviderType:   ap,
    84  		ValidationRules:  g.validationRules,
    85  	}, errors.Wrapf(err, "cannot build the Types for resource %q", cfg.Name)
    86  }
    87  
    88  func injectServerSideApplyListMergeKeys(cfg *config.Resource) error { //nolint:gocyclo // Easier to follow the logic in a single function
    89  	for f, s := range cfg.ServerSideApplyMergeStrategies {
    90  		if s.ListMergeStrategy.MergeStrategy != config.ListTypeMap {
    91  			continue
    92  		}
    93  		if s.ListMergeStrategy.ListMapKeys.InjectedKey.Key == "" && len(s.ListMergeStrategy.ListMapKeys.Keys) == 0 {
    94  			return errors.Errorf("list map keys configuration for the object list %q is empty", f)
    95  		}
    96  		if s.ListMergeStrategy.ListMapKeys.InjectedKey.Key == "" {
    97  			continue
    98  		}
    99  		sch := config.GetSchema(cfg.TerraformResource, f)
   100  		if sch == nil {
   101  			return errors.Errorf("cannot find the Terraform schema for the argument at the path %q", f)
   102  		}
   103  		if sch.Type != schema.TypeList && sch.Type != schema.TypeSet {
   104  			return errors.Errorf("fieldpath %q is not a Terraform list or set", f)
   105  		}
   106  		el, ok := sch.Elem.(*schema.Resource)
   107  		if !ok {
   108  			return errors.Errorf("fieldpath %q is a Terraform list or set but its element type is not a Terraform *schema.Resource", f)
   109  		}
   110  		for k := range el.Schema {
   111  			if k == s.ListMergeStrategy.ListMapKeys.InjectedKey.Key {
   112  				return errors.Errorf("element schema for the object list %q already contains the argument key %q", f, k)
   113  			}
   114  		}
   115  		el.Schema[s.ListMergeStrategy.ListMapKeys.InjectedKey.Key] = &schema.Schema{
   116  			Type:        schema.TypeString,
   117  			Required:    true,
   118  			Description: descriptionInjectedKey,
   119  		}
   120  		if s.ListMergeStrategy.ListMapKeys.InjectedKey.DefaultValue != "" {
   121  			el.Schema[s.ListMergeStrategy.ListMapKeys.InjectedKey.Key].Default = s.ListMergeStrategy.ListMapKeys.InjectedKey.DefaultValue
   122  		}
   123  	}
   124  	return nil
   125  }
   126  
   127  func (g *Builder) buildResource(res *schema.Resource, cfg *config.Resource, tfPath []string, xpPath []string, asBlocksMode bool, names ...string) (*types.Named, *types.Named, *types.Named, error) { //nolint:gocyclo
   128  	// NOTE(muvaf): There can be fields in the same CRD with same name but in
   129  	// different types. Since we generate the type using the field name, there
   130  	// can be collisions. In order to be able to generate unique names consistently,
   131  	// we need to process all fields in the same order all the time.
   132  	keys := sortedKeys(res.Schema)
   133  
   134  	typeNames, err := NewTypeNames(names, g.Package, cfg.OverrideFieldNames)
   135  	if err != nil {
   136  		return nil, nil, nil, err
   137  	}
   138  
   139  	r := &resource{}
   140  	for _, snakeFieldName := range keys {
   141  		var reference *config.Reference
   142  		cPath := fieldPath(append(tfPath, snakeFieldName))
   143  		ref, ok := cfg.References[cPath]
   144  		// if a reference is configured and the field does not belong to status
   145  		if ok && !IsObservation(res.Schema[snakeFieldName]) {
   146  			reference = &ref
   147  		}
   148  
   149  		var f *Field
   150  		switch {
   151  		case res.Schema[snakeFieldName].Sensitive:
   152  			var drop bool
   153  			f, drop, err = NewSensitiveField(g, cfg, r, res.Schema[snakeFieldName], snakeFieldName, tfPath, xpPath, names, asBlocksMode)
   154  			if err != nil {
   155  				return nil, nil, nil, err
   156  			}
   157  			if drop {
   158  				continue
   159  			}
   160  		case reference != nil:
   161  			f, err = NewReferenceField(g, cfg, r, res.Schema[snakeFieldName], reference, snakeFieldName, tfPath, xpPath, names, asBlocksMode)
   162  			if err != nil {
   163  				return nil, nil, nil, err
   164  			}
   165  		default:
   166  			f, err = NewField(g, cfg, r, res.Schema[snakeFieldName], snakeFieldName, tfPath, xpPath, names, asBlocksMode)
   167  			if err != nil {
   168  				return nil, nil, nil, err
   169  			}
   170  		}
   171  		f.AddToResource(g, r, typeNames, cfg.SchemaElementOptions.AddToObservation(cPath))
   172  	}
   173  
   174  	paramType, obsType, initType := g.AddToBuilder(typeNames, r)
   175  	return paramType, obsType, initType, nil
   176  }
   177  
   178  // AddToBuilder adds fields to the Builder.
   179  func (g *Builder) AddToBuilder(typeNames *TypeNames, r *resource) (*types.Named, *types.Named, *types.Named) {
   180  	// NOTE(muvaf): Not every struct has both computed and configurable fields,
   181  	// so some types we generate here are empty and unnecessary. However,
   182  	// there are valid types with zero fields and we don't have the information
   183  	// to differentiate between valid zero fields and unnecessary one. So we generate
   184  	// two structs for every complex type.
   185  	// See usage of wafv2EmptySchema() in aws_wafv2_web_acl here:
   186  	// https://github.com/hashicorp/terraform-provider-aws/blob/main/aws/wafv2_helper.go#L13
   187  	paramType := types.NewNamed(typeNames.ParameterTypeName, types.NewStruct(r.paramFields, r.paramTags), nil)
   188  	g.genTypes = append(g.genTypes, paramType)
   189  
   190  	initType := types.NewNamed(typeNames.InitTypeName, types.NewStruct(r.initFields, r.initTags), nil)
   191  	g.genTypes = append(g.genTypes, initType)
   192  
   193  	obsType := types.NewNamed(typeNames.ObservationTypeName, types.NewStruct(r.obsFields, r.obsTags), nil)
   194  	g.genTypes = append(g.genTypes, obsType)
   195  
   196  	for _, p := range r.topLevelRequiredParams {
   197  		g.validationRules += "\n"
   198  		sp := sanitizePath(p.path)
   199  		if p.includeInit {
   200  			g.validationRules += fmt.Sprintf(`// +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.%s) || (has(self.initProvider) && has(self.initProvider.%s))",message="spec.forProvider.%s is a required parameter"`, sp, sp, p.path)
   201  		} else {
   202  			g.validationRules += fmt.Sprintf(`// +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.%s)",message="spec.forProvider.%s is a required parameter"`, sp, p.path)
   203  		}
   204  	}
   205  
   206  	return paramType, obsType, initType
   207  }
   208  
   209  func (g *Builder) buildSchema(f *Field, cfg *config.Resource, names []string, cpath string, r *resource) (types.Type, types.Type, error) { //nolint:gocyclo
   210  	switch f.Schema.Type {
   211  	case schema.TypeBool:
   212  		return types.NewPointer(types.Universe.Lookup("bool").Type()), nil, nil
   213  	case schema.TypeFloat:
   214  		return types.NewPointer(types.Universe.Lookup("float64").Type()), nil, nil
   215  	case schema.TypeInt:
   216  		return types.NewPointer(types.Universe.Lookup("int64").Type()), nil, nil
   217  	case schema.TypeString:
   218  		return types.NewPointer(types.Universe.Lookup("string").Type()), nil, nil
   219  	case schema.TypeMap, schema.TypeList, schema.TypeSet:
   220  		names = append(names, f.Name.Camel)
   221  		if f.Schema.Type != schema.TypeMap {
   222  			// We don't want to have a many-to-many relationship in case of a Map, since we use SecretReference as
   223  			// the type of XP field. In this case, we want to have a one-to-many relationship which is handled at
   224  			// runtime in the controller.
   225  			f.TerraformPaths = append(f.TerraformPaths, wildcard)
   226  			f.CRDPaths = append(f.CRDPaths, wildcard)
   227  		}
   228  		var elemType types.Type
   229  		var initElemType types.Type
   230  		switch et := f.Schema.Elem.(type) {
   231  		case schema.ValueType:
   232  			switch et {
   233  			case schema.TypeBool:
   234  				elemType = types.Universe.Lookup("bool").Type()
   235  			case schema.TypeFloat:
   236  				elemType = types.Universe.Lookup("float64").Type()
   237  			case schema.TypeInt:
   238  				elemType = types.Universe.Lookup("int64").Type()
   239  			case schema.TypeString:
   240  				elemType = types.Universe.Lookup("string").Type()
   241  			case schema.TypeMap, schema.TypeList, schema.TypeSet, schema.TypeInvalid:
   242  				return nil, nil, errors.Errorf("element type of %s is basic but not one of known basic types", fieldPath(names))
   243  			}
   244  			initElemType = elemType
   245  		case *schema.Schema:
   246  			newf, err := NewField(g, cfg, r, et, f.Name.Snake, f.TerraformPaths, f.CRDPaths, names, false)
   247  			if err != nil {
   248  				return nil, nil, err
   249  			}
   250  			elemType = newf.FieldType
   251  			initElemType = elemType
   252  		case *schema.Resource:
   253  			var asBlocksMode bool
   254  			// TODO(muvaf): We skip the other type once we choose one of param
   255  			// or obs types. This might cause some fields to be completely omitted.
   256  			if f.Schema.ConfigMode == schema.SchemaConfigModeAttr {
   257  				asBlocksMode = true
   258  			}
   259  			paramType, obsType, initType, err := g.buildResource(et, cfg, f.TerraformPaths, f.CRDPaths, asBlocksMode, names...)
   260  			if err != nil {
   261  				return nil, nil, errors.Wrapf(err, "cannot infer type from resource schema of element type of %s", fieldPath(names))
   262  			}
   263  			initElemType = initType
   264  
   265  			switch {
   266  			case IsObservation(f.Schema):
   267  				if obsType == nil {
   268  					return nil, nil, errors.Errorf("element type of %s is computed but the underlying schema does not return observation type", fieldPath(names))
   269  				}
   270  				elemType = obsType
   271  				// There are some types that are computed and not optional (observation field) but also has nested fields
   272  				// that can go under spec. This check prevents the elimination of fields in parameter type, by checking
   273  				// whether the schema in observation type has nested parameter (spec) fields.
   274  				if paramType.Underlying().String() != emptyStruct {
   275  					var tParam, tInit types.Type
   276  					if cfg.SchemaElementOptions.EmbeddedObject(cpath) {
   277  						tParam = types.NewPointer(paramType)
   278  						tInit = types.NewPointer(initType)
   279  					} else {
   280  						tParam = types.NewSlice(paramType)
   281  						tInit = types.NewSlice(initType)
   282  					}
   283  					r.addParameterField(f, types.NewField(token.NoPos, g.Package, f.Name.Camel, tParam, false))
   284  					r.addInitField(f, types.NewField(token.NoPos, g.Package, f.Name.Camel, tInit, false), g, nil)
   285  				}
   286  			default:
   287  				if paramType == nil {
   288  					return nil, nil, errors.Errorf("element type of %s is configurable but the underlying schema does not return a parameter type", fieldPath(names))
   289  				}
   290  				elemType = paramType
   291  				// There are some types that are parameter field but also has nested fields that can go under status.
   292  				// This check prevents the elimination of fields in observation type, by checking whether the schema in
   293  				// parameter type has nested observation (status) fields.
   294  				if obsType.Underlying().String() != emptyStruct {
   295  					var t types.Type
   296  					if cfg.SchemaElementOptions.EmbeddedObject(cpath) {
   297  						t = types.NewPointer(obsType)
   298  					} else {
   299  						t = types.NewSlice(obsType)
   300  					}
   301  					field := types.NewField(token.NoPos, g.Package, f.Name.Camel, t, false)
   302  					r.addObservationField(f, field)
   303  				}
   304  			}
   305  		// if unset
   306  		// see: https://github.com/crossplane/upjet/issues/177
   307  		case nil:
   308  			elemType = types.Universe.Lookup("string").Type()
   309  			initElemType = elemType
   310  		default:
   311  			return nil, nil, errors.Errorf("element type of %s should be either schema.Resource or schema.Schema", fieldPath(names))
   312  		}
   313  
   314  		// if the singleton list is to be replaced by an embedded object
   315  		if cfg.SchemaElementOptions.EmbeddedObject(cpath) {
   316  			return types.NewPointer(elemType), types.NewPointer(initElemType), nil
   317  		}
   318  		// NOTE(muvaf): Maps and slices are already pointers, so we don't need to
   319  		// wrap them even if they are optional.
   320  		if f.Schema.Type == schema.TypeMap {
   321  			return types.NewMap(types.Universe.Lookup("string").Type(), elemType), types.NewMap(types.Universe.Lookup("string").Type(), initElemType), nil
   322  		}
   323  		return types.NewSlice(elemType), types.NewSlice(initElemType), nil
   324  	case schema.TypeInvalid:
   325  		return nil, nil, errors.Errorf("invalid schema type %s", f.Schema.Type.String())
   326  	default:
   327  		return nil, nil, errors.Errorf("unexpected schema type %s", f.Schema.Type.String())
   328  	}
   329  }
   330  
   331  // TypeNames represents the parameter and observation name of the resource.
   332  type TypeNames struct {
   333  	ParameterTypeName   *types.TypeName
   334  	InitTypeName        *types.TypeName
   335  	ObservationTypeName *types.TypeName
   336  }
   337  
   338  // NewTypeNames returns a new TypeNames object.
   339  func NewTypeNames(fieldPaths []string, pkg *types.Package, overrideFieldNames map[string]string) (*TypeNames, error) {
   340  	paramTypeName, err := generateTypeName("Parameters", pkg, overrideFieldNames, fieldPaths...)
   341  	if err != nil {
   342  		return nil, errors.Wrapf(err, "cannot generate parameters type name of %s", fieldPath(fieldPaths))
   343  	}
   344  	paramName := types.NewTypeName(token.NoPos, pkg, paramTypeName, nil)
   345  
   346  	initTypeName, err := generateTypeName("InitParameters", pkg, overrideFieldNames, fieldPaths...)
   347  	if err != nil {
   348  		return nil, errors.Wrapf(err, "cannot generate init parameters type name of %s", fieldPath(fieldPaths))
   349  	}
   350  	initName := types.NewTypeName(token.NoPos, pkg, initTypeName, nil)
   351  
   352  	obsTypeName, err := generateTypeName("Observation", pkg, overrideFieldNames, fieldPaths...)
   353  	if err != nil {
   354  		return nil, errors.Wrapf(err, "cannot generate observation type name of %s", fieldPath(fieldPaths))
   355  	}
   356  	obsName := types.NewTypeName(token.NoPos, pkg, obsTypeName, nil)
   357  
   358  	// We insert them to the package scope so that the type name calculations in
   359  	// recursive calls are checked against their upper level type's name as well.
   360  	pkg.Scope().Insert(paramName)
   361  	pkg.Scope().Insert(initName)
   362  	pkg.Scope().Insert(obsName)
   363  
   364  	return &TypeNames{ParameterTypeName: paramName, InitTypeName: initName, ObservationTypeName: obsName}, nil
   365  }
   366  
   367  type resource struct {
   368  	paramFields, initFields, obsFields []*types.Var
   369  	paramTags, initTags, obsTags       []string
   370  	topLevelRequiredParams             []*topLevelRequiredParam
   371  }
   372  
   373  type topLevelRequiredParam struct {
   374  	path        string
   375  	includeInit bool
   376  }
   377  
   378  func newTopLevelRequiredParam(path string, includeInit bool) *topLevelRequiredParam {
   379  	return &topLevelRequiredParam{path: path, includeInit: includeInit}
   380  }
   381  
   382  func (r *resource) addParameterField(f *Field, field *types.Var) {
   383  	requiredBySchema := !f.Schema.Optional || f.Required
   384  	// Note(turkenh): We are collecting the top level required parameters that
   385  	// are not identifier fields. This is for generating CEL validation rules for
   386  	// those parameters and not to require them if the management policy is set
   387  	// Observe Only. In other words, if we are not creating or managing the
   388  	// resource, we don't need to provide those parameters which are:
   389  	// - requiredBySchema => required
   390  	// - !f.Identifier => not identifiers - i.e. region, zone, etc.
   391  	// - len(f.CanonicalPaths) == 1 => top level, i.e. not a nested field
   392  	// TODO (lsviben): We should add CEL rules for all required fields,
   393  	// not just the top level ones, due to having all forProvider
   394  	// fields now optional. CEL rules should check if a field is
   395  	// present either in forProvider or initProvider.
   396  	// https://github.com/crossplane/upjet/issues/239
   397  	if requiredBySchema && !f.Identifier && len(f.CanonicalPaths) == 1 {
   398  		requiredBySchema = false
   399  		// If the field is not a terraform field, we should not require it in init,
   400  		// as it is not an initProvider field.
   401  		r.topLevelRequiredParams = append(r.topLevelRequiredParams, newTopLevelRequiredParam(f.TransformedName, f.TFTag != "-"))
   402  	}
   403  
   404  	// Note(lsviben): Only fields which are not also initProvider fields should have a required kubebuilder comment.
   405  	f.Comment.Required = ptr.To(requiredBySchema && !f.isInit())
   406  
   407  	// For removing omitempty tag from json tag, we are just checking if the field is required by the schema.
   408  	if requiredBySchema {
   409  		// Required fields should not have omitempty tag in json tag.
   410  		// TODO(muvaf): This overrides user intent if they provided custom
   411  		// JSON tag.
   412  		r.paramTags = append(r.paramTags, fmt.Sprintf(`json:"%s" tf:"%s"`, strings.TrimSuffix(f.JSONTag, ",omitempty"), f.TFTag))
   413  	} else {
   414  		r.paramTags = append(r.paramTags, fmt.Sprintf(`json:"%s" tf:"%s"`, f.JSONTag, f.TFTag))
   415  	}
   416  
   417  	r.paramFields = append(r.paramFields, field)
   418  }
   419  
   420  func (r *resource) addInitField(f *Field, field *types.Var, g *Builder, typeNames *types.TypeName) {
   421  	// If the field is not an init field, we don't add it.
   422  	if !f.isInit() {
   423  		return
   424  	}
   425  
   426  	r.initTags = append(r.initTags, fmt.Sprintf(`json:"%s" tf:"%s"`, f.JSONTag, f.TFTag))
   427  
   428  	// If the field is a nested type, we need to add it as the init type.
   429  	if f.InitType != nil {
   430  		field = types.NewField(token.NoPos, g.Package, f.Name.Camel, f.InitType, false)
   431  	}
   432  
   433  	r.initFields = append(r.initFields, field)
   434  
   435  	if f.Reference != nil {
   436  		r.addReferenceFields(g, typeNames, f, true)
   437  	}
   438  }
   439  
   440  func (r *resource) addObservationField(f *Field, field *types.Var) {
   441  	for _, obsF := range r.obsFields {
   442  		if obsF.Name() == field.Name() {
   443  			// If the field is already added, we don't add it again.
   444  			// Some nested types could have been previously added as an
   445  			// observation type while building their schema: https://github.com/crossplane/upjet/blob/b89baca4ae24c8fbd8eb403c353ca18916093e5e/pkg/types/builder.go#L206
   446  			return
   447  		}
   448  	}
   449  	r.obsFields = append(r.obsFields, field)
   450  	r.obsTags = append(r.obsTags, fmt.Sprintf(`json:"%s" tf:"%s"`, f.JSONTag, f.TFTag))
   451  }
   452  
   453  func (r *resource) addReferenceFields(g *Builder, paramName *types.TypeName, field *Field, isInit bool) {
   454  	refFields, refTags := g.generateReferenceFields(paramName, field)
   455  	if isInit {
   456  		r.initTags = append(r.initTags, refTags...)
   457  		r.initFields = append(r.initFields, refFields...)
   458  	} else {
   459  		r.paramTags = append(r.paramTags, refTags...)
   460  		r.paramFields = append(r.paramFields, refFields...)
   461  	}
   462  }
   463  
   464  // generateTypeName generates a unique name for the type if its original name
   465  // is used by another one. It adds the former field names recursively until it
   466  // finds a unique name.
   467  func generateTypeName(suffix string, pkg *types.Package, overrideFieldNames map[string]string, names ...string) (calculated string, _ error) {
   468  	defer func() {
   469  		if v, ok := overrideFieldNames[calculated]; ok {
   470  			calculated = v
   471  		}
   472  	}()
   473  	n := names[len(names)-1] + suffix
   474  	for i := len(names) - 2; i >= 0; i-- {
   475  		if pkg.Scope().Lookup(n) == nil {
   476  			calculated = n
   477  			return
   478  		}
   479  		n = names[i] + n
   480  	}
   481  	if pkg.Scope().Lookup(n) == nil {
   482  		calculated = n
   483  		return
   484  	}
   485  	// start from 2 considering the 1st of this type is the one without an
   486  	// index.
   487  	for i := 2; i < 10; i++ {
   488  		nn := fmt.Sprintf("%s_%d", n, i)
   489  		if pkg.Scope().Lookup(nn) == nil {
   490  			calculated = nn
   491  			return
   492  		}
   493  	}
   494  	return "", errors.Errorf("could not generate a unique name for %s", n)
   495  }
   496  
   497  // IsObservation returns whether the specified Schema belongs to an observed
   498  // attribute, i.e., whether it's a required computed field.
   499  func IsObservation(s *schema.Schema) bool {
   500  	// NOTE(muvaf): If a field is not optional but computed, then it's
   501  	// definitely an observation field.
   502  	// If it's optional but also computed, then it means the field has a server
   503  	// side default but user can change it, so it needs to go to parameters.
   504  	return s.Computed && !s.Optional
   505  }
   506  
   507  func sortedKeys(m map[string]*schema.Schema) []string {
   508  	if len(m) == 0 {
   509  		return nil
   510  	}
   511  	keys := make([]string, len(m))
   512  	i := 0
   513  	for k := range m {
   514  		keys[i] = k
   515  		i++
   516  	}
   517  	sort.Strings(keys)
   518  	return keys
   519  }
   520  
   521  func fieldPath(parts []string) string {
   522  	seg := make(fieldpath.Segments, len(parts))
   523  	for i, p := range parts {
   524  		if p == wildcard {
   525  			continue
   526  		}
   527  		seg[i] = fieldpath.Field(p)
   528  	}
   529  	return seg.String()
   530  }
   531  
   532  func fieldPathWithWildcard(parts []string) string {
   533  	seg := make(fieldpath.Segments, len(parts))
   534  	for i, p := range parts {
   535  		seg[i] = fieldpath.Field(p)
   536  	}
   537  	return seg.String()
   538  }
   539  
   540  func sanitizePath(p string) string {
   541  	for _, reserved := range celReservedKeywords {
   542  		if p == reserved {
   543  			return fmt.Sprintf(celEscapeSequence, p)
   544  		}
   545  	}
   546  	return p
   547  }