mmv1/api/resource/examples.go (227 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 resource import ( "bytes" "fmt" "log" "net/url" "path/filepath" "regexp" "slices" "strings" "text/template" "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" "github.com/golang/glog" ) type IamMember struct { Member, Role string } // Generates configs to be shown as examples in docs and outputted as tests // from a shared template type Examples struct { // The name of the example in lower snake_case. // Generally takes the form of the resource name followed by some detail // about the specific test. For example, "address_with_subnetwork". Name string // The id of the "primary" resource in an example. Used in import tests. // This is the value that will appear in the Terraform config url. For // example: // resource "google_compute_address" {{primary_resource_id}} { // ... // } PrimaryResourceId string `yaml:"primary_resource_id"` // Optional resource type of the "primary" resource. Used in import tests. // If set, this will override the default resource type implied from the // object parent PrimaryResourceType string `yaml:"primary_resource_type,omitempty"` // BootstrapIam will automatically bootstrap the given member/role pairs. // This should be used in cases where specific IAM permissions must be // present on the default test project, to avoid race conditions between // tests. Permissions attached to resources created in a test should instead // be provisioned with standard terraform resources. BootstrapIam []IamMember `yaml:"bootstrap_iam,omitempty"` // Vars is a Hash from template variable names to output variable names. // It will use the provided value as a prefix for generated tests, and // insert it into the docs verbatim. Vars map[string]string // Some variables need to hold special values during tests, and cannot // be inferred by Open in Cloud Shell. For instance, org_id // needs to be the correct value during integration tests, or else // org tests cannot pass. Other examples include an existing project_id, // a zone, a service account name, etc. // // test_env_vars is a Hash from template variable names to one of the // following symbols: // - PROJECT_NAME // - CREDENTIALS // - REGION // - ORG_ID // - ORG_TARGET // - BILLING_ACCT // - MASTER_BILLING_ACCT // - SERVICE_ACCT // - CUST_ID // - IDENTITY_USER // - CHRONICLE_ID // - VMWAREENGINE_PROJECT // This list corresponds to the `get*FromEnv` methods in provider_test.go. TestEnvVars map[string]string `yaml:"test_env_vars,omitempty"` // Hash to provider custom override values for generating test config // If field my-var is set in this hash, it will replace vars[my-var] in // tests. i.e. if vars["network"] = "my-vpc", without override: // - doc config will have `network = "my-vpc"` // - tests config will have `"network = my-vpc%{random_suffix}"` // with context // map[string]interface{}{ // "random_suffix": acctest.RandString() // } // // If test_vars_overrides["network"] = "nameOfVpc()" // - doc config will have `network = "my-vpc"` // - tests will replace with `"network = %{network}"` with context // map[string]interface{}{ // "network": nameOfVpc // ... // } TestVarsOverrides map[string]string `yaml:"test_vars_overrides,omitempty"` // Hash to provider custom override values for generating oics config // See test_vars_overrides for more details OicsVarsOverrides map[string]string `yaml:"oics_vars_overrides,omitempty"` // The version name of of the example's version if it's different than the // resource version, eg. `beta` // // This should be the highest version of all the features used in the // example; if there's a single beta field in an example, the example's // min_version is beta. This is only needed if an example uses features // with a different version than the resource; a beta resource's examples // are all automatically versioned at beta. // // When an example has a version of beta, each resource must use the // `google-beta` provider in the config. If the `google` provider is // implicitly used, the test will fail. // // NOTE: Until Terraform 0.12 is released and is used in the OiCS tests, an // explicit provider block should be defined. While the tests @ 0.12 will // use `google-beta` automatically, past Terraform versions required an // explicit block. MinVersion string `yaml:"min_version,omitempty"` // Extra properties to ignore read on during import. // These properties will likely be custom code. IgnoreReadExtra []string `yaml:"ignore_read_extra,omitempty"` // Whether to skip generating tests for this resource ExcludeTest bool `yaml:"exclude_test,omitempty"` // Whether to skip generating docs for this example ExcludeDocs bool `yaml:"exclude_docs,omitempty"` // Whether to skip import tests for this example ExcludeImportTest bool `yaml:"exclude_import_test,omitempty"` // The name of the primary resource for use in IAM tests. IAM tests need // a reference to the primary resource to create IAM policies for PrimaryResourceName string `yaml:"primary_resource_name,omitempty"` // The name of the location/region override for use in IAM tests. IAM // tests may need this if the location is not inherited on the resource // for one reason or another RegionOverride string `yaml:"region_override,omitempty"` // The path to this example's Terraform config. // Defaults to `templates/terraform/examples/{{name}}.tf.erb` ConfigPath string `yaml:"config_path,omitempty"` // If the example should be skipped during VCR testing. // This is the case when something about the resource or config causes VCR to fail for example // a resource with a unique identifier generated within the resource via id.UniqueId() // Or a config with two fine grained resources that have a race condition during create SkipVcr bool `yaml:"skip_vcr,omitempty"` // The reason to skip a test. For example, a link to a ticket explaining the issue that needs to be resolved before // unskipping the test. If this is not empty, the test will be skipped. SkipTest string `yaml:"skip_test,omitempty"` // Specify which external providers are needed for the testcase. // Think before adding as there is latency and adds an external dependency to // your test so avoid if you can. ExternalProviders []string `yaml:"external_providers,omitempty"` DocumentationHCLText string `yaml:"-"` TestHCLText string `yaml:"-"` OicsHCLText string `yaml:"-"` } // Set default value for fields func (e *Examples) UnmarshalYAML(unmarshal func(any) error) error { type exampleAlias Examples aliasObj := (*exampleAlias)(e) err := unmarshal(aliasObj) if err != nil { return err } if e.ConfigPath == "" { e.ConfigPath = fmt.Sprintf("templates/terraform/examples/%s.tf.tmpl", e.Name) } e.SetHCLText() return nil } func (e *Examples) Validate(rName string) { if e.Name == "" { log.Fatalf("Missing `name` for one example in resource %s", rName) } e.ValidateExternalProviders() } func (e *Examples) ValidateExternalProviders() { // Official providers supported by HashiCorp // https://registry.terraform.io/search/providers?namespace=hashicorp&tier=official HASHICORP_PROVIDERS := []string{"aws", "random", "null", "template", "azurerm", "kubernetes", "local", "external", "time", "vault", "archive", "tls", "helm", "azuread", "http", "cloudinit", "tfe", "dns", "consul", "vsphere", "nomad", "awscc", "googleworkspace", "hcp", "boundary", "ad", "azurestack", "opc", "oraclepaas", "hcs", "salesforce"} var unallowedProviders []string for _, p := range e.ExternalProviders { if !slices.Contains(HASHICORP_PROVIDERS, p) { unallowedProviders = append(unallowedProviders, p) } } if len(unallowedProviders) > 0 { log.Fatalf("Providers %#v are not allowed. Only providers published by HashiCorp are allowed.", unallowedProviders) } } // Executes example templates for documentation and tests func (e *Examples) SetHCLText() { originalVars := e.Vars originalTestEnvVars := e.TestEnvVars docTestEnvVars := make(map[string]string) docs_defaults := map[string]string{ "PROJECT_NAME": "my-project-name", "PROJECT_NUMBER": "1111111111111", "CREDENTIALS": "my/credentials/filename.json", "REGION": "us-west1", "ORG_ID": "123456789", "ORG_DOMAIN": "example.com", "ORG_TARGET": "123456789", "BILLING_ACCT": "000000-0000000-0000000-000000", "MASTER_BILLING_ACCT": "000000-0000000-0000000-000000", "SERVICE_ACCT": "my@service-account.com", "CUST_ID": "A01b123xz", "IDENTITY_USER": "cloud_identity_user", "PAP_DESCRIPTION": "description", "CHRONICLE_ID": "00000000-0000-0000-0000-000000000000", "VMWAREENGINE_PROJECT": "my-vmwareengine-project", } // Apply doc defaults to test_env_vars from YAML for key := range e.TestEnvVars { docTestEnvVars[key] = docs_defaults[e.TestEnvVars[key]] } e.TestEnvVars = docTestEnvVars e.DocumentationHCLText = ExecuteTemplate(e, e.ConfigPath, true) e.DocumentationHCLText = regexp.MustCompile(`\n\n$`).ReplaceAllString(e.DocumentationHCLText, "\n") // Remove region tags re1 := regexp.MustCompile(`# \[[a-zA-Z_ ]+\]\n`) re2 := regexp.MustCompile(`\n# \[[a-zA-Z_ ]+\]`) e.DocumentationHCLText = re1.ReplaceAllString(e.DocumentationHCLText, "") e.DocumentationHCLText = re2.ReplaceAllString(e.DocumentationHCLText, "") testVars := make(map[string]string) testTestEnvVars := make(map[string]string) // Override vars to inject test values into configs - will have // - "a-example-var-value%{random_suffix}"" // - "%{my_var}" for overrides that have custom Golang values for key, value := range originalVars { var newVal string if strings.Contains(value, "-") { newVal = fmt.Sprintf("tf-test-%s", value) } else if strings.Contains(value, "_") { newVal = fmt.Sprintf("tf_test_%s", value) } else { // Some vars like descriptions shouldn't have prefix newVal = value } // Random suffix is 10 characters and standard name length <= 64 if len(newVal) > 54 { newVal = newVal[:54] } testVars[key] = fmt.Sprintf("%s%%{random_suffix}", newVal) } // Apply overrides from YAML for key := range e.TestVarsOverrides { testVars[key] = fmt.Sprintf("%%{%s}", key) } for key := range originalTestEnvVars { testTestEnvVars[key] = fmt.Sprintf("%%{%s}", key) } e.Vars = testVars e.TestEnvVars = testTestEnvVars e.TestHCLText = ExecuteTemplate(e, e.ConfigPath, true) e.TestHCLText = regexp.MustCompile(`\n\n$`).ReplaceAllString(e.TestHCLText, "\n") // Remove region tags e.TestHCLText = re1.ReplaceAllString(e.TestHCLText, "") e.TestHCLText = re2.ReplaceAllString(e.TestHCLText, "") e.TestHCLText = SubstituteTestPaths(e.TestHCLText) // Reset the example e.Vars = originalVars e.TestEnvVars = originalTestEnvVars } func ExecuteTemplate(e any, templatePath string, appendNewline bool) string { templates := []string{ templatePath, "templates/terraform/expand_resource_ref.tmpl", "templates/terraform/custom_flatten/bigquery_table_ref.go.tmpl", "templates/terraform/flatten_property_method.go.tmpl", "templates/terraform/expand_property_method.go.tmpl", "templates/terraform/update_mask.go.tmpl", "templates/terraform/nested_query.go.tmpl", "templates/terraform/unordered_list_customize_diff.go.tmpl", } templateFileName := filepath.Base(templatePath) tmpl, err := template.New(templateFileName).Funcs(google.TemplateFunctions).ParseFiles(templates...) if err != nil { glog.Exit(err) } contents := bytes.Buffer{} if err = tmpl.ExecuteTemplate(&contents, templateFileName, e); err != nil { glog.Exit(err) } rs := contents.String() if !strings.HasSuffix(rs, "\n") && appendNewline { rs = fmt.Sprintf("%s\n", rs) } return rs } func (e *Examples) OiCSLink() string { v := url.Values{} v.Add("cloudshell_git_repo", "https://github.com/terraform-google-modules/docs-examples.git") v.Add("cloudshell_working_dir", e.Name) v.Add("cloudshell_image", "gcr.io/cloudshell-images/cloudshell:latest") v.Add("open_in_editor", "main.tf") v.Add("cloudshell_print", "./motd") v.Add("cloudshell_tutorial", "./tutorial.md") u := url.URL{ Scheme: "https", Host: "console.cloud.google.com", Path: "/cloudshell/open", RawQuery: v.Encode(), } return u.String() } func (e *Examples) TestSlug(productName, resourceName string) string { ret := fmt.Sprintf("%s%s_%sExample", productName, resourceName, google.Camelize(e.Name, "lower")) return ret } func (e *Examples) ResourceType(terraformName string) string { if e.PrimaryResourceType != "" { return e.PrimaryResourceType } return terraformName } func SubstituteExamplePaths(config string) string { config = strings.ReplaceAll(config, "../static/img/header-logo.png", "../static/header-logo.png") config = strings.ReplaceAll(config, "path/to/private.key", "../static/ssl_cert/test.key") config = strings.ReplaceAll(config, "path/to/id_rsa.pub", "../static/ssh_rsa.pub") config = strings.ReplaceAll(config, "path/to/certificate.crt", "../static/ssl_cert/test.crt") return config } func SubstituteTestPaths(config string) string { config = strings.ReplaceAll(config, "../static/img/header-logo.png", "test-fixtures/header-logo.png") config = strings.ReplaceAll(config, "path/to/private.key", "test-fixtures/test.key") config = strings.ReplaceAll(config, "path/to/certificate.crt", "test-fixtures/test.crt") config = strings.ReplaceAll(config, "path/to/index.zip", "%{zip_path}") config = strings.ReplaceAll(config, "verified-domain.com", "tf-test-domain%{random_suffix}.gcp.tfacc.hashicorptest.com") config = strings.ReplaceAll(config, "path/to/id_rsa.pub", "test-fixtures/ssh_rsa.pub") return config } // Executes example templates for documentation and tests func (e *Examples) SetOiCSHCLText() { originalVars := e.Vars originalTestEnvVars := e.TestEnvVars // // Remove region tags re1 := regexp.MustCompile(`# \[[a-zA-Z_ ]+\]\n`) re2 := regexp.MustCompile(`\n# \[[a-zA-Z_ ]+\]`) testVars := make(map[string]string) for key, value := range originalVars { testVars[key] = fmt.Sprintf("%s-${local.name_suffix}", value) } // Apply overrides from YAML for key, value := range e.OicsVarsOverrides { testVars[key] = value } e.Vars = testVars e.OicsHCLText = ExecuteTemplate(e, e.ConfigPath, true) e.OicsHCLText = regexp.MustCompile(`\n\n$`).ReplaceAllString(e.OicsHCLText, "\n") // Remove region tags e.OicsHCLText = re1.ReplaceAllString(e.OicsHCLText, "") e.OicsHCLText = re2.ReplaceAllString(e.OicsHCLText, "") e.OicsHCLText = SubstituteExamplePaths(e.OicsHCLText) // Reset the example e.Vars = originalVars e.TestEnvVars = originalTestEnvVars }