mmv1/api/type.go (613 lines of code) (raw):

// Copyright 2024 Google Inc. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package api import ( "fmt" "log" "strings" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/resource" "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" "golang.org/x/exp/slices" ) // Represents a property type type Type struct { Name string `yaml:"name,omitempty"` // original value of :name before the provider override happens // same as :name if not overridden in provider ApiName string `yaml:"api_name,omitempty"` // TODO rewrite: improve the parsing of properties based on type in resource yaml files. Type string DefaultValue interface{} `yaml:"default_value,omitempty"` // Expected to follow the format as follows: // // description: | // This is a description of a field. // If it comprises multiple lines, it must continue to be indented. // Description string `yaml:"description,omitempty"` Exclude bool `yaml:"exclude,omitempty"` // Add a deprecation message for a field that's been deprecated in the API // use the YAML chomping folding indicator (>-) if this is a multiline // string, as providers expect a single-line one w/o a newline. DeprecationMessage string `yaml:"deprecation_message,omitempty"` // Add a removed message for fields no longer supported in the API. This should // be used for fields supported in one version but have been removed from // a different version. RemovedMessage string `yaml:"removed_message,omitempty"` // If set value will not be sent to server on sync. // For nested fields, this also needs to be set on each descendant (ie. self, // child, etc.). Output bool `yaml:"output,omitempty"` // If set to true, changes in the field's value require recreating the // resource. // For nested fields, this only applies at the current level. This means // it should be explicitly added to each field that needs the ForceNew // behavior. Immutable bool `yaml:"immutable,omitempty"` // Indicates that this field is client-side only (aka virtual.) ClientSide bool `yaml:"client_side,omitempty"` // url_param_only will not send the field in the resource body and will // not attempt to read the field from the API response. // NOTE - this doesn't work for nested fields UrlParamOnly bool `yaml:"url_param_only,omitempty"` // For nested fields, this only applies within the parent. // For example, an optional parent can contain a required child. Required bool `yaml:"required,omitempty"` // Additional query Parameters to append to GET calls. ReadQueryParams string `yaml:"read_query_params,omitempty"` UpdateVerb string `yaml:"update_verb,omitempty"` UpdateUrl string `yaml:"update_url,omitempty"` // Some updates only allow updating certain fields at once (generally each // top-level field can be updated one-at-a-time). If this is set, we group // fields to update by (verb, url, fingerprint, id) instead of just // (verb, url, fingerprint), to allow multiple fields to reuse the same // endpoints. UpdateId string `yaml:"update_id,omitempty"` // The fingerprint value required to update this field. Downstreams should // GET the resource and parse the fingerprint value while doing each update // call. This ensures we can supply the fingerprint to each distinct // request. FingerprintName string `yaml:"fingerprint_name,omitempty"` // If true, we will include the empty value in requests made including // this attribute (both creates and updates). This rarely needs to be // set to true, and corresponds to both the "NullFields" and // "ForceSendFields" concepts in the autogenerated API clients. SendEmptyValue bool `yaml:"send_empty_value,omitempty"` // [Optional] If true, empty nested objects are sent to / read from the // API instead of flattened to null. // The difference between this and send_empty_value is that send_empty_value // applies when the key of an object is empty; this applies when the values // are all nil / default. eg: "expiration: null" vs "expiration: {}" // In the case of Terraform, this occurs when a block in config has optional // values, and none of them are used. Terraform returns a nil instead of an // empty map[string]interface{} like we'd expect. AllowEmptyObject bool `yaml:"allow_empty_object,omitempty"` MinVersion string `yaml:"min_version,omitempty"` ExactVersion string `yaml:"exact_version,omitempty"` // A list of properties that conflict with this property. Uses the "lineage" // field to identify the property eg: parent.meta.label.foo Conflicts []string `yaml:"conflicts,omitempty"` // A list of properties that at least one of must be set. AtLeastOneOf []string `yaml:"at_least_one_of,omitempty"` // A list of properties that exactly one of must be set. ExactlyOneOf []string `yaml:"exactly_one_of,omitempty"` // A list of properties that are required to be set together. RequiredWith []string `yaml:"required_with,omitempty"` // Can only be overridden - we should never set this ourselves. NewType string `yaml:"-"` Properties []*Type `yaml:"properties,omitempty"` EnumValues []string `yaml:"enum_values,omitempty"` ExcludeDocsValues bool `yaml:"exclude_docs_values,omitempty"` // ==================== // Array Fields // ==================== ItemType *Type `yaml:"item_type,omitempty"` MinSize string `yaml:"min_size,omitempty"` MaxSize string `yaml:"max_size,omitempty"` // Adds a ValidateFunc to the item schema ItemValidation resource.Validation `yaml:"item_validation,omitempty"` ParentName string `yaml:"parent_name,omitempty"` // ==================== // ResourceRef Fields // ==================== Resource string `yaml:"resource,omitempty"` Imports string `yaml:"imports,omitempty"` // ==================== // Terraform Overrides // ==================== // Adds a DiffSuppressFunc to the schema DiffSuppressFunc string `yaml:"diff_suppress_func,omitempty"` StateFunc string `yaml:"state_func,omitempty"` // Adds a StateFunc to the schema Sensitive bool `yaml:"sensitive,omitempty"` // Adds `Sensitive: true` to the schema WriteOnly bool `yaml:"write_only,omitempty"` // Adds `WriteOnly: true` to the schema // Does not set this value to the returned API value. Useful for fields // like secrets where the returned API value is not helpful. IgnoreRead bool `yaml:"ignore_read,omitempty"` // Adds a ValidateFunc to the schema Validation resource.Validation `yaml:"validation,omitempty"` // Indicates that this is an Array that should have Set diff semantics. UnorderedList bool `yaml:"unordered_list,omitempty"` IsSet bool `yaml:"is_set,omitempty"` // Uses a Set instead of an Array // Optional function to determine the unique ID of an item in the set // If not specified, schema.HashString (when elements are string) or // schema.HashSchema are used. SetHashFunc string `yaml:"set_hash_func,omitempty"` // if true, then we get the default value from the Google API if no value // is set in the terraform configuration for this field. // It translates to setting the field to Computed & Optional in the schema. // For nested fields, this only applies at the current level. This means // it should be explicitly added to each field that needs the defaulting // behavior. DefaultFromApi bool `yaml:"default_from_api,omitempty"` // https://github.com/hashicorp/terraform/pull/20837 // Apply a ConfigMode of SchemaConfigModeAttr to the field. // This should be avoided for new fields, and only used with old ones. SchemaConfigModeAttr bool `yaml:"schema_config_mode_attr,omitempty"` // Names of fields that should be included in the updateMask. UpdateMaskFields []string `yaml:"update_mask_fields,omitempty"` // For a TypeMap, the expander function to call on the key. // Defaults to expandString. KeyExpander string `yaml:"key_expander,omitempty"` // For a TypeMap, the DSF to apply to the key. KeyDiffSuppressFunc string `yaml:"key_diff_suppress_func,omitempty"` // ==================== // Map Fields // ==================== // The type definition of the contents of the map. ValueType *Type `yaml:"value_type,omitempty"` // While the API doesn't give keys an explicit name, we specify one // because in Terraform the key has to be a property of the object. // // The name of the key. Used in the Terraform schema as a field name. KeyName string `yaml:"key_name,omitempty"` // A description of the key's format. Used in Terraform to describe // the field in documentation. KeyDescription string `yaml:"key_description,omitempty"` // ==================== // KeyValuePairs Fields // ==================== // Ignore writing the "effective_labels" and "effective_annotations" fields to API. IgnoreWrite bool `yaml:"ignore_write,omitempty"` // ==================== // Schema Modifications // ==================== // Schema modifications change the schema of a resource in some // fundamental way. They're not very portable, and will be hard to // generate so we should limit their use. Generally, if you're not // converting existing Terraform resources, these shouldn't be used. // // With great power comes great responsibility. // Flattens a NestedObject by removing that field from the Terraform // schema but will preserve it in the JSON sent/retrieved from the API // // EX: a API schema where fields are nested (eg: `one.two.three`) and we // desire the properties of the deepest nested object (eg: `three`) to // become top level properties in the Terraform schema. By overriding // the properties `one` and `one.two` and setting flatten_object then // all the properties in `three` will be at the root of the TF schema. // // We need this for cases where a field inside a nested object has a // default, if we can't spend a breaking change to fix a misshapen // field, or if the UX is _much_ better otherwise. // // WARN: only fully flattened properties are currently supported. In the // example above you could not flatten `one.two` without also flattening // all of it's parents such as `one` FlattenObject bool `yaml:"flatten_object,omitempty"` // =========== // Custom code // =========== // All custom code attributes are string-typed. The string should // be the name of a template file which will be compiled in the // specified / described place. // A custom expander replaces the default expander for an attribute. // It is called as part of Create, and as part of Update if // object.input is false. It can return an object of any type, // so the function header *is* part of the custom code template. // As with flatten, `property` and `prefix` are available. CustomExpand string `yaml:"custom_expand,omitempty"` // A custom flattener replaces the default flattener for an attribute. // It is called as part of Read. It can return an object of any // type, and may sometimes need to return an object with non-interface{} // type so that the d.Set() call will succeed, so the function // header *is* a part of the custom code template. To help with // creating the function header, `property` and `prefix` are available, // just as they are in the standard flattener template. CustomFlatten string `yaml:"custom_flatten,omitempty"` ResourceMetadata *Resource `yaml:"resource_metadata,omitempty"` ParentMetadata *Type `yaml:"parent_metadata,omitempty"` // is nil for top-level properties // The prefix used as part of the property expand/flatten function name // flatten{{$.GetPrefix}}{{$.TitlelizeProperty}} Prefix string `yaml:"prefix,omitempty"` } const MAX_NAME = 20 func (t *Type) SetDefault(r *Resource) { t.ResourceMetadata = r if t.UpdateVerb == "" { t.UpdateVerb = t.ResourceMetadata.UpdateVerb } switch { case t.IsA("Array"): t.ItemType.Name = t.Name t.ItemType.ParentName = t.Name t.ItemType.ParentMetadata = t t.ItemType.SetDefault(r) case t.IsA("Map"): if t.KeyExpander == "" { t.KeyExpander = "tpgresource.ExpandString" } t.ValueType.ParentName = t.Name t.ValueType.ParentMetadata = t t.ValueType.SetDefault(r) case t.IsA("NestedObject"): if t.Name == "" { t.Name = t.ParentName } if t.Description == "" { t.Description = "A nested object resource." } for _, p := range t.Properties { p.ParentMetadata = t p.SetDefault(r) } case t.IsA("ResourceRef"): if t.Name == "" { t.Name = t.Resource } if t.Description == "" { t.Description = fmt.Sprintf("A reference to %s resource", t.Resource) } case t.IsA("Fingerprint"): // Represents a fingerprint. A fingerprint is an output-only // field used for optimistic locking during updates. // They are fetched from the GCP response. t.Output = true default: } if t.ApiName == "" { t.ApiName = t.Name } } func (t *Type) Validate(rName string) { if t.Name == "" { log.Fatalf("Missing `name` for proprty with type %s in resource %s", t.Type, rName) } if t.Output && t.Required { log.Fatalf("Property %s cannot be output and required at the same time in resource %s.", t.Name, rName) } if t.DefaultFromApi && t.DefaultValue != nil { log.Fatalf("'default_value' and 'default_from_api' cannot be both set in resource %s", rName) } if t.WriteOnly && (t.DefaultFromApi || t.Output) { log.Fatalf("Property %s cannot be write_only and default_from_api or output at the same time in resource %s", t.Name, rName) } if t.WriteOnly && t.Sensitive { log.Fatalf("Property %s cannot be write_only and sensitive at the same time in resource %s", t.Name, rName) } t.validateLabelsField() switch { case t.IsA("Array"): t.ItemType.Validate(rName) case t.IsA("Map"): t.ValueType.Validate(rName) case t.IsA("NestedObject"): for _, p := range t.Properties { p.Validate(rName) } default: } } // TODO rewrite: add validations // check :description, required: true // check :update_verb, allowed: %i[POST PUT PATCH NONE], // check_default_value_property // check_conflicts // check_at_least_one_of // check_exactly_one_of // check_required_with // check the allowed types for Type field // check the allowed fields for each type, for example, KeyName is only allowed for Map // Prints a dot notation path to where the field is nested within the parent // object. eg: parent.meta.label.foo // The only intended purpose is to allow better error messages. Some objects // and at some points in the build this doesn't output a valid output. func (t Type) Lineage() string { if t.ParentMetadata == nil { return google.Underscore(t.Name) } return fmt.Sprintf("%s.%s", t.ParentMetadata.Lineage(), google.Underscore(t.Name)) } // Returns a dot notation path to where the field is nested within the parent // object. eg: parent.meta.label.foo // This format is intended for resource metadata, to be used for connecting a Terraform // type with a corresponding API type. func (t Type) MetadataLineage() string { if t.ParentMetadata == nil { return google.Underscore(t.Name) } // Skip arrays because otherwise the array name will be included twice if t.ParentMetadata.IsA("Array") { return t.ParentMetadata.MetadataLineage() } return fmt.Sprintf("%s.%s", t.ParentMetadata.MetadataLineage(), google.Underscore(t.Name)) } // Returns a dot notation path to where the field is nested within the parent // object. eg: parent.meta.label.foo // This format is intended for to represent an API type. func (t Type) MetadataApiLineage() string { apiName := t.ApiName if t.ParentMetadata == nil { return google.Underscore(apiName) } if t.ParentMetadata.IsA("Array") { return t.ParentMetadata.MetadataApiLineage() } return fmt.Sprintf("%s.%s", t.ParentMetadata.MetadataApiLineage(), google.Underscore(apiName)) } // Returns the lineage in snake case func (t Type) LineageAsSnakeCase() string { if t.ParentMetadata == nil { return google.Underscore(t.Name) } return fmt.Sprintf("%s_%s", t.ParentMetadata.LineageAsSnakeCase(), google.Underscore(t.Name)) } // Prints the access path of the field in the configration eg: metadata.0.labels // The only intended purpose is to get the value of the labes field by calling d.Get(). func (t Type) TerraformLineage() string { if t.ParentMetadata == nil || t.ParentMetadata.FlattenObject { return google.Underscore(t.Name) } return fmt.Sprintf("%s.0.%s", t.ParentMetadata.TerraformLineage(), google.Underscore(t.Name)) } func (t Type) EnumValuesToString(quoteSeperator string, addEmpty bool) string { var values []string for _, val := range t.EnumValues { values = append(values, fmt.Sprintf("%s%s%s", quoteSeperator, val, quoteSeperator)) } if addEmpty && !slices.Contains(values, "\"\"") && !t.Required { values = append(values, "\"\"") } return strings.Join(values, ", ") } func (t Type) TitlelizeProperty() string { return google.Camelize(t.Name, "upper") } // If the Prefix field is already set, returns the value. // Otherwise, set the Prefix field and returns the value. func (t *Type) GetPrefix() string { if t.Prefix == "" { if t.ParentMetadata == nil { nestedPrefix := "" // TODO: Use the nestedPrefix for tgc provider to be consistent with terraform provider if t.ResourceMetadata.NestedQuery != nil && t.ResourceMetadata.Compiler != "terraformgoogleconversion-codegen" { nestedPrefix = "Nested" } t.Prefix = fmt.Sprintf("%s%s", nestedPrefix, t.ResourceMetadata.ResourceName()) } else { if t.ParentMetadata != nil && (t.ParentMetadata.IsA("Array") || t.ParentMetadata.IsA("Map")) { t.Prefix = t.ParentMetadata.GetPrefix() } else { if t.ParentMetadata != nil && t.ParentMetadata.ParentMetadata != nil && t.ParentMetadata.ParentMetadata.IsA("Map") { t.Prefix = fmt.Sprintf("%s%s", t.ParentMetadata.GetPrefix(), t.ParentMetadata.ParentMetadata.TitlelizeProperty()) } else { t.Prefix = fmt.Sprintf("%s%s", t.ParentMetadata.GetPrefix(), t.ParentMetadata.TitlelizeProperty()) } } } } return t.Prefix } func (t Type) ResourceType() string { r := t.ResourceRef() if r == nil { return "" } path := strings.Split(r.BaseUrl, "/") return path[len(path)-1] } // TODO rewrite: validation // func (t *Type) check_default_value_property() { // return if @default_value.nil? // case self // when Api::Type::String // clazz = ::String // when Api::Type::Integer // clazz = ::Integer // when Api::Type::Double // clazz = ::Float // when Api::Type::Enum // clazz = ::Symbol // when Api::Type::Boolean // clazz = :boolean // when Api::Type::ResourceRef // clazz = [::String, ::Hash] // else // raise "Update 'check_default_value_property' method to support " \ // "default value for type //{self.class}" // end // check :default_value, type: clazz // } // Checks that all conflicting properties actually exist. // This currently just returns if empty, because we don't want to do the check, since // this list will have a full path for nested attributes. // func (t *Type) check_conflicts() { // check :conflicts, type: ::Array, default: [], item_type: ::String // return if @conflicts.empty? // } // Returns list of properties that are in conflict with this property. // func (t *Type) conflicting() { func (t Type) Conflicting() []string { if t.ResourceMetadata == nil { return []string{} } return t.Conflicts } // TODO rewrite: validation // Checks that all properties that needs at least one of their fields actually exist. // This currently just returns if empty, because we don't want to do the check, since // this list will have a full path for nested attributes. // func (t *Type) check_at_least_one_of() { // check :at_least_one_of, type: ::Array, default: [], item_type: ::String // return if @at_least_one_of.empty? // } // Returns list of properties that needs at least one of their fields set. // func (t *Type) at_least_one_of_list() { func (t Type) AtLeastOneOfList() []string { if t.ResourceMetadata == nil { return []string{} } return t.AtLeastOneOf } // TODO rewrite: validation // Checks that all properties that needs exactly one of their fields actually exist. // This currently just returns if empty, because we don't want to do the check, since // this list will have a full path for nested attributes. // func (t *Type) check_exactly_one_of() { // check :exactly_one_of, type: ::Array, default: [], item_type: ::String // return if @exactly_one_of.empty? // } // Returns list of properties that needs exactly one of their fields set. // func (t *Type) exactly_one_of_list() { func (t Type) ExactlyOneOfList() []string { if t.ResourceMetadata == nil { return []string{} } return t.ExactlyOneOf } // TODO rewrite: validation // Checks that all properties that needs required with their fields actually exist. // This currently just returns if empty, because we don't want to do the check, since // this list will have a full path for nested attributes. // func (t *Type) check_required_with() { // check :required_with, type: ::Array, default: [], item_type: ::String // return if @required_with.empty? // } // Returns list of properties that needs required with their fields set. func (t Type) RequiredWithList() []string { if t.ResourceMetadata == nil { return []string{} } return t.RequiredWith } func (t Type) Parent() *Type { return t.ParentMetadata } func (t Type) MinVersionObj() *product.Version { if t.MinVersion != "" { return t.ResourceMetadata.ProductMetadata.versionObj(t.MinVersion) } else { return t.ResourceMetadata.MinVersionObj() } } func (t *Type) exactVersionObj() *product.Version { if t.ExactVersion == "" { return nil } return t.ResourceMetadata.ProductMetadata.versionObj(t.ExactVersion) } func (t *Type) ExcludeIfNotInVersion(version *product.Version) { if !t.Exclude { if versionObj := t.exactVersionObj(); versionObj != nil { t.Exclude = versionObj.CompareTo(version) != 0 } if !t.Exclude { t.Exclude = version.CompareTo(t.MinVersionObj()) < 0 } } if t.IsA("NestedObject") { for _, p := range t.Properties { p.ExcludeIfNotInVersion(version) } } else if t.IsA("Array") && t.ItemType.IsA("NestedObject") { t.ItemType.ExcludeIfNotInVersion(version) } } func (t Type) IsA(clazz string) bool { if clazz == "" { log.Fatalf("class cannot be empty") } if t.NewType != "" { return t.NewType == clazz } return t.Type == clazz } // Returns nested properties for this property. func (t Type) NestedProperties() []*Type { props := make([]*Type, 0) switch { case t.IsA("Array"): if t.ItemType.IsA("NestedObject") { props = google.Reject(t.ItemType.NestedProperties(), func(p *Type) bool { return t.Exclude }) } case t.IsA("NestedObject"): props = t.UserProperties() case t.IsA("Map"): props = google.Reject(t.ValueType.NestedProperties(), func(p *Type) bool { return t.Exclude }) default: } return props } // Returns write-only properties for this property. func (t Type) WriteOnlyProperties() []*Type { props := make([]*Type, 0) switch { case t.IsA("Array"): if t.ItemType.IsA("NestedObject") { props = google.Reject(t.ItemType.WriteOnlyProperties(), func(p *Type) bool { return t.Exclude }) } case t.IsA("NestedObject"): props = google.Select(t.UserProperties(), func(p *Type) bool { return p.WriteOnly }) case t.IsA("Map"): props = google.Reject(t.ValueType.WriteOnlyProperties(), func(p *Type) bool { return t.Exclude }) default: } return props } func (t Type) Removed() bool { return t.RemovedMessage != "" } func (t Type) Deprecated() bool { return t.DeprecationMessage != "" } func (t *Type) GetDescription() string { return strings.TrimSpace(strings.TrimRight(t.Description, "\n")) } // TODO rewrite: validation // class Array < Composite // check :item_type, type: [::String, NestedObject, ResourceRef, Enum], required: true // unless @item_type.is_a?(NestedObject) || @item_type.is_a?(ResourceRef) \ // || @item_type.is_a?(Enum) || type?(@item_type) // raise "Invalid type //{@item_type}" // end // This function is for array field func (t Type) ItemTypeClass() string { if !t.IsA("Array") { return "" } return t.ItemType.Type } func (t Type) TFType(s string) string { switch s { case "Boolean": return "schema.TypeBool" case "Double": return "schema.TypeFloat" case "Integer": return "schema.TypeInt" case "String": return "schema.TypeString" case "Time": return "schema.TypeString" case "Enum": return "schema.TypeString" case "ResourceRef": return "schema.TypeString" case "NestedObject": return "schema.TypeList" case "Array": return "schema.TypeList" case "KeyValuePairs": return "schema.TypeMap" case "KeyValueLabels": return "schema.TypeMap" case "KeyValueTerraformLabels": return "schema.TypeMap" case "KeyValueEffectiveLabels": return "schema.TypeMap" case "KeyValueAnnotations": return "schema.TypeMap" case "Map": return "schema.TypeSet" case "Fingerprint": return "schema.TypeString" } return "schema.TypeString" } // TODO rewrite: validation // // Represents an enum, and store is valid values // class Enum < Primitive // values // skip_docs_values // func (t *Type) validate // super // check :values, type: ::Array, item_type: [Symbol, ::String, ::Integer], required: true // check :skip_docs_values, type: :boolean // end // // Represents a reference to another resource // class ResourceRef < Type // // The fields which can be overridden in provider.yaml. // module Fields // resource // imports // end // include Fields // func (t *Type) validate // super // @name = @resource if @name.nil? // @description = "A reference to //{@resource} resource" \ // if @description.nil? // return if @__resource.nil? || @__resource.exclude || @exclude // check :resource, type: ::String, required: true // check :imports, type: ::String, required: TrueClass // // TODO: (camthornton) product reference may not exist yet // return if @__resource.__product.nil? // check_resource_ref_property_exists // end func (t Type) ResourceRef() *Resource { if !t.IsA("ResourceRef") { return nil } product := t.ResourceMetadata.ProductMetadata resources := google.Select(product.Objects, func(obj *Resource) bool { return obj.Name == t.Resource }) return resources[0] } // TODO rewrite: validation // func (t *Type) check_resource_ref_property_exists // return unless defined?(resource_ref.all_user_properties) // exported_props = resource_ref.all_user_properties // exported_props << Api::Type::String.new('selfLink') \ // if resource_ref.has_self_link // raise "'//{@imports}' does not exist on '//{@resource}'" \ // if exported_props.none? { |p| p.name == @imports } // end // end // // An structured object composed of other objects. // class NestedObject < Composite // func (t *Type) validate // @description = 'A nested object resource' if @description.nil? // @name = @__name if @name.nil? // super // raise "Properties missing on //{name}" if @properties.nil? // @properties.each do |p| // p.set_variable(@__resource, :__resource) // p.set_variable(self, :__parent) // end // check :properties, type: ::Array, item_type: Api::Type, required: true // end // Returns all properties including the ones that are excluded // This is used for PropertyOverride validation func (t Type) AllProperties() []*Type { return t.Properties } func (t Type) UserProperties() []*Type { if t.IsA("NestedObject") { if t.Properties == nil { log.Fatalf("Field '{%s}' properties are nil!", t.Lineage()) } return google.Reject(t.Properties, func(p *Type) bool { return p.Exclude }) } return nil } // Returns the list of top-level properties once any nested objects with // flatten_object set to true have been collapsed func (t *Type) RootProperties() []*Type { props := make([]*Type, 0) for _, p := range t.UserProperties() { if p.FlattenObject { props = google.Concat(props, p.RootProperties()) } else { props = append(props, p) } } return props } // An array of string -> string key -> value pairs, such as labels. // While this is technically a map, it's split out because it's a much // simpler property to generate and means we can avoid conditional logic // in Map. func NewProperty(name, apiName string, options []func(*Type)) *Type { p := &Type{ Name: name, ApiName: apiName, } for _, option := range options { option(p) } return p } func propertyWithType(t string) func(*Type) { return func(p *Type) { p.Type = t } } func propertyWithOutput(output bool) func(*Type) { return func(p *Type) { p.Output = output } } func propertyWithDescription(description string) func(*Type) { return func(p *Type) { p.Description = description } } func propertyWithMinVersion(minVersion string) func(*Type) { return func(p *Type) { p.MinVersion = minVersion } } func propertyWithUpdateVerb(updateVerb string) func(*Type) { return func(p *Type) { p.UpdateVerb = updateVerb } } func propertyWithUpdateUrl(updateUrl string) func(*Type) { return func(p *Type) { p.UpdateUrl = updateUrl } } func propertyWithImmutable(immutable bool) func(*Type) { return func(p *Type) { p.Immutable = immutable } } func propertyWithClientSide(clientSide bool) func(*Type) { return func(p *Type) { p.ClientSide = clientSide } } func propertyWithIgnoreWrite(ignoreWrite bool) func(*Type) { return func(p *Type) { p.IgnoreWrite = ignoreWrite } } func (t *Type) validateLabelsField() { productName := t.ResourceMetadata.ProductMetadata.Name resourceName := t.ResourceMetadata.Name lineage := t.Lineage() if lineage == "labels" || lineage == "metadata.labels" || lineage == "configuration.labels" { if !t.IsA("KeyValueLabels") && // The label value must be empty string, so skip this resource !(productName == "CloudIdentity" && resourceName == "Group") && // The "labels" field has type Array, so skip this resource !(productName == "DeploymentManager" && resourceName == "Deployment") && // https://github.com/hashicorp/terraform-provider-google/issues/16219 !(productName == "Edgenetwork" && resourceName == "Network") && // https://github.com/hashicorp/terraform-provider-google/issues/16219 !(productName == "Edgenetwork" && resourceName == "Subnet") && // "userLabels" is the resource labels field !(productName == "Monitoring" && resourceName == "NotificationChannel") && // The "labels" field has type Array, so skip this resource !(productName == "Monitoring" && resourceName == "MetricDescriptor") { log.Fatalf("Please use type KeyValueLabels for field %s in resource %s/%s", lineage, productName, resourceName) } } else if t.IsA("KeyValueLabels") { log.Fatalf("Please don't use type KeyValueLabels for field %s in resource %s/%s", lineage, productName, resourceName) } if lineage == "annotations" || lineage == "metadata.annotations" { if !t.IsA("KeyValueAnnotations") && // The "annotations" field has "ouput: true", so skip this eap resource !(productName == "Gkeonprem" && resourceName == "BareMetalAdminClusterEnrollment") { log.Fatalf("Please use type KeyValueAnnotations for field %s in resource %s/%s", lineage, productName, resourceName) } } else if t.IsA("KeyValueAnnotations") { log.Fatalf("Please don't use type KeyValueAnnotations for field %s in resource %s/%s", lineage, productName, resourceName) } } func (t Type) fieldMinVersion() string { return t.MinVersion } // TODO rewrite: validation // // An array of string -> string key -> value pairs used specifically for the "labels" field. // // The field name with this type should be "labels" literally. // class KeyValueLabels < KeyValuePairs // func (t *Type) validate // super // return unless @name != 'labels' // raise "The field //{name} has the type KeyValueLabels, but the field name is not 'labels'!" // end // end // // An array of string -> string key -> value pairs used for the "terraform_labels" field. // class KeyValueTerraformLabels < KeyValuePairs // end // // An array of string -> string key -> value pairs used for the "effective_labels" // // and "effective_annotations" fields. // class KeyValueEffectiveLabels < KeyValuePairs // end // // An array of string -> string key -> value pairs used specifically for the "annotations" field. // // The field name with this type should be "annotations" literally. // class KeyValueAnnotations < KeyValuePairs // func (t *Type) validate // super // return unless @name != 'annotations' // raise "The field //{name} has the type KeyValueAnnotations,\ // but the field name is not 'annotations'!" // end // end // TODO rewrite: validation // // Map from string keys -> nested object entries // class Map < Composite // func (t *Type) validate // super // check :key_name, type: ::String, required: true // check :key_description, type: ::String // check :value_type, type: Api::Type::NestedObject, required: true // raise "Invalid type //{@value_type}" unless type?(@value_type) // end func (t Type) PropertyNsPrefix() []string { return []string{ "Google", google.Camelize(t.ResourceMetadata.ProductMetadata.Name, "upper"), "Property", } } // "Namespace" - prefix with product and resource - a property with // information from the "object" variable func (t Type) NamespaceProperty() string { name := google.Camelize(t.Name, "upper") p := t for p.Parent() != nil { p = *p.Parent() name = fmt.Sprintf("%s%s", google.Camelize(p.Name, "upper"), name) } return fmt.Sprintf("%s%s%s", google.Camelize(t.ResourceMetadata.ProductMetadata.ApiName, "lower"), t.ResourceMetadata.Name, name) } func (t Type) CustomTemplate(templatePath string, appendNewline bool) string { return resource.ExecuteTemplate(&t, templatePath, appendNewline) } func (t *Type) GetIdFormat() string { return t.ResourceMetadata.GetIdFormat() } func (t *Type) GoLiteral(value interface{}) string { switch v := value.(type) { case int: return fmt.Sprintf("%d", v) case float64: return fmt.Sprintf("%.1f", v) case bool: return fmt.Sprintf("%v", v) case string: if !strings.HasPrefix(v, "\"") { return fmt.Sprintf("\"%s\"", v) } return v case []string: for i, val := range v { v[i] = fmt.Sprintf("\"%v\"", val) } return fmt.Sprintf("[]string{%s}", strings.Join(v, ",")) default: panic(fmt.Errorf("unknown go literal type %+v", value)) } } func (t *Type) IsForceNew() bool { if t.IsA("KeyValueLabels") && t.ResourceMetadata.RootLabels() { return false } if t.IsA("KeyValueTerraformLabels") && !t.ResourceMetadata.Updatable() && !t.ResourceMetadata.RootLabels() { return true } // Client-side fields don't inherit immutability if t.ClientSide { return t.Immutable } parent := t.Parent() return !t.WriteOnly && (!t.Output || t.IsA("KeyValueEffectiveLabels")) && (t.Immutable || (t.ResourceMetadata.Immutable && t.UpdateUrl == "" && (parent == nil || (parent.IsForceNew() && !(parent.FlattenObject && t.IsA("KeyValueLabels")))))) } // Returns true if the type does not correspond to an API type func (t *Type) ProviderOnly() bool { // These are special case fields created by the generator which have no API counterpart if t.IsA("KeyValueEffectiveLabels") || t.IsA("KeyValueTerraformLabels") { return true } if t.UrlParamOnly || t.ClientSide { return true } // The type is provider-only if any of its ancestors are provider-only (it is inherited) parent := t.Parent() return parent != nil && parent.ProviderOnly() } // Returns an updated path for a given Terraform field path (e.g. // 'a_field', 'parent_field.0.child_name'). Returns nil if the property // is not included in the resource's properties and removes keys that have // been flattened // FYI: Fields that have been renamed should use the new name, however, flattened // fields still need to be included, ie: // flattenedField > newParent > renameMe should be passed to this function as // flattened_field.0.new_parent.0.im_renamed // TODO(emilymye): Change format of input for // exactly_one_of/at_least_one_of/etc to use camelcase, MM properities and // convert to snake in this method func (t *Type) GetPropertySchemaPath(schemaPath string) string { nestedProps := t.ResourceMetadata.UserProperites() var pathTkns []string for _, pname := range strings.Split(schemaPath, ".0.") { camelPname := google.Camelize(pname, "lower") index := slices.IndexFunc(nestedProps, func(p *Type) bool { return p.Name == camelPname }) // if we couldn't find it, see if it was renamed at the top level if index == -1 { index = slices.IndexFunc(nestedProps, func(p *Type) bool { return p.Name == schemaPath }) } if index == -1 { return "" } prop := nestedProps[index] nestedProps = prop.NestedProperties() if !prop.FlattenObject { pathTkns = append(pathTkns, google.Underscore(pname)) } } if len(pathTkns) == 0 || pathTkns[len(pathTkns)-1] == "" { return "" } return strings.Join(pathTkns[:], ".0.") } func (t Type) GetPropertySchemaPathList(propertyList []string) []string { var list []string for _, path := range propertyList { path = t.GetPropertySchemaPath(path) if path != "" { list = append(list, path) } } return list }