tpgtools/resource.go (704 lines of code) (raw):

// Copyright 2021 Google LLC. All Rights Reserved. // // 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 main import ( "fmt" "io/ioutil" "os" "path" "sort" "strings" "bitbucket.org/creachadair/stringset" "github.com/golang/glog" "github.com/nasa9084/go-openapi" "gopkg.in/yaml.v2" ) const GoPkgTerraformSdkValidation = "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" // Resource is tpgtools' model of what a information is necessary to generate a // resource in TPG. type Resource struct { productMetadata *ProductMetadata // ID is the Terraform resource id format as a pattern string. Additionally, // import formats can be derived from it. ID string // If the Terraform ID format should be used instead of the DCL ID function. // For example, resources with a regional/global cannot use the DCL ID formatter. UseTerraformID bool // ImportFormats are pattern format strings for importing the Terraform resource. // TODO: if none are set, the resource does not support import. ImportFormats []string // AppendToBasePath is a string that will be appended to the end of the API base path. // rarely needed in cases where the shared mm basepath does not include the version // as in Montioring https://git.io/Jz4Wn AppendToBasePath string // ReplaceInBasePath contains a string replacement for the config base path, // replacing one substring with another. ReplaceInBasePath BasePathReplacement // SkipInProvider is true when the resource shouldn't be included in the dclResources // map for the provider. This is usually because it was already included through mmv1. SkipInProvider bool // title is the name of the resource in snake_case. For example, // "instance", "backend_service". title SnakeCaseTerraformResourceName // dclTitle is the name of the resource in TitleCase, as parsed from the relevant // DCL YAML file. For example, "Instance", "BackendService". // This is particularly useful for acronymizations that exist in // resource names, like OSPolicy. We store it separately from title // because it can differ, especially in the case of split resources: // "region_backend_service" vs "BackendService". We use this to // create the names of DCL functions - "Apply{{dclTitle}}". dclTitle TitleCaseResourceName // dclStructName is the name of the resource struct in the DCL. In almost all cases // this is == to dclTitle, but sometimes, due to (for instance) reserved words in the // DCL conflicting with resource names, this differs. We use this to create DCL // structs: `foo := compute.{{dclStructName}}{field1: "bar"}`. dclStructName TitleCaseResourceName // Description of the Terraform resource Description string // Lock name for a mutex to prevent concurrent API calls for a given resource Mutex string // Properties are the fields of a resource. Properties may be nested. Properties []Property // InsertTimeoutMinutes is the timeout value in minutes for the resource // create operation InsertTimeoutMinutes int // UpdateTimeoutMinutes is the timeout value in minutes for the resource // update operation UpdateTimeoutMinutes int // DeleteTimeoutMinutes is the timeout value in minutes for the resource // delete operation DeleteTimeoutMinutes int // PreCreateFunction is the name of a function that's called before the // Creation call for a resource- specifically, before the id is recorded. PreCreateFunction *string // PostCreateFunction is the name of a function that's called immediately // after the Creation call for a resource. PostCreateFunction *string // PreDeleteFunction is the name of a function that's called immediately // prior to the Delete call for a resource. PreDeleteFunction *string // CustomImportFunction is the name of a function that's called in place // of the standard import template code CustomImportFunction *string // CustomizeDiff is a list of functions to set as the Terraform schema // CustomizeDiff field CustomizeDiff []string // List of other Golang packages to import in a resources' generated Go file additionalFileImportSet stringset.Set // ListFields is the list of fields required for a list call. ListFields []string // location is one of "zone", "region", or "global". location string // HasProject tells us if the resource has a project field. HasProject bool // HasCreate tells us if the resource has a create endpoint. HasCreate bool // HasSweeper says if this resource has a generated sweeper. HasSweeper bool // These are all of the reused types. ReusedTypes []Property // If this resource requires a state hint to update correctly StateHint bool // CustomCreateDirectiveFunction is the name of a function that takes the // object to be created and returns a list of directive to use for the apply // call CustomCreateDirectiveFunction *string // SkipDeleteFunction is the name of a function that takes the // object and config and returns a boolean for if Terraform should make // the delete call for the resource SkipDeleteFunction *string // SerializationOnly defines if this resource should not generate provider files // and only be used for serialization SerializationOnly bool // CustomSerializer defines the function this resource should use to serialize itself. CustomSerializer *string // TerraformProductName is the Product name overridden from the DCL TerraformProductName *SnakeCaseProductName // The array of Samples associated with the resource Samples []Sample // Versions specific information about this resource versionMetadata Version // Reference points to the rest API Reference *Link // Guides point to non-rest useful context for the resource. Guides []Link // The current schema version SchemaVersion int // The schema versions from 0 to the current schema version SchemaVersions []int // Whether to generate long form versions of resource sample tests GenerateLongFormTests bool } type Link struct { text string url string } type BasePathReplacement struct { Present bool Old string New string } func (l Link) Markdown() string { return fmt.Sprintf("[%s](%s)", l.text, l.url) } func (r *Resource) fillLinksFromExtensionsMap(m map[string]interface{}) { ref, ok := m["x-dcl-ref"].(map[string]interface{}) if ok { r.Reference = &Link{url: ref["url"].(string), text: ref["text"].(string)} } gs, ok := m["x-dcl-guides"].([]interface{}) if ok { for _, g := range gs { guide := g.(map[interface{}]interface{}) r.Guides = append(r.Guides, Link{url: guide["url"].(string), text: guide["text"].(string)}) } } } // Name is the shortname of a resource. For example, "instance". func (r Resource) Name() SnakeCaseTerraformResourceName { return r.title } func (r Resource) TitleCaseFullName() TitleCaseFullName { return TitleCaseFullName(snakeToTitleCase(concatenateSnakeCase(r.ProductName(), r.Name())).titlecase()) } func (r Resource) DCLTitle() TitleCaseResourceName { return r.dclTitle } func (r Resource) DCLStructName() TitleCaseResourceName { if r.dclStructName != "" { return r.dclStructName } return r.dclTitle } // TerraformName is the Terraform resource type used in HCL configuration. // For example, "google_compute_instance" func (r Resource) TerraformName() SnakeCaseFullName { googlePrefix := miscellaneousNameSnakeCase("google") if r.TerraformProductName != nil { if r.TerraformProductName.snakecase() == "" { return SnakeCaseFullName(concatenateSnakeCase(googlePrefix, r.Name())) } return SnakeCaseFullName(concatenateSnakeCase(googlePrefix, *r.TerraformProductName, r.Name())) } return SnakeCaseFullName(concatenateSnakeCase(googlePrefix, r.ProductName(), r.Name())) } // PathType is the title-cased name of a resource preceded by its package, // often used to namespace functions. For example, "RedisInstance". func (r Resource) PathType() TitleCaseFullName { return r.TitleCaseFullName() } // Package is the namespace of the package within the dcl // the Package is normally a lowercase variant of ProductName func (r Resource) Package() DCLPackageName { return r.productMetadata.PackageName } func (r Resource) TitleCasePackageName() RenderedString { return RenderedString(snakeToTitleCase(r.ProductName()).titlecase()) } func (r Resource) DocsSection() miscellaneousNameTitleCase { return r.productMetadata.DocsSection() } // ProductName is the snakecase product name of a resource. For example, // "network_services". func (r Resource) ProductName() SnakeCaseProductName { return r.productMetadata.ProductName } func (r Resource) ProductMetadata() *ProductMetadata { copy := *r.productMetadata return &copy } // DCLPackage is the package name of the DCL client library to use for this // resource. For example, the Package "access_context_manager" at version GA would have a // DCLPackage of "accesscontextmanager", and at beta would be "accesscontextmanager/beta". func (r Resource) DCLPackage() DCLPackageNameWithVersion { return r.productMetadata.PackageNameWithVersion() } // Updatable returns true if the resource should have an update method. // This will avoid the error message: // "All fields are ForceNew or Computed w/out Optional, Update is superfluous" func (r Resource) Updatable() bool { for _, p := range r.SchemaProperties() { if !p.ForceNew && !(!p.Optional && p.Computed) { return true } } return false } // The resource has other mutable fields, besides "labels" and "terraform_labels" fields func (r Resource) HasMutableNonLabelsFields() bool { for _, p := range r.SchemaProperties() { if !p.IsResourceLabels() && p.Name() != "terraform_labels" && !p.ForceNew && !(!p.Optional && p.Computed) { return true } } return false } // Objects are properties with sub-properties func (r Resource) Objects() (props []Property) { for _, v := range r.Properties { if len(v.Properties) != 0 { // If this property uses a reused type, add it one-time afterwards to avoid multiple creations. if v.ref != "" { continue } props = append(props, v) props = append(props, v.Objects()...) } } for _, v := range r.ReusedTypes { props = append(props, v) props = append(props, v.Objects()...) } return props } // SchemaProperties is the list of resource properties filtered to those included in schema. func (r Resource) SchemaProperties() (props []Property) { return collapsedProperties(r.Properties) } // Enum arrays are not arrays of strings in the DCL and require special // conversion func (r Resource) EnumArrays() (props []Property) { // Top level for _, v := range r.Properties { if v.Type.typ.Items != nil && len(v.Type.typ.Items.Enum) > 0 { props = append(props, v) } } // Look for nested for _, n := range r.Objects() { for _, v := range n.Properties { if v.Type.typ.Items != nil && len(v.Type.typ.Items.Enum) > 0 { props = append(props, v) } } } return props } // AdditionalGoPackages returns a sorted list of additional Go packages to import. func (r Resource) AdditionalFileImports() []string { sl := make([]string, 0, len(r.additionalFileImportSet)) for k := range r.additionalFileImportSet { sl = append(sl, k) } sort.Strings(sl) return sl } // If this resource has a server generated field that is used to read the // resource. This must be set during create func (r Resource) HasServerGeneratedName() bool { identityFields := idParts(r.ID) for _, p := range r.Properties { if stringInSlice(p.Name(), identityFields) { if !p.Settable { return true } } } return false } // SweeperFunctionArgs returns a list of arguments for calling a sweeper function. func (r Resource) SweeperFunctionArgs() string { vals := make([]string, 0) for _, v := range r.ListFields { vals = append(vals, fmt.Sprintf("d[\"%s\"]", v)) } if len(vals) > 0 { return strings.Join(vals, ",") + "," } else { return "" } } // Returns the name of the ID function for the Terraform resource. func (r Resource) IDFunction() string { for _, p := range r.Properties { if p.forwardSlashAllowed { return "tpgresource.ReplaceVars" } } return "tpgresource.ReplaceVarsForId" } // Check if the resource has the lables field for the resource func (r Resource) HasLabels() bool { for _, p := range r.Properties { if p.IsResourceLabels() { return true } } return false } // Check if the resource has the annotations field for the resource func (r Resource) HasAnnotations() bool { for _, p := range r.Properties { if p.IsResourceAnnotations() { return true } } return false } // ResourceInput is a Resource along with additional generation metadata. type ResourceInput struct { Resource } // RegisterReusedType adds a new reused type if the type does not already exist. func (r Resource) RegisterReusedType(p Property) []Property { found := false for _, v := range r.ReusedTypes { if v.ref == p.ref { found = true } } if !found { r.ReusedTypes = append(r.ReusedTypes, p) } return r.ReusedTypes } func createResource(schema *openapi.Schema, info *openapi.Info, typeFetcher *TypeFetcher, overrides Overrides, product *ProductMetadata, version Version, location string) (*Resource, error) { resourceTitle := strings.Split(info.Title, "/")[1] res := Resource{ title: SnakeCaseTerraformResourceName(jsonToSnakeCase(resourceTitle).snakecase()), dclStructName: TitleCaseResourceName(schema.Title), dclTitle: TitleCaseResourceName(resourceTitle), productMetadata: product, versionMetadata: version, Description: info.Description, location: location, InsertTimeoutMinutes: 20, UpdateTimeoutMinutes: 20, DeleteTimeoutMinutes: 20, } // Since the resource's "info" extension field can't be accessed, the relevant // extensions have been copied into the schema objects. res.fillLinksFromExtensionsMap(schema.Extension) // Resource Override: Custom Timeout ctd := CustomTimeoutDetails{} ctdOk, err := overrides.ResourceOverrideWithDetails(CustomTimeout, &ctd, location) if err != nil { return nil, fmt.Errorf("failed to decode custom timeout details: %v", err) } if ctdOk { res.InsertTimeoutMinutes = ctd.TimeoutMinutes res.UpdateTimeoutMinutes = ctd.TimeoutMinutes res.DeleteTimeoutMinutes = ctd.TimeoutMinutes } if overrides.ResourceOverride(SkipInProvider, location) { res.SkipInProvider = true } crname := CustomResourceNameDetails{} crnameOk, err := overrides.ResourceOverrideWithDetails(CustomResourceName, &crname, location) if err != nil { return nil, fmt.Errorf("failed to decode custom resource name details: %v", err) } if crnameOk { res.title = SnakeCaseTerraformResourceName(crname.Title) } id, customID, err := findResourceID(schema, overrides, location) if err != nil { return nil, err } res.ID = id res.UseTerraformID = customID // Resource Override: Custom Import Function cifd := CustomImportFunctionDetails{} cifdOk, err := overrides.ResourceOverrideWithDetails(CustomImport, &cifd, location) if err != nil { return nil, fmt.Errorf("failed to decode custom import function details: %v", err) } if cifdOk { res.CustomImportFunction = &cifd.Function } // Resource Override: Append to Base Path atbpd := AppendToBasePathDetails{} atbpOk, err := overrides.ResourceOverrideWithDetails(AppendToBasePath, &atbpd, location) if err != nil { return nil, fmt.Errorf("failed to decode append to base path details: %v", err) } if atbpOk { res.AppendToBasePath = atbpd.String } // Resource Override: Replace in Base Path ribpd := ReplaceInBasePathDetails{} ribpOk, err := overrides.ResourceOverrideWithDetails(ReplaceInBasePath, &ribpd, location) if err != nil { return nil, fmt.Errorf("failed to decode replace in base path details: %v", err) } if ribpOk { res.ReplaceInBasePath.Present = true res.ReplaceInBasePath.Old = ribpd.Old res.ReplaceInBasePath.New = ribpd.New } // Resource Override: Mutex md := MutexDetails{} mdOk, err := overrides.ResourceOverrideWithDetails(Mutex, &md, location) if err != nil { return nil, fmt.Errorf("failed to decode mutex details: %v", err) } if mdOk { res.Mutex = md.Mutex } props, err := createPropertiesFromSchema(schema, typeFetcher, overrides, &res, nil, location) if err != nil { return nil, err } res.Properties = props onlyLongFormFormat := shouldAllowForwardSlashInFormat(res.ID, res.Properties) // Resource Override: Import formats ifd := ImportFormatDetails{} ifdOk, err := overrides.ResourceOverrideWithDetails(ImportFormat, &ifd, location) if err != nil { return nil, fmt.Errorf("failed to decode import format details: %v", err) } if ifdOk { res.ImportFormats = ifd.Formats } else { res.ImportFormats = defaultImportFormats(res.ID, onlyLongFormFormat) } _, res.HasProject = schema.Properties["project"] // Resource Override: Virtual field for _, vfd := range overrides.ResourceOverridesWithDetails(VirtualField, location) { vf := VirtualFieldDetails{} if err := convert(vfd, &vf); err != nil { return nil, fmt.Errorf("error converting type: %v", err) } res.Properties = append(res.Properties, readVirtualField(vf)) } // Resource-level pre and post action functions preCreate := PreCreateFunctionDetails{} preCreOk, err := overrides.ResourceOverrideWithDetails(PreCreate, &preCreate, location) if err != nil { return nil, fmt.Errorf("failed to decode pre create function details: %v", err) } if preCreOk { res.PreCreateFunction = &preCreate.Function } postCreate := PostCreateFunctionDetails{} postCreOk, err := overrides.ResourceOverrideWithDetails(PostCreate, &postCreate, location) if err != nil { return nil, fmt.Errorf("failed to decode post create function details: %v", err) } if postCreOk { res.PostCreateFunction = &postCreate.Function } pd := PreDeleteFunctionDetails{} pdOk, err := overrides.ResourceOverrideWithDetails(PreDelete, &pd, location) if err != nil { return nil, fmt.Errorf("failed to decode pre delete function details: %v", err) } if pdOk { res.PreDeleteFunction = &pd.Function } // Resource Override: Customize Diff cdiff := CustomizeDiffDetails{} cdOk, err := overrides.ResourceOverrideWithDetails(CustomizeDiff, &cdiff, location) if err != nil { return nil, fmt.Errorf("failed to decode customize diff details: %v", err) } if cdOk { res.CustomizeDiff = cdiff.Functions } if res.HasLabels() { res.CustomizeDiff = append(res.CustomizeDiff, "tpgresource.SetLabelsDiff") } if res.HasAnnotations() { res.CustomizeDiff = append(res.CustomizeDiff, "tpgresource.SetAnnotationsDiff") } // ListFields if parameters, ok := typeFetcher.doc.Paths["list"]; ok { for _, param := range parameters.Parameters { if param.Name != "" { res.ListFields = append(res.ListFields, param.Name) } } } // Determine if a resource has a create method. res.HasCreate, _ = schema.Extension["x-dcl-has-create"].(bool) // Determine if a resource can use a generated sweeper or not // We only supply a certain set of parent values to sweepers, so only generate // one if it will actually work- resources with resource parents are not // sweepable, in particular, such as nested resources or fine-grained // resources. Additional special cases can be handled with overrides. res.HasSweeper = true validSweeperParameters := []string{"project", "region", "location", "zone", "billingAccount"} if deleteAllInfo, ok := typeFetcher.doc.Paths["deleteAll"]; ok { for _, p := range deleteAllInfo.Parameters { // if any field isn't a standard sweeper parameter, don't make a sweeper if !stringInSlice(p.Name, validSweeperParameters) { res.HasSweeper = false } } } else { // if deleteAll wasn't found, the DCL hasn't published a sweeper res.HasSweeper = false } if overrides.ResourceOverride(NoSweeper, location) { if res.HasSweeper == false { return nil, fmt.Errorf("superfluous NO_SWEEPER specified for %q", res.TerraformName()) } res.HasSweeper = false } stateHint, ok := schema.Extension["x-dcl-uses-state-hint"].(bool) if ok { res.StateHint = stateHint } // Resource Override: CustomCreateDirectiveFunction createDirectiveFunc := CustomCreateDirectiveDetails{} createDirectiveFuncOk, err := overrides.ResourceOverrideWithDetails(CustomCreateDirective, &createDirectiveFunc, location) if err != nil { return nil, fmt.Errorf("failed to decode custom create directive function details: %v", err) } if createDirectiveFuncOk { res.CustomCreateDirectiveFunction = &createDirectiveFunc.Function } // Resource Override: SkipDeleteFunction skipDeleteFunc := SkipDeleteFunctionDetails{} skipDeleteFuncOk, err := overrides.ResourceOverrideWithDetails(SkipDeleteFunction, &skipDeleteFunc, location) if err != nil { return nil, fmt.Errorf("failed to decode skip delete details: %v", err) } if skipDeleteFuncOk { res.SkipDeleteFunction = &skipDeleteFunc.Function } // Resource Override: SerializationOnly res.SerializationOnly = overrides.ResourceOverride(SerializationOnly, location) // Resource Override: CustomSerializer customSerializerFunc := CustomSerializerDetails{} customSerializerFuncOk, err := overrides.ResourceOverrideWithDetails(CustomSerializer, &customSerializerFunc, location) if err != nil { return nil, fmt.Errorf("failed to decode custom serializer function details: %v", err) } if customSerializerFuncOk { res.CustomSerializer = &customSerializerFunc.Function } // Resource Override: TerraformProductName terraformProductName := TerraformProductNameDetails{} terraformProductNameOk, err := overrides.ResourceOverrideWithDetails(TerraformProductName, &terraformProductName, location) if err != nil { return nil, fmt.Errorf("failed to decode terraform product name function details: %v", err) } if terraformProductNameOk { scpn := SnakeCaseProductName(terraformProductName.Product) res.TerraformProductName = &scpn } // Resource Override: StateUpgrade stateUpgrade := StateUpgradeDetails{} stateUpgradeOk, err := overrides.ResourceOverrideWithDetails(StateUpgrade, &stateUpgrade, location) if err != nil { return nil, fmt.Errorf("Failed to decode state upgrade details: %v", err) } if stateUpgradeOk { res.SchemaVersion = stateUpgrade.SchemaVersion res.SchemaVersions = make([]int, res.SchemaVersion) for i := range res.SchemaVersions { res.SchemaVersions[i] = i } } if overrides.ResourceOverride(GenerateLongFormTests, location) { res.GenerateLongFormTests = true } res.Samples = res.loadSamples() return &res, nil } func readVirtualField(vf VirtualFieldDetails) Property { prop := Property{ title: vf.Name, Type: Type{&openapi.Schema{Type: vf.Type}}, } if vf.Type == "boolean" { def := "false" prop.Default = &def } if vf.Output { prop.Computed = true } else { prop.Optional = true prop.Settable = true } return prop } func (r Resource) TestSamples() []Sample { return r.getSamples(false) } func (r Resource) DocSamples() []Sample { return r.getSamples(true) } func (r Resource) getSamples(docs bool) []Sample { out := []Sample{} if len(r.Samples) < 1 { return out } var hideList []string if docs { hideList = r.Samples[0].DocHide if len(r.Samples[0].DocHideConditional) > 0 { for _, dochidec := range r.Samples[0].DocHideConditional { if r.location == dochidec.Location { hideList = append(hideList, dochidec.Name) } } } } else { hideList = r.Samples[0].Testhide if len(r.Samples[0].TestHideConditional) > 0 { for _, testhidec := range r.Samples[0].TestHideConditional { if r.location == testhidec.Location { hideList = append(hideList, testhidec.Name) } } } } for _, sample := range r.Samples { shouldhide := false for _, hideName := range hideList { if sample.FileName == hideName { shouldhide = true } } if !shouldhide { out = append(out, sample) } } return out } func (r *Resource) getSampleAccessoryFolder() Filepath { resourceType := strings.ToLower(r.DCLTitle().titlecase()) packageName := r.productMetadata.PackageName.lowercase() sampleAccessoryFolder := path.Join(*tPath, packageName, "samples", resourceType) return Filepath(sampleAccessoryFolder) } func (r *Resource) loadSamples() []Sample { samples := []Sample{} handWritten := r.loadHandWrittenSamples() dclSamples := r.loadDCLSamples() samples = append(samples, dclSamples...) samples = append(samples, handWritten...) return samples } func (r *Resource) loadHandWrittenSamples() []Sample { sampleAccessoryFolder := r.getSampleAccessoryFolder() sampleFriendlyMetaPath := path.Join(string(sampleAccessoryFolder), "meta.yaml") samples := []Sample{} if !pathExists(sampleFriendlyMetaPath) { return samples } files, err := ioutil.ReadDir(string(sampleAccessoryFolder)) if err != nil { glog.Exit(err) } for _, file := range files { if fileName := strings.ToLower(file.Name()); !strings.HasSuffix(fileName, ".tf.tmpl") || strings.Contains(fileName, "_update") { continue } sample := Sample{} sampleName := strings.Split(file.Name(), ".")[0] sampleDefinitionFile := path.Join(string(sampleAccessoryFolder), sampleName+".yaml") var tc []byte if pathExists(sampleDefinitionFile) { tc, err = mergeYaml(sampleDefinitionFile, sampleFriendlyMetaPath) } else { tc, err = ioutil.ReadFile(sampleFriendlyMetaPath) } if err != nil { glog.Exit(err) } err = yaml.UnmarshalStrict(tc, &sample) if err != nil { glog.Exit(err) } versionMatch := false // if no versions provided assume all versions if len(sample.Versions) <= 0 { sample.HasGAEquivalent = true versionMatch = true } else { for _, v := range sample.Versions { if v == r.versionMetadata.V { versionMatch = true } if v == "ga" { sample.HasGAEquivalent = true } } } if !versionMatch { glog.Errorf("skipping %q due to no version match", file.Name()) continue } sample.SamplesPath = sampleAccessoryFolder sample.resourceReference = r sample.FileName = file.Name() sample.PrimaryResource = &(sample.FileName) if sample.Name == nil || *sample.Name == "" { sampleName = strings.Split(sample.FileName, ".")[0] sample.Name = &sampleName } sample.TestSlug = RenderedString(snakeToTitleCase(miscellaneousNameSnakeCase(sampleName)).titlecase() + "HandWritten") // The "labels" and "annotations" fields in the state are decided by the configuration. // During importing, as the configuration is unavailableafter, the "labels" and "annotations" fields in the state will be empty. // So add the "labels" and the "annotations" fields to the ImportStateVerifyIgnore list. if r.HasLabels() { sample.IgnoreRead = append(sample.IgnoreRead, "labels", "terraform_labels") } if r.HasAnnotations() { sample.IgnoreRead = append(sample.IgnoreRead, "annotations") } samples = append(samples, sample) } return samples } func (r *Resource) loadDCLSamples() []Sample { sampleAccessoryFolder := r.getSampleAccessoryFolder() packagePath := r.productMetadata.PackagePath version := r.versionMetadata.V resourceType := r.DCLTitle() sampleFriendlyMetaPath := path.Join(string(sampleAccessoryFolder), "meta.yaml") samples := []Sample{} if mode != nil && *mode == "serialization" { return samples } // Samples appear in the root product folder packagePath = Filepath(strings.Split(string(packagePath), "/")[0]) samplesPath := Filepath(path.Join(*fPath, string(packagePath), "samples")) files, err := ioutil.ReadDir(string(samplesPath)) if err != nil { // ignore the error if the file just doesn't exist if !os.IsNotExist(err) { glog.Exit(err) } } for _, file := range files { if !strings.HasSuffix(file.Name(), ".yaml") { continue } sample := Sample{} sampleOGFilePath := path.Join(string(samplesPath), file.Name()) var tc []byte if pathExists(sampleFriendlyMetaPath) { tc, err = mergeYaml(sampleOGFilePath, sampleFriendlyMetaPath) } else { glog.Errorf("warning : sample meta does not exist for %v at %q", r.TerraformName(), sampleFriendlyMetaPath) tc, err = ioutil.ReadFile(path.Join(string(samplesPath), file.Name())) } if err != nil { glog.Exit(err) } err = yaml.UnmarshalStrict(tc, &sample) if err != nil { glog.Exit(err) } versionMatch := false for _, v := range sample.Versions { if v == version { versionMatch = true } if v == "ga" { sample.HasGAEquivalent = true versionMatch = true } } primaryResource := *sample.PrimaryResource var parts []miscellaneousNameSnakeCase parts = assertSnakeArray(strings.Split(primaryResource, ".")) primaryResourceName := snakeToTitleCase(parts[len(parts)-2]) if !versionMatch { continue } else if !strings.EqualFold(primaryResourceName.titlecase(), resourceType.titlecase()) { // This scenario will occur for product folders with multiple resources continue } sample.SamplesPath = samplesPath sample.resourceReference = r sample.FileName = file.Name() var dependencies []Dependency mainResource := sample.generateSampleDependencyWithName(primaryResource, "primary") dependencies = append(dependencies, mainResource) for _, dFileName := range sample.DependencyFileNames { dependency := sample.generateSampleDependency(dFileName) dependencies = append(dependencies, dependency) } sample.DependencyList = dependencies sample.TestSlug = RenderedString(sampleNameToTitleCase(*sample.Name).titlecase()) // The "labels" and "annotations" fields in the state are decided by the configuration. // During importing, as the configuration is unavailable, the "labels" and "annotations" fields in the state will be empty. // So add the "labels" and the "annotations" fields to the ImportStateVerifyIgnore list. if r.HasLabels() { sample.IgnoreRead = append(sample.IgnoreRead, "labels", "terraform_labels") } if r.HasAnnotations() { sample.IgnoreRead = append(sample.IgnoreRead, "annotations") } if r.GenerateLongFormTests { longFormSample := sample longFormSample.LongForm = true var longFormDependencies []Dependency mainResourceLongForm := longFormSample.generateSampleDependencyWithName(primaryResource, "primary") longFormDependencies = append(longFormDependencies, mainResourceLongForm) for _, dFileName := range longFormSample.DependencyFileNames { longFormDependency := sample.generateSampleDependency(dFileName) longFormDependencies = append(longFormDependencies, longFormDependency) } longFormSample.DependencyList = longFormDependencies longFormSample.TestSlug += "LongForm" samples = append(samples, longFormSample) } samples = append(samples, sample) } return samples }