mmv1/api/resource.go (1,274 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" "maps" "regexp" "sort" "strings" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/resource" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/utils" "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" "golang.org/x/exp/slices" ) const RELATIVE_MAGICIAN_LOCATION = "mmv1/" const GITHUB_BASE_URL = "https://github.com/GoogleCloudPlatform/magic-modules/tree/main/" + RELATIVE_MAGICIAN_LOCATION type Resource struct { Name string // original value of :name before the provider override happens // same as :name if not overridden in provider ApiName string `yaml:"api_name,omitempty"` // [Required] A description of the resource that's surfaced in provider // documentation. Description string // [Required] Reference links provided in // downstream documentation. Expected to follow the format as follows: // // references: // guides: // 'Guide name': 'official_documentation_url' // api: 'rest_api_reference_url/version' // References resource.ReferenceLinks `yaml:"references,omitempty"` // [Required] The GCP "relative URI" of a resource, relative to the product // base URL. It can often be inferred from the `create` path. BaseUrl string `yaml:"base_url,omitempty"` // ==================== // Common Configuration // ==================== // // [Optional] The minimum API version this resource is in. Defaults to ga. MinVersion string `yaml:"min_version,omitempty"` // [Optional] If set to true, don't generate the resource. Exclude bool `yaml:"exclude,omitempty"` // [Optional] If set to true, the resource is not able to be updated. Immutable bool `yaml:"immutable,omitempty"` // [Optional] If set to true, this resource uses an update mask to perform // updates. This is typical of newer GCP APIs. UpdateMask bool `yaml:"update_mask,omitempty"` // [Optional] If set to true, the object has a `self_link` field. This is // typical of older GCP APIs. HasSelfLink bool `yaml:"has_self_link,omitempty"` // [Optional] The validator "relative URI" of a resource, relative to the product // base URL. Specific to defining the resource as a CAI asset. CaiBaseUrl string `yaml:"cai_base_url,omitempty"` // ==================== // URL / HTTP Configuration // ==================== // // [Optional] The "identity" URL of the resource. Defaults to: // * base_url when the create_verb is POST // * self_link when the create_verb is PUT or PATCH SelfLink string `yaml:"self_link,omitempty"` // [Optional] The URL used to creating the resource. Defaults to: // * collection url when the create_verb is POST // * self_link when the create_verb is PUT or PATCH CreateUrl string `yaml:"create_url,omitempty"` // [Optional] The URL used to delete the resource. Defaults to the self // link. DeleteUrl string `yaml:"delete_url,omitempty"` // [Optional] The URL used to update the resource. Defaults to the self // link. UpdateUrl string `yaml:"update_url,omitempty"` // [Optional] The HTTP verb used during create. Defaults to POST. CreateVerb string `yaml:"create_verb,omitempty"` // [Optional] The HTTP verb used during read. Defaults to GET. ReadVerb string `yaml:"read_verb,omitempty"` // [Optional] The HTTP verb used during update. Defaults to PUT. UpdateVerb string `yaml:"update_verb,omitempty"` // [Optional] The HTTP verb used during delete. Defaults to DELETE. DeleteVerb string `yaml:"delete_verb,omitempty"` // [Optional] Additional Query Parameters to append to GET. Defaults to "" ReadQueryParams string `yaml:"read_query_params,omitempty"` // ==================== // Collection / Identity URL Configuration // ==================== // // [Optional] This is the name of the list of items // within the collection (list) json. Will default to the // camelcase plural name of the resource. CollectionUrlKey string `yaml:"collection_url_key,omitempty"` // [Optional] An ordered list of names of parameters that uniquely identify // the resource. // Generally, it's safe to leave empty, in which case it defaults to `name`. // Other values are normally useful in cases where an object has a parent // and is identified by some non-name value, such as an ip+port pair. // If you're writing a fine-grained resource (eg with nested_query) a value // must be set. Identity []string `yaml:"identity,omitempty"` // [Optional] (Api::Resource::NestedQuery) This is useful in case you need // to change the query made for GET requests only. In particular, this is // often used to extract an object from a parent object or a collection. // Note that if both nested_query and custom_code.decoder are provided, // the decoder will be included within the code handling the nested query. NestedQuery *resource.NestedQuery `yaml:"nested_query,omitempty"` // ==================== // IAM Configuration // ==================== // // [Optional] (Api::Resource::IamPolicy) Configuration of a resource's // resource-specific IAM Policy. IamPolicy *resource.IamPolicy `yaml:"iam_policy,omitempty"` // [Optional] If set to true, don't generate the resource itself; only // generate the IAM policy. // TODO rewrite: rename? ExcludeResource bool `yaml:"exclude_resource,omitempty"` // [Optional] GCP kind, e.g. `compute//disk` Kind string `yaml:"kind,omitempty"` // [Optional] If set to true, indicates that a resource is not configurable // such as GCP regions. Readonly bool `yaml:"readonly,omitempty"` // ==================== // Terraform Overrides // ==================== // [Optional] If non-empty, overrides the full filename prefix // i.e. google/resource_product_{{resource_filename_override}}.go // i.e. google/resource_product_{{resource_filename_override}}_test.go FilenameOverride string `yaml:"filename_override,omitempty"` // If non-empty, overrides the full given resource name. // i.e. 'google_project' for resourcemanager.Project // Use Provider::Terraform::Config.legacy_name to override just // product name. // Note: This should not be used for vanity names for new products. // This was added to handle preexisting handwritten resources that // don't match the natural generated name exactly, and to support // services with a mix of handwritten and generated resources. LegacyName string `yaml:"legacy_name,omitempty"` // The Terraform resource id format used when calling //setId(...). // For instance, `{{name}}` means the id will be the resource name. IdFormat string `yaml:"id_format,omitempty"` // Override attribute used to handwrite the formats for generating regex strings // that match templated values to a self_link when importing, only necessary when // a resource is not adequately covered by the standard provider generated options. // Leading a token with `%` // i.e. {{%parent}}/resource/{{resource}} // will allow that token to hold multiple /'s. // // Expected to be formatted as follows: // // import_format: // - example_import_one // - example_import_two // ImportFormat []string `yaml:"import_format,omitempty"` CustomCode resource.CustomCode `yaml:"custom_code,omitempty"` Docs resource.Docs `yaml:"docs,omitempty"` // This block inserts entries into the customdiff.All() block in the // resource schema -- the code for these custom diff functions must // be included in the resource constants or come from tpgresource CustomDiff []string `yaml:"custom_diff,omitempty"` // Lock name for a mutex to prevent concurrent API calls for a given // resource. Mutex string `yaml:"mutex,omitempty"` // Examples in documentation. Backed by generated tests, and have // corresponding OiCS walkthroughs. Examples []resource.Examples // If true, generates product operation handling logic. AutogenAsync bool `yaml:"autogen_async,omitempty"` // If true, resource is not importable ExcludeImport bool `yaml:"exclude_import,omitempty"` // If true, exclude resource from Terraform Validator // (i.e. terraform-provider-conversion) ExcludeTgc bool `yaml:"exclude_tgc,omitempty"` // If true, skip sweeper generation for this resource ExcludeSweeper bool `yaml:"exclude_sweeper,omitempty"` // Override sweeper settings Sweeper resource.Sweeper `yaml:"sweeper,omitempty"` Timeouts *Timeouts `yaml:"timeouts,omitempty"` // An array of function names that determine whether an error is retryable. ErrorRetryPredicates []string `yaml:"error_retry_predicates,omitempty"` // An array of function names that determine whether an error is not retryable. ErrorAbortPredicates []string `yaml:"error_abort_predicates,omitempty"` // Optional attributes for declaring a resource's current version and generating // state_upgrader code to the output .go file from files stored at // mmv1/templates/terraform/state_migrations/ // used for maintaining state stability with resources first provisioned on older api versions. SchemaVersion int `yaml:"schema_version,omitempty"` // From this schema version on, state_upgrader code is generated for the resource. // When unset, state_upgrade_base_schema_version defauts to 0. // Normally, it is not needed to be set. StateUpgradeBaseSchemaVersion int `yaml:"state_upgrade_base_schema_version,omitempty"` StateUpgraders bool `yaml:"state_upgraders,omitempty"` // Do not apply the default attribution label ExcludeAttributionLabel bool `yaml:"exclude_attribution_label,omitempty"` // This block inserts the named function and its attribute into the // resource schema -- the code for the migrate_state function must // be included in the resource constants or come from tpgresource // included for backwards compatibility as an older state migration method // and should not be used for new resources. MigrateState string `yaml:"migrate_state,omitempty"` // Set to true for resources that are unable to be deleted, such as KMS keyrings or project // level resources such as firebase project ExcludeDelete bool `yaml:"exclude_delete,omitempty"` // Set to true for resources that are unable to be read from the API, such as // public ca external account keys ExcludeRead bool `yaml:"exclude_read,omitempty"` // Set to true for resources that wish to disable automatic generation of default provider // value customdiff functions // TODO rewrite: 1 instance used ExcludeDefaultCdiff bool `yaml:"exclude_default_cdiff,omitempty"` // This enables resources that get their project via a reference to a different resource // instead of a project field to use User Project Overrides SupportsIndirectUserProjectOverride bool `yaml:"supports_indirect_user_project_override,omitempty"` // If true, the resource's project field can be specified as either the short form project // id or the long form projects/project-id. The extra projects/ string will be removed from // urls and ids. This should only be used for resources that previously supported long form // project ids for backwards compatibility. LegacyLongFormProject bool `yaml:"legacy_long_form_project,omitempty"` // Function to transform a read error so that handleNotFound recognises // it as a 404. This should be added as a handwritten fn that takes in // an error and returns one. ReadErrorTransform string `yaml:"read_error_transform,omitempty"` // If true, resources that failed creation will be marked as tainted. As a consequence // these resources will be deleted and recreated on the next apply call. This pattern // is preferred over deleting the resource directly in post_create_failure hooks. TaintResourceOnFailedCreate bool `yaml:"taint_resource_on_failed_create,omitempty"` // Add a deprecation message for a resource that's been deprecated in the API. DeprecationMessage string `yaml:"deprecation_message,omitempty"` Async *Async // Tag autogen resources so that we can track them. In the future this will // control if a resource is continuously generated from public OpenAPI docs AutogenStatus string `yaml:"autogen_status"` // The three groups of []*Type fields are expected to be strictly ordered within a yaml file // in the sequence of Virtual Fields -> Parameters -> Properties // Virtual fields are Terraform-only fields that control Terraform's // behaviour. They don't map to underlying API fields (although they // may map to parameters), and will require custom code to be added to // control them. // // Virtual fields are similar to url_param_only fields in that they create // a schema entry which is not read from or submitted to the API. However // virtual fields are meant to provide toggles for Terraform-specific behavior in a resource // (eg: delete_contents_on_destroy) whereas url_param_only fields _should_ // be used for url construction. // // Both are resource level fields and do not make sense, and are also not // supported, for nested fields. Nested fields that shouldn't be included // in API payloads are better handled with custom expand/encoder logic. VirtualFields []*Type `yaml:"virtual_fields,omitempty"` Parameters []*Type Properties []*Type ProductMetadata *Product `yaml:"-"` // The version name provided by the user through CI TargetVersionName string `yaml:"-"` // The compiler to generate the downstream files, for example "terraformgoogleconversion-codegen". Compiler string `yaml:"-"` // The API "resource type kind" used for this resource e.g., "Function". // If this is not set, then :name is used instead, which is strongly // preferred wherever possible. Its main purpose is for supporting // fine-grained resources and legacy resources. ApiResourceTypeKind string `yaml:"api_resource_type_kind,omitempty"` // The API URL patterns used by this resource that represent variants e.g., // "folders/{folder}/feeds/{feed}". Each pattern must match the value // defined in the API exactly. The use of `api_variant_patterns` is only // meaningful when the resource type has multiple parent types available. // This is commonly used for resources that have a project, folder, and // organization variant, however most resources do not need it. ApiVariantPatterns []string `yaml:"api_variant_patterns,omitempty"` ImportPath string `yaml:"-"` SourceYamlFile string `yaml:"-"` } func (r *Resource) UnmarshalYAML(unmarshal func(any) error) error { type resourceAlias Resource aliasObj := (*resourceAlias)(r) err := unmarshal(aliasObj) if err != nil { return err } return nil } func (r *Resource) SetDefault(product *Product) { if r.CreateVerb == "" { r.CreateVerb = "POST" } if r.ReadVerb == "" { r.ReadVerb = "GET" } if r.DeleteVerb == "" { r.DeleteVerb = "DELETE" } if r.UpdateVerb == "" { r.UpdateVerb = "PUT" } if r.ApiName == "" { r.ApiName = r.Name } if r.CollectionUrlKey == "" { r.CollectionUrlKey = google.Camelize(google.Plural(r.Name), "lower") } if r.IdFormat == "" { r.IdFormat = r.SelfLinkUri() } if len(r.VirtualFields) > 0 { for _, f := range r.VirtualFields { f.ClientSide = true } } r.ProductMetadata = product for _, property := range r.AllProperties() { property.SetDefault(r) } for _, vf := range r.VirtualFields { vf.SetDefault(r) } if r.IamPolicy != nil && r.IamPolicy.MinVersion == "" { r.IamPolicy.MinVersion = r.MinVersion } if r.Timeouts == nil { r.Timeouts = NewTimeouts() } } func (r *Resource) Validate() { if r.Name == "" { log.Fatalf("Missing `name` for resource") } if r.NestedQuery != nil && r.NestedQuery.IsListOfIds && len(r.Identity) != 1 { log.Fatalf("`is_list_of_ids: true` implies resource has exactly one `identity` property") } // Ensures we have all properties defined for _, i := range r.Identity { hasIdentify := slices.ContainsFunc(r.AllUserProperties(), func(p *Type) bool { return p.Name == i }) if !hasIdentify { log.Fatalf("Missing property/parameter for identity %s", i) } } if r.Description == "" { log.Fatalf("Missing `description` for resource %s", r.Name) } if !r.Exclude { if len(r.Properties) == 0 { log.Fatalf("Missing `properties` for resource %s", r.Name) } } allowed := []string{"POST", "PUT", "PATCH"} if !slices.Contains(allowed, r.CreateVerb) { log.Fatalf("Value on `create_verb` should be one of %#v", allowed) } allowed = []string{"GET", "POST"} if !slices.Contains(allowed, r.ReadVerb) { log.Fatalf("Value on `read_verb` should be one of %#v", allowed) } allowed = []string{"POST", "PUT", "PATCH", "DELETE"} if !slices.Contains(allowed, r.DeleteVerb) { log.Fatalf("Value on `delete_verb` should be one of %#v", allowed) } allowed = []string{"POST", "PUT", "PATCH"} if !slices.Contains(allowed, r.UpdateVerb) { log.Fatalf("Value on `update_verb` should be one of %#v", allowed) } for _, property := range r.AllProperties() { property.Validate(r.Name) } if r.IamPolicy != nil { r.IamPolicy.Validate(r.Name) } if r.NestedQuery != nil { r.NestedQuery.Validate(r.Name) } for _, example := range r.Examples { example.Validate(r.Name) } if r.Async != nil { r.Async.Validate() } } // ==================== // Custom Getters and Setters // ==================== // Returns all properties and parameters including the ones that are // excluded. This is used for PropertyOverride validation func (r Resource) AllProperties() []*Type { return google.Concat(r.Properties, r.Parameters) } func (r Resource) AllPropertiesInVersion() []*Type { return google.Reject(google.Concat(r.Properties, r.Parameters), func(p *Type) bool { return p.Exclude }) } func (r Resource) PropertiesWithExcluded() []*Type { return r.Properties } func (r Resource) UserProperites() []*Type { return google.Reject(r.Properties, func(p *Type) bool { return p.Exclude }) } func (r Resource) UserParameters() []*Type { return google.Reject(r.Parameters, func(p *Type) bool { return p.Exclude }) } func (r Resource) UserVirtualFields() []*Type { return google.Reject(r.VirtualFields, func(p *Type) bool { return p.Exclude }) } func (r Resource) ServiceVersion() string { if r.CaiBaseUrl != "" { return extractVersionFromBaseUrl(r.CaiBaseUrl) } return extractVersionFromBaseUrl(r.BaseUrl) } func extractVersionFromBaseUrl(baseUrl string) string { parts := strings.Split(baseUrl, "/") // starts with v... if parts[0] != "" && parts[0][0] == 'v' { return parts[0] } // starts with /v... if parts[0] == "" && parts[1][0] == 'v' { return parts[1] } return "" } // Return the user-facing properties in client tools; this ends up meaning // both properties and parameters but without any that are excluded due to // version mismatches or manual exclusion func (r Resource) AllUserProperties() []*Type { return google.Concat(r.UserProperites(), r.UserParameters()) } func (r Resource) RequiredProperties() []*Type { return google.Select(r.AllUserProperties(), func(p *Type) bool { return p.Required }) } func (r Resource) AllNestedProperties(props []*Type) []*Type { nested := props for _, prop := range props { if nestedProperties := prop.NestedProperties(); !prop.FlattenObject && nestedProperties != nil { nested = google.Concat(nested, r.AllNestedProperties(nestedProperties)) } } return nested } func (r Resource) SensitiveProps() []*Type { props := r.AllNestedProperties(r.RootProperties()) return google.Select(props, func(p *Type) bool { return p.Sensitive }) } func (r Resource) WriteOnlyProps() []*Type { props := r.AllNestedProperties(r.RootProperties()) return google.Select(props, func(p *Type) bool { return p.WriteOnly }) } func (r Resource) SensitivePropsToString() string { var props []string for _, prop := range r.SensitiveProps() { props = append(props, fmt.Sprintf("`%s`", prop.Lineage())) } return strings.Join(props, ", ") } func (r Resource) WriteOnlyPropsToString() string { var props []string for _, prop := range r.WriteOnlyProps() { props = append(props, fmt.Sprintf("`%s`", prop.Lineage())) } return strings.Join(props, ", ") } // All settable properties in the resource. // Fingerprints aren't *really" settable properties, but they behave like one. // At Create, they have no value but they can just be read in anyways, and after a Read // they will need to be set in every Update. func (r Resource) SettableProperties() []*Type { props := make([]*Type, 0) props = google.Reject(r.AllUserProperties(), func(v *Type) bool { return v.Output && !v.IsA("Fingerprint") && !v.IsA("KeyValueEffectiveLabels") }) props = google.Reject(props, func(v *Type) bool { return v.UrlParamOnly }) props = google.Reject(props, func(v *Type) bool { return v.IsA("KeyValueLabels") || v.IsA("KeyValueAnnotations") }) return props } func (r Resource) IsSettableProperty(t *Type) bool { return slices.Contains(r.SettableProperties(), t) } func (r Resource) UnorderedListProperties() []*Type { return google.Select(r.SettableProperties(), func(t *Type) bool { return t.UnorderedList }) } // Properties that will be returned in the API body func (r Resource) GettableProperties() []*Type { return google.Reject(r.AllUserProperties(), func(v *Type) bool { return v.UrlParamOnly }) } // Returns the list of top-level properties once any nested objects with flatten_object // set to true have been collapsed func (r Resource) RootProperties() []*Type { props := make([]*Type, 0) for _, p := range r.AllUserProperties() { if p.FlattenObject { props = google.Concat(props, p.RootProperties()) } else { props = append(props, p) } } return props } // Returns a sorted list of all "leaf" properties, meaning properties that have // no children. func (r Resource) LeafProperties() []*Type { types := r.AllNestedProperties(google.Concat(r.RootProperties(), r.UserVirtualFields())) // Remove types that have children, because we only want "leaf" fields types = slices.DeleteFunc(types, func(t *Type) bool { nestedProperties := t.NestedProperties() return len(nestedProperties) > 0 }) // Sort types by lineage slices.SortFunc(types, func(a, b *Type) int { if a.MetadataLineage() < b.MetadataLineage() { return -1 } return 1 }) return types } // Return the product-level async object, or the resource-specific one // if one exists. func (r Resource) GetAsync() *Async { if r.Async != nil { return r.Async } return r.ProductMetadata.Async } // Return the resource-specific identity properties, or a best guess of the // `name` value for the resource. func (r Resource) GetIdentity() []*Type { props := r.AllUserProperties() if r.Identity != nil { identities := google.Select(props, func(p *Type) bool { return slices.Contains(r.Identity, p.Name) }) slices.SortFunc(identities, func(a, b *Type) int { return slices.Index(r.Identity, a.Name) - slices.Index(r.Identity, b.Name) }) return identities } return google.Select(props, func(p *Type) bool { return p.Name == "name" }) } func (r *Resource) AddLabelsRelatedFields(props []*Type, parent *Type) []*Type { for _, p := range props { if p.IsA("KeyValueLabels") { props = r.addLabelsFields(props, parent, p) } else if p.IsA("KeyValueAnnotations") { props = r.addAnnotationsFields(props, parent, p) } else if p.IsA("NestedObject") && len(p.AllProperties()) > 0 { p.Properties = r.AddLabelsRelatedFields(p.AllProperties(), p) } } return props } func (r *Resource) addLabelsFields(props []*Type, parent *Type, labels *Type) []*Type { if parent == nil || parent.FlattenObject { if r.ExcludeAttributionLabel { r.CustomDiff = append(r.CustomDiff, "tpgresource.SetLabelsDiffWithoutAttributionLabel") } else { r.CustomDiff = append(r.CustomDiff, "tpgresource.SetLabelsDiff") } } else if parent.Name == "metadata" { r.CustomDiff = append(r.CustomDiff, "tpgresource.SetMetadataLabelsDiff") } terraformLabelsField := buildTerraformLabelsField("labels", parent, labels) effectiveLabelsField := buildEffectiveLabelsField("labels", labels) props = append(props, terraformLabelsField, effectiveLabelsField) // The effective_labels field is used to write to API, instead of the labels field. labels.IgnoreWrite = true labels.Description = fmt.Sprintf("%s\n\n%s", labels.Description, getLabelsFieldNote(labels.Name)) if parent == nil { labels.Immutable = false } return props } func (r *Resource) HasLabelsField() bool { for _, p := range r.Properties { if p.Name == "labels" { return true } } return false } func (r *Resource) addAnnotationsFields(props []*Type, parent *Type, annotations *Type) []*Type { // The effective_annotations field is used to write to API, // instead of the annotations field. annotations.IgnoreWrite = true annotations.Description = fmt.Sprintf("%s\n\n%s", annotations.Description, getLabelsFieldNote(annotations.Name)) if parent == nil { r.CustomDiff = append(r.CustomDiff, "tpgresource.SetAnnotationsDiff") } else if parent.Name == "metadata" { r.CustomDiff = append(r.CustomDiff, "tpgresource.SetMetadataAnnotationsDiff") } effectiveAnnotationsField := buildEffectiveLabelsField("annotations", annotations) props = append(props, effectiveAnnotationsField) return props } func buildEffectiveLabelsField(name string, labels *Type) *Type { description := fmt.Sprintf("All of %s (key/value pairs) present on the resource in GCP, "+ "including the %s configured through Terraform, other clients and services.", name, name) t := "KeyValueEffectiveLabels" n := fmt.Sprintf("effective%s", strings.Title(name)) options := []func(*Type){ propertyWithType(t), propertyWithOutput(true), propertyWithDescription(description), propertyWithMinVersion(labels.fieldMinVersion()), propertyWithUpdateVerb(labels.UpdateVerb), propertyWithUpdateUrl(labels.UpdateUrl), propertyWithImmutable(labels.Immutable), } return NewProperty(n, name, options) } func buildTerraformLabelsField(name string, parent *Type, labels *Type) *Type { description := fmt.Sprintf("The combination of %s configured directly on the resource\n"+ " and default %s configured on the provider.", name, name) immutable := false if parent != nil { immutable = labels.Immutable } n := fmt.Sprintf("terraform%s", strings.Title(name)) options := []func(*Type){ propertyWithType("KeyValueTerraformLabels"), propertyWithOutput(true), propertyWithDescription(description), propertyWithMinVersion(labels.fieldMinVersion()), propertyWithIgnoreWrite(true), propertyWithUpdateUrl(labels.UpdateUrl), propertyWithImmutable(immutable), } return NewProperty(n, name, options) } // Check if the resource has root "labels" field func (r Resource) RootLabels() bool { for _, p := range r.RootProperties() { if p.IsA("KeyValueLabels") { return true } } return false } // Return labels fields that should be added to ImportStateVerifyIgnore func (r Resource) IgnoreReadLabelsFields(props []*Type) []string { fields := make([]string, 0) for _, p := range props { if p.IsA("KeyValueLabels") || p.IsA("KeyValueTerraformLabels") || p.IsA("KeyValueAnnotations") { fields = append(fields, p.TerraformLineage()) } else if p.IsA("NestedObject") && len(p.AllProperties()) > 0 { fields = google.Concat(fields, r.IgnoreReadLabelsFields(p.AllProperties())) } } return fields } func getLabelsFieldNote(title string) string { return fmt.Sprintf( "**Note**: This field is non-authoritative, and will only manage the %s present "+ "in your configuration.\n"+ "Please refer to the field `effective_%s` for all of the %s present on the resource.", title, title, title) } func (r Resource) StateMigrationFile() string { return fmt.Sprintf("templates/terraform/state_migrations/%s_%s.go.tmpl", google.Underscore(r.ProductMetadata.Name), google.Underscore(r.Name)) } // ==================== // Version-related methods // ==================== func (r Resource) MinVersionObj() *product.Version { if r.MinVersion != "" { return r.ProductMetadata.versionObj(r.MinVersion) } else { return r.ProductMetadata.lowestVersion() } } func (r Resource) NotInVersion(version *product.Version) bool { return version.CompareTo(r.MinVersionObj()) < 0 } // Recurses through all nested properties and parameters and changes their // 'exclude' instance variable if the property is at a version below the // one that is passed in. func (r *Resource) ExcludeIfNotInVersion(version *product.Version) { if !r.Exclude { r.Exclude = r.NotInVersion(version) } if r.Properties != nil { for _, p := range r.Properties { p.ExcludeIfNotInVersion(version) } } if r.Parameters != nil { for _, p := range r.Parameters { p.ExcludeIfNotInVersion(version) } } } // ==================== // URL-related methods // ==================== // Returns the "self_link_url" which is generally really the resource's GET // URL. In older resources generally, this was the self_link value & was the // product.base_url + resource.base_url + '/name' // In newer resources there is much less standardisation in terms of value. // Generally for them though, it's the product.base_url + resource.name func (r Resource) SelfLinkUrl() string { s := []string{r.ProductMetadata.BaseUrl, r.SelfLinkUri()} return strings.Join(s, "") } // Returns the partial uri / relative path of a resource. In newer resources, // this is the name. This fn is named self_link_uri for consistency, but // could otherwise be considered to be "path" func (r Resource) SelfLinkUri() string { // If the terms in this are not snake-cased, this will require // an override in Terraform. if r.SelfLink != "" { return r.SelfLink } return strings.Join([]string{r.BaseUrl, "{{name}}"}, "/") } func (r Resource) CollectionUrl() string { s := []string{r.ProductMetadata.BaseUrl, r.collectionUri()} return strings.Join(s, "") } func (r Resource) collectionUri() string { return r.BaseUrl } func (r Resource) CreateUri() string { if r.CreateUrl != "" { return r.CreateUrl } if r.CreateVerb == "" || r.CreateVerb == "POST" { return r.collectionUri() } return r.SelfLinkUri() } func (r Resource) UpdateUri() string { if r.UpdateUrl != "" { return r.UpdateUrl } return r.SelfLinkUri() } func (r Resource) DeleteUri() string { if r.DeleteUrl != "" { return r.DeleteUrl } return r.SelfLinkUri() } func (r Resource) ResourceName() string { return fmt.Sprintf("%s%s", r.ProductMetadata.Name, r.Name) } // Filter the properties to keep only the ones don't have custom update // method and group them by update url & verb. func propertiesWithoutCustomUpdate(properties []*Type) []*Type { return google.Select(properties, func(p *Type) bool { return p.UpdateUrl == "" || p.UpdateVerb == "" || p.UpdateVerb == "NOOP" }) } func (r Resource) UpdateBodyProperties() []*Type { updateProp := propertiesWithoutCustomUpdate(r.SettableProperties()) if r.UpdateVerb == "PATCH" { updateProp = google.Reject(updateProp, func(p *Type) bool { return p.Immutable }) } return updateProp } // Handwritten TF Operation objects will be shaped like accessContextManager // while the Google Go Client will have a name like accesscontextmanager func (r Resource) ClientNamePascal() string { clientName := r.ProductMetadata.ClientName if clientName == "" { clientName = r.ProductMetadata.Name } return google.Camelize(clientName, "upper") } func (r Resource) PackageName() string { return strings.ToLower(r.ProductMetadata.Name) } // In order of preference, use TF override, // general defined timeouts, or default Timeouts func (r Resource) GetTimeouts() *Timeouts { timeoutsFiltered := r.Timeouts if timeoutsFiltered == nil { if async := r.GetAsync(); async != nil && async.Operation != nil { timeoutsFiltered = async.Operation.Timeouts } if timeoutsFiltered == nil { timeoutsFiltered = NewTimeouts() } } return timeoutsFiltered } func (r Resource) HasProject() bool { return strings.Contains(r.BaseUrl, "{{project}}") || strings.Contains(r.CreateUrl, "{{project}}") } func (r Resource) IncludeProjectForOperation() bool { return strings.Contains(r.BaseUrl, "{{project}}") || (r.GetAsync().IsA("OpAsync") && r.GetAsync().IncludeProject) } func (r Resource) HasRegion() bool { found := false for _, p := range r.Parameters { if p.Name == "region" && p.IgnoreRead { found = true break } } return found && strings.Contains(r.BaseUrl, "{{region}}") } func (r Resource) HasZone() bool { found := false for _, p := range r.Parameters { if p.Name == "zone" && p.IgnoreRead { found = true break } } return found && strings.Contains(r.BaseUrl, "{{zone}}") } // resource functions needed for template that previously existed in terraform.go // but due to how files are being inherited here it was easier to put in here // taken wholesale from tpgtools func (r Resource) Updatable() bool { if !r.Immutable { return true } for _, p := range r.AllPropertiesInVersion() { if p.UpdateUrl != "" { return true } } return false } // ==================== // Debugging Methods // ==================== // Prints a dot notation path to where the field is nested within the parent // object when called on a property. eg: parent.meta.label.foo // Redefined on Resource to terminate the calls up the parent chain. func (r Resource) Lineage() string { return r.Name } func (r Resource) TerraformName() string { if r.LegacyName != "" { return r.LegacyName } return fmt.Sprintf("google_%s_%s", r.ProductMetadata.TerraformName(), google.Underscore(r.Name)) } func (r Resource) ImportIdFormatsFromResource() []string { return ImportIdFormats(r.ImportFormat, r.Identity, r.BaseUrl) } // Returns a list of import id formats for a given resource. If an id // contains provider-default values, this fn will return formats both // including and omitting the value. // // If a resource has an explicit import_format value set, that will be the // base import url used. Next, the values of `identity` will be used to // construct a URL. Finally, `{{name}}` will be used by default. // // For instance, if the resource base url is: // // projects/{{project}}/global/networks // // It returns 3 formats: // a) self_link: projects/{{project}}/global/networks/{{name}} // b) short id: {{project}}/{{name}} // c) short id w/o defaults: {{name}} func ImportIdFormats(importFormat, identity []string, baseUrl string) []string { var idFormats []string if len(importFormat) == 0 { underscoredBaseUrl := baseUrl if len(identity) == 0 { idFormats = []string{fmt.Sprintf("%s/{{name}}", underscoredBaseUrl)} } else { var transformedIdentity []string for _, id := range identity { transformedIdentity = append(transformedIdentity, fmt.Sprintf("{{%s}}", id)) } identityPath := strings.Join(transformedIdentity, "/") idFormats = []string{fmt.Sprintf("%s/%s", underscoredBaseUrl, google.Underscore(identityPath))} } } else { idFormats = importFormat } // short id: {{project}}/{{zone}}/{{name}} fieldMarkers := regexp.MustCompile(`{{[[:word:]]+}}`).FindAllString(idFormats[0], -1) shortIdFormat := strings.Join(fieldMarkers, "/") // short ids without fields with provider-level defaults: // without project fieldMarkers = slices.DeleteFunc(fieldMarkers, func(s string) bool { return s == "{{project}}" }) shortIdDefaultProjectFormat := strings.Join(fieldMarkers, "/") // without project or location fieldMarkers = slices.DeleteFunc(fieldMarkers, func(s string) bool { return s == "{{region}}" }) fieldMarkers = slices.DeleteFunc(fieldMarkers, func(s string) bool { return s == "{{zone}}" }) shortIdDefaultFormat := strings.Join(fieldMarkers, "/") // If the id format can include `/` characters we cannot allow short forms such as: // `{{project}}/{{%name}}` as there is no way to differentiate between // project-name/resource-name and resource-name/with-slash if !strings.Contains(idFormats[0], "%") { idFormats = append(idFormats, shortIdFormat, shortIdDefaultProjectFormat, shortIdDefaultFormat) } slices.SortFunc(idFormats, func(a, b string) int { i := strings.Count(a, "/") j := strings.Count(b, "/") if i == j { return strings.Count(a, "{{") - strings.Count(b, "{{") } return i - j }) slices.Reverse(idFormats) // Remove duplicates from idFormats uniq := make([]string, len(idFormats)) uniq[0] = idFormats[0] i := 1 j := 1 for j < len(idFormats) { format := idFormats[j] if format != uniq[i-1] { uniq[i] = format i++ } j++ } uniq = google.Reject(slices.Compact(uniq), func(i string) bool { return i == "" }) return uniq } func (r Resource) IgnoreReadPropertiesToString(e resource.Examples) string { var props []string for _, tp := range r.AllUserProperties() { if tp.UrlParamOnly || tp.IsA("ResourceRef") { props = append(props, fmt.Sprintf("\"%s\"", google.Underscore(tp.Name))) } } for _, tp := range e.IgnoreReadExtra { props = append(props, fmt.Sprintf("\"%s\"", tp)) } for _, tp := range r.IgnoreReadLabelsFields(r.PropertiesWithExcluded()) { props = append(props, fmt.Sprintf("\"%s\"", tp)) } for _, tp := range ignoreReadFields(r.AllUserProperties()) { props = append(props, fmt.Sprintf("\"%s\"", tp)) } slices.Sort(props) if len(props) > 0 { return fmt.Sprintf("[]string{%s}", strings.Join(props, ", ")) } return "" } func ignoreReadFields(props []*Type) []string { var fields []string for _, tp := range props { if tp.IgnoreRead && !tp.UrlParamOnly && !tp.IsA("ResourceRef") { fields = append(fields, tp.TerraformLineage()) } else if tp.IsA("NestedObject") && tp.AllProperties() != nil { fields = append(fields, ignoreReadFields(tp.AllProperties())...) } } return fields } func (r *Resource) SetCompiler(t string) { r.Compiler = fmt.Sprintf("%s-codegen", strings.ToLower(t)) } // Returns the id format of an object, or self_link_uri if none is explicitly defined // We prefer the long name of a resource as the id so that users can reference // resources in a standard way, and most APIs accept short name, long name or self_link func (r Resource) GetIdFormat() string { idFormat := r.IdFormat if idFormat == "" { idFormat = r.SelfLinkUri() } return idFormat } // Returns true if the Type is in the ID format and false otherwise. func (r Resource) InIdFormat(prop Type) bool { fields := r.ExtractIdentifiers(r.GetIdFormat()) return slices.Contains(fields, google.Underscore(prop.Name)) } // Returns true if at least one of the fields in the ID format is computed func (r Resource) HasComputedIdFormatFields() bool { idFormatFields := map[string]struct{}{} for _, f := range r.ExtractIdentifiers(r.GetIdFormat()) { idFormatFields[f] = struct{}{} } for _, p := range r.GettableProperties() { // Skip fields not in the id format if _, ok := idFormatFields[google.Underscore(p.Name)]; !ok { continue } if (p.Output || p.DefaultFromApi) && !p.IgnoreRead { return true } } return false } // ==================== // Template Methods // ==================== // Functions used to create slices of resource properties that could not otherwise be called from within generating templates. func (r Resource) ReadProperties() []*Type { return google.Reject(r.GettableProperties(), func(p *Type) bool { return p.IgnoreRead }) } func (r Resource) FlattenedProperties() []*Type { return google.Select(r.ReadProperties(), func(p *Type) bool { return p.FlattenObject }) } func (r Resource) IsInIdentity(t Type) bool { for _, i := range r.GetIdentity() { if i.Name == t.Name { return true } } return false } // ==================== // Iam Methods // ==================== func (r Resource) IamParentResourceName() string { var parentResourceName string if r.IamPolicy != nil { parentResourceName = r.IamPolicy.ParentResourceAttribute } if parentResourceName == "" { parentResourceName = google.Underscore(r.Name) } return parentResourceName } // For example: "projects/{{project}}/schemas/{{name}}" func (r Resource) IamResourceUri() string { var resourceUri string if r.IamPolicy != nil { resourceUri = r.IamPolicy.BaseUrl } if resourceUri == "" { resourceUri = r.SelfLinkUri() } return resourceUri } // For example: "projects/%s/schemas/%s" func (r Resource) IamResourceUriFormat() string { return regexp.MustCompile(`\{\{%?(\w+)\}\}`).ReplaceAllString(r.IamResourceUri(), "%s") } // For example: the uri "projects/{{project}}/schemas/{{name}}" // The paramerters are "project", "schema". func (r Resource) IamResourceParams() []string { resourceUri := strings.ReplaceAll(r.IamResourceUri(), "{{name}}", fmt.Sprintf("{{%s}}", r.IamParentResourceName())) return r.ExtractIdentifiers(resourceUri) } func (r Resource) IsInIamResourceParams(param string) bool { return slices.Contains(r.IamResourceParams(), param) } // For example: for the uri "projects/{{project}}/schemas/{{name}}", // the string qualifiers are "u.project, u.schema" func (r Resource) IamResourceUriStringQualifiers() string { var transformed []string for _, param := range r.IamResourceParams() { transformed = append(transformed, fmt.Sprintf("u.%s", google.Camelize(param, "lower"))) } return strings.Join(transformed[:], ", ") } // For example, for the url "projects/{{project}}/schemas/{{schema}}", // the identifiers are "project", "schema". func (r Resource) ExtractIdentifiers(url string) []string { matches := regexp.MustCompile(`\{\{%?(\w+)\}\}`).FindAllStringSubmatch(url, -1) var result []string for _, match := range matches { result = append(result, match[1]) } return result } func (r Resource) IamImportFormats() []string { var importFormat []string if r.IamPolicy != nil { importFormat = r.IamPolicy.ImportFormat } if len(importFormat) == 0 { importFormat = r.ImportFormat } return importFormat } // For example, "projects/{{project}}/schemas/{{name}}", "{{project}}/{{name}}", "{{name}}" func (r Resource) RawImportIdFormatsFromIam() []string { return ImportIdFormats(r.IamImportFormats(), r.Identity, r.BaseUrl) } // For example, projects/(?P<project>[^/]+)/schemas/(?P<schema>[^/]+)", "(?P<project>[^/]+)/(?P<schema>[^/]+)", "(?P<schema>[^/]+) func (r Resource) ImportIdRegexesFromIam() string { var transformed []string importIdFormats := r.RawImportIdFormatsFromIam() for _, s := range importIdFormats { s = google.Format2Regex(s) s = strings.ReplaceAll(s, "<name>", fmt.Sprintf("<%s>", r.IamParentResourceName())) transformed = append(transformed, s) } return strings.Join(slices.Compact(transformed[:]), "\", \"") } // For example, "projects/{{project}}/schemas/{{name}}", "{{project}}/{{name}}", "{{name}}" func (r Resource) ImportIdFormatsFromIam() []string { importIdFormats := r.RawImportIdFormatsFromIam() var transformed []string for _, s := range importIdFormats { transformed = append(transformed, strings.ReplaceAll(s, "%", "")) } return transformed } // For example, projects/{{project}}/schemas/{{schema}} func (r Resource) FirstIamImportIdFormat() string { importIdFormats := r.ImportIdFormatsFromIam() if len(importIdFormats) == 0 { return "" } first := importIdFormats[0] first = strings.ReplaceAll(first, "{{name}}", fmt.Sprintf("{{%s}}", google.Underscore(r.Name))) return first } func (r Resource) IamTerraformName() string { return fmt.Sprintf("%s_iam", r.TerraformName()) } func (r Resource) IamSelfLinkIdentifiers() []string { var selfLink string if r.IamPolicy != nil { selfLink = r.IamPolicy.SelfLink } if selfLink == "" { selfLink = r.SelfLinkUrl() } return r.ExtractIdentifiers(selfLink) } // Returns the resource properties that are idenfifires in the selflink url func (r Resource) IamSelfLinkProperties() []*Type { params := r.IamSelfLinkIdentifiers() urlProperties := google.Select(r.AllUserProperties(), func(p *Type) bool { return slices.Contains(params, p.Name) }) return urlProperties } // Returns the attributes from the selflink url func (r Resource) IamAttributes() []string { var attributes []string ids := r.IamSelfLinkIdentifiers() for i, p := range ids { var attribute string if i == len(ids)-1 { attribute = r.IamPolicy.ParentResourceAttribute if attribute == "" { attribute = p } } else { attribute = p } attributes = append(attributes, attribute) } return attributes } // Since most resources define a "basic" config as their first example, // we can reuse that config to create a resource to test IAM resources with. func (r Resource) FirstTestExample() resource.Examples { examples := google.Reject(r.Examples, func(e resource.Examples) bool { return e.ExcludeTest }) examples = google.Reject(examples, func(e resource.Examples) bool { return (r.ProductMetadata.VersionObjOrClosest(r.TargetVersionName).CompareTo(r.ProductMetadata.VersionObjOrClosest(e.MinVersion)) < 0) }) return examples[0] } func (r Resource) ExamplePrimaryResourceId() string { examples := google.Reject(r.Examples, func(e resource.Examples) bool { return e.ExcludeTest }) examples = google.Reject(examples, func(e resource.Examples) bool { return (r.ProductMetadata.VersionObjOrClosest(r.TargetVersionName).CompareTo(r.ProductMetadata.VersionObjOrClosest(e.MinVersion)) < 0) }) if len(examples) == 0 { examples = google.Reject(r.Examples, func(e resource.Examples) bool { return (r.ProductMetadata.VersionObjOrClosest(r.TargetVersionName).CompareTo(r.ProductMetadata.VersionObjOrClosest(e.MinVersion)) < 0) }) } return examples[0].PrimaryResourceId } func (r Resource) IamParentSourceType() string { t := r.IamPolicy.ParentResourceType if t == "" { t = r.TerraformName() } return t } func (r Resource) IamImportFormat() string { var importFormat string if len(r.IamPolicy.ImportFormat) > 0 { importFormat = r.IamPolicy.ImportFormat[0] } else { importFormat = r.IamPolicy.SelfLink if importFormat == "" { importFormat = r.SelfLinkUrl() } } importFormat = regexp.MustCompile(`\{\{%?(\w+)\}\}`).ReplaceAllString(importFormat, "%s") return strings.ReplaceAll(importFormat, r.ProductMetadata.BaseUrl, "") } func (r Resource) IamImportQualifiersForTest() string { var importFormat string if len(r.IamPolicy.ImportFormat) > 0 { importFormat = r.IamPolicy.ImportFormat[0] } else { importFormat = r.IamPolicy.SelfLink if importFormat == "" { importFormat = r.SelfLinkUrl() } } params := r.ExtractIdentifiers(importFormat) var importQualifiers []string for i, param := range params { if param == "project" { if i != len(params)-1 { // If the last parameter is project then we want to create a new project to use for the test, so don't default from the environment if r.IamPolicy.TestProjectName == "" { importQualifiers = append(importQualifiers, "envvar.GetTestProjectFromEnv()") } else { importQualifiers = append(importQualifiers, `context["project_id"]`) } } } else if param == "zone" && r.IamPolicy.SubstituteZoneValue { importQualifiers = append(importQualifiers, "envvar.GetTestZoneFromEnv()") } else if param == "region" || param == "location" { example := r.FirstTestExample() if example.RegionOverride == "" { importQualifiers = append(importQualifiers, "envvar.GetTestRegionFromEnv()") } else { importQualifiers = append(importQualifiers, fmt.Sprintf("\"%s\"", example.RegionOverride)) } } else if param == "universe_domain" { importQualifiers = append(importQualifiers, "envvar.GetTestUniverseDomainFromEnv()") } else { break } } if len(importQualifiers) == 0 { return "" } return strings.Join(importQualifiers, ", ") } func (r Resource) OrderProperties(props []*Type) []*Type { req := google.Select(props, func(p *Type) bool { return p.Required }) slices.SortFunc(req, CompareByName) rest := google.Reject(props, func(p *Type) bool { return p.Output || p.Required }) slices.SortFunc(rest, CompareByName) output := google.Select(props, func(p *Type) bool { return p.Output }) slices.SortFunc(output, CompareByName) returnProps := google.Concat(req, rest) return google.Concat(returnProps, output) } func CompareByName(a, b *Type) int { return strings.Compare(a.Name, b.Name) } func (r Resource) GetPropertyUpdateMasksGroupKeys(properties []*Type) []string { keys := []string{} for _, prop := range properties { if prop.FlattenObject { k := r.GetPropertyUpdateMasksGroupKeys(prop.Properties) keys = append(keys, k...) } else { keys = append(keys, google.Underscore(prop.Name)) } } return keys } func (r Resource) GetPropertyUpdateMasksGroups(properties []*Type, maskPrefix string) map[string][]string { maskGroups := map[string][]string{} for _, prop := range properties { if prop.FlattenObject { maps.Copy(maskGroups, r.GetPropertyUpdateMasksGroups(prop.Properties, prop.ApiName+".")) } else if len(prop.UpdateMaskFields) > 0 { maskGroups[google.Underscore(prop.Name)] = prop.UpdateMaskFields } else { maskGroups[google.Underscore(prop.Name)] = []string{maskPrefix + prop.ApiName} } } return maskGroups } // Formats whitespace in the style of the old Ruby generator's descriptions in documentation func (r Resource) FormatDocDescription(desc string, indent bool) string { if desc == "" { return "" } returnString := desc if indent { returnString = strings.ReplaceAll(returnString, "\n\n", "\n") returnString = strings.ReplaceAll(returnString, "\n", "\n ") // fix removing for ruby -> go transition diffs returnString = strings.ReplaceAll(returnString, "\n \n **Note**: This field is non-authoritative,", "\n\n **Note**: This field is non-authoritative,") return fmt.Sprintf("\n %s", strings.TrimSuffix(returnString, "\n ")) } return strings.TrimSuffix(returnString, "\n") } func (r Resource) CustomTemplate(templatePath string, appendNewline bool) string { output := resource.ExecuteTemplate(&r, templatePath, appendNewline) if !appendNewline { output = strings.TrimSuffix(output, "\n") } return output } // Returns the key of the list of resources in the List API response // Used to get the list of resources to sweep func (r Resource) ResourceListKey() string { var k string if r.NestedQuery != nil && len(r.NestedQuery.Keys) > 0 { k = r.NestedQuery.Keys[0] } if k == "" { k = r.CollectionUrlKey } return k } func (r Resource) ListUrlTemplate() string { return strings.Replace(r.CollectionUrl(), "zones/{{zone}}", "aggregated", 1) } func (r Resource) DeleteUrlTemplate() string { return fmt.Sprintf("%s%s", r.ProductMetadata.BaseUrl, r.DeleteUri()) } func (r Resource) LastNestedQueryKey() string { if r.NestedQuery == nil { return "" } len := len(r.NestedQuery.Keys) return r.NestedQuery.Keys[len-1] } func (r Resource) FirstIdentityProp() *Type { idProps := r.GetIdentity() if len(idProps) == 0 { return nil } return idProps[0] } type UpdateGroup struct { UpdateUrl string UpdateVerb string UpdateId string FingerprintName string } func (r Resource) propertiesWithCustomUpdate(properties []*Type) []*Type { return google.Reject(properties, func(p *Type) bool { return p.UpdateUrl == "" || p.UpdateVerb == "" || p.UpdateVerb == "NOOP" || p.IsA("KeyValueTerraformLabels") || p.IsA("KeyValueLabels") }) } func (r Resource) PropertiesByCustomUpdate(properties []*Type) map[UpdateGroup][]*Type { customUpdateProps := r.propertiesWithCustomUpdate(properties) groupedCustomUpdateProps := map[UpdateGroup][]*Type{} for _, prop := range customUpdateProps { groupedProperty := UpdateGroup{UpdateUrl: prop.UpdateUrl, UpdateVerb: prop.UpdateVerb, UpdateId: prop.UpdateId, FingerprintName: prop.FingerprintName} groupedCustomUpdateProps[groupedProperty] = append(groupedCustomUpdateProps[groupedProperty], prop) } return groupedCustomUpdateProps } func (r Resource) PropertiesByCustomUpdateGroups() []UpdateGroup { customUpdateProps := r.propertiesWithCustomUpdate(r.RootProperties()) var updateGroups []UpdateGroup for _, prop := range customUpdateProps { groupedProperty := UpdateGroup{UpdateUrl: prop.UpdateUrl, UpdateVerb: prop.UpdateVerb, UpdateId: prop.UpdateId, FingerprintName: prop.FingerprintName} if slices.Contains(updateGroups, groupedProperty) { continue } updateGroups = append(updateGroups, groupedProperty) } sort.Slice(updateGroups, func(i, j int) bool { a := updateGroups[i] b := updateGroups[j] if a.UpdateVerb != b.UpdateVerb { return a.UpdateVerb > b.UpdateVerb } return a.UpdateId < b.UpdateId }) return updateGroups } func (r Resource) FieldSpecificUpdateMethods() bool { return (len(r.PropertiesByCustomUpdate(r.RootProperties())) > 0) } func (r Resource) CustomUpdatePropertiesByKey(properties []*Type, updateUrl string, updateId string, fingerprintName string, updateVerb string) []*Type { groupedProperties := r.PropertiesByCustomUpdate(properties) groupedProperty := UpdateGroup{UpdateUrl: updateUrl, UpdateVerb: updateVerb, UpdateId: updateId, FingerprintName: fingerprintName} return google.Reject(groupedProperties[groupedProperty], func(p *Type) bool { return p.UrlParamOnly }) } func (r Resource) PropertyNamesToStrings(properties []*Type) []string { var propertyNames []string for _, prop := range properties { propertyNames = append(propertyNames, google.Underscore(prop.Name)) } return propertyNames } func (r Resource) IsExcluded() bool { return r.Exclude || r.ExcludeResource } func (r Resource) TestExamples() []resource.Examples { return google.Reject(google.Reject(r.Examples, func(e resource.Examples) bool { return e.ExcludeTest }), func(e resource.Examples) bool { return e.MinVersion != "" && slices.Index(product.ORDER, r.TargetVersionName) < slices.Index(product.ORDER, e.MinVersion) }) } func (r Resource) VersionedProvider(exampleVersion string) bool { var vp string if exampleVersion != "" { vp = exampleVersion } else if r.MinVersion == "" { vp = r.ProductMetadata.lowestVersion().Name } else { vp = r.MinVersion } return vp != "" && vp != "ga" } func (r Resource) StateUpgradersCount() []int { var nums []int for i := r.StateUpgradeBaseSchemaVersion; i < r.SchemaVersion; i++ { nums = append(nums, i) } return nums } func (r Resource) CaiProductBaseUrl() string { version := r.ProductMetadata.VersionObjOrClosest(r.TargetVersionName) baseUrl := version.CaiBaseUrl if baseUrl == "" { baseUrl = version.BaseUrl } return baseUrl } // Returns the Cai product backend name from the version base url // base_url: https://accessapproval.googleapis.com/v1/ -> accessapproval func (r Resource) CaiProductBackendName(caiProductBaseUrl string) string { backendUrl := strings.Split(strings.Split(caiProductBaseUrl, "://")[1], ".googleapis.com")[0] return strings.ToLower(backendUrl) } // Gets the Cai asset name template, which could include version // For example: //monitoring.googleapis.com/v3/projects/{{project}}/services/{{service_id}} func (r Resource) rawCaiAssetNameTemplate(productBackendName string) string { caiBaseUrl := "" if r.CaiBaseUrl != "" { caiBaseUrl = fmt.Sprintf("%s/{{name}}", r.CaiBaseUrl) } if caiBaseUrl == "" { caiBaseUrl = r.SelfLink } if caiBaseUrl == "" { caiBaseUrl = fmt.Sprintf("%s/{{name}}", r.BaseUrl) } return fmt.Sprintf("//%s.googleapis.com/%s", productBackendName, caiBaseUrl) } // Gets the Cai asset name template, which doesn't include version // For example: //monitoring.googleapis.com/projects/{{project}}/services/{{service_id}} func (r Resource) CaiAssetNameTemplate(productBackendName string) string { template := r.rawCaiAssetNameTemplate(productBackendName) versionRegex, err := regexp.Compile(`\/(v\d[^\/]*)\/`) if err != nil { log.Fatalf("Cannot compile the regular expression: %v", err) } return versionRegex.ReplaceAllString(template, "/") } // Gets the Cai API version func (r Resource) CaiApiVersion(productBackendName, caiProductBaseUrl string) string { template := r.rawCaiAssetNameTemplate(productBackendName) versionRegex, err := regexp.Compile(`\/(v\d[^\/]*)\/`) if err != nil { log.Fatalf("Cannot compile the regular expression: %v", err) } apiVersion := strings.ReplaceAll(versionRegex.FindString(template), "/", "") if apiVersion != "" { return apiVersion } splits := strings.Split(caiProductBaseUrl, "/") for i := 0; i < len(splits); i++ { if splits[len(splits)-1-i] != "" { return splits[len(splits)-1-i] } } return "" } // For example: the uri "projects/{{project}}/schemas/{{name}}" // The paramerter is "schema" as "project" is not returned. func (r Resource) CaiIamResourceParams() []string { resourceUri := strings.ReplaceAll(r.IamResourceUri(), "{{name}}", fmt.Sprintf("{{%s}}", r.IamParentResourceName())) return google.Reject(r.ExtractIdentifiers(resourceUri), func(param string) bool { return param == "project" }) } // Gets the Cai IAM asset name template // For example: //monitoring.googleapis.com/v3/projects/{{project}}/services/{{service_id}} func (r Resource) CaiIamAssetNameTemplate(productBackendName string) string { iamImportFormat := r.IamImportFormats() if len(iamImportFormat) > 0 { name := strings.ReplaceAll(iamImportFormat[0], "{{name}}", fmt.Sprintf("{{%s}}", r.IamParentResourceName())) name = strings.ReplaceAll(name, "%", "") return fmt.Sprintf("//%s.googleapis.com/%s", productBackendName, name) } caiBaseUrl := r.CaiBaseUrl if caiBaseUrl == "" { caiBaseUrl = r.SelfLink } if caiBaseUrl == "" { caiBaseUrl = r.BaseUrl } return fmt.Sprintf("//%s.googleapis.com/%s/{{%s}}", productBackendName, caiBaseUrl, r.IamParentResourceName()) } func urlContainsOnlyAllowedKeys(templateURL string, allowedKeys []string) bool { // Create regex to match anything between {{ and }} re := regexp.MustCompile(`{{\s*([^}]+)\s*}}`) // Find all matches in the template URL matches := re.FindAllStringSubmatch(templateURL, -1) // Create a map of allowed keys for O(1) lookup allowedKeysMap := make(map[string]bool) for _, key := range allowedKeys { allowedKeysMap[key] = true } // Check each found key against the allowed keys for _, match := range matches { if len(match) < 2 { continue } // Trim spaces from the key key := strings.TrimSpace(match[1]) // If the key isn't in our allowed list, return false if !allowedKeysMap[key] { return false } } return true } func (r Resource) ShouldGenerateSweepers() bool { if !r.ExcludeSweeper && !utils.IsEmpty(r.Sweeper) { return true } allowedKeys := []string{"project", "region", "location", "zone", "billing_account"} if !urlContainsOnlyAllowedKeys(r.ListUrlTemplate(), allowedKeys) { return false } if r.ExcludeSweeper || r.CustomCode.CustomDelete != "" || r.CustomCode.PreDelete != "" || r.CustomCode.PostDelete != "" || r.ExcludeDelete { return false } return true } func (r Resource) GithubURL() string { return GITHUB_BASE_URL + r.SourceYamlFile } func (r Resource) CodeHeader(templatePath string) string { templateUrl := GITHUB_BASE_URL + templatePath return fmt.Sprintf(`// ---------------------------------------------------------------------------- // // *** AUTO GENERATED CODE *** Type: MMv1 *** // // ---------------------------------------------------------------------------- // // This code is generated by Magic Modules using the following: // // Configuration: %s // Template: %s // // DO NOT EDIT this file directly. Any changes made to this file will be // overwritten during the next generation cycle. // // ----------------------------------------------------------------------------`, r.GithubURL(), templateUrl) } func (r Resource) MarkdownHeader(templatePath string) string { return strings.Replace(r.CodeHeader(templatePath), "//", "#", -1) }