tpgtools/property.go (769 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"
"sort"
"strings"
"unicode"
"github.com/golang/glog"
"github.com/nasa9084/go-openapi"
)
// Property is the representation of a TPG resource property in tpgtools.
type Property struct {
// title is the name of a property.
title string
// PackageName is the title-cased shortname of a field as it appears as a
// property in the DCL. For example, "MachineType".
PackageName string
// Type is the type of a property.
Type
// Settable indicates that a field is settable in desired specs provided to
// Apply.
// Settable fields may be Computed- fields are sometimes Optional + Computed
// to indicate that they have complex default values in the API.
Settable bool
// Only applies to nested object properties.
// Indicates this property should be excluded and its
// subproperties should be brought up a level.
Collapsed bool
// Elem is the value to insert into the Elem field. If empty, none will be
// inserted.
// In most cases, this should be a function call to a schema function. For
// TypeMaps, this will be a one-liner adding a primitive elem.
Elem *string
ElemIsBasicType bool
// ConflictsWith is the list of fields that this field conflicts with
ConflictsWith ConflictsWith
// ConflictsWith is the list of fields that this field conflicts with
// in JSONCase. For example, "machineType"
JSONCaseConflictsWith []string
// Default is the default for the field.
Default *string
// raw schema values
Required bool
Optional bool
Computed bool
Sensitive bool
WriteOnly bool
ForceNew bool
Description string
DiffSuppressFunc *string
ValidateFunc *string
SetHashFunc *string
MaxItems *int64
MinItems *int64
ConfigMode *string
Removed *string
Deprecated *string
// end raw schema values
// StateGetter is the line of code to retrieve a field from the `d`
// ResourceData or (TODO:) from a map[string]interface{}
StateGetter *string
// StateSetter is the line of code to set a field in the `d` ResourceData
// or (TODO:) a map[string]interface{}
StateSetter *string
// If this field is a three-state boolean in DCL which is represented as a
// string in terraform. This is done so that the DCL can distinguish between
// the field being unset and being set to false.
EnumBool bool
// Whether this field is only used as a url parameter
Parameter bool
// Whether this field has long form behavior in the DCL
HasLongForm bool
// An IdentityGetter is a function to retrieve the value of an "identity" field
// from state. Identity fields will sometimes allow retrieval from multiple
// fields or from the user's environment variables.
// In the most common case, project/region/zone will use special resource-level
// properties instead of IdentityGetters. However, if they have atypical
// behaviour, such as sourcing a region from a zone, an IdentityGetter will be
// used instead.
IdentityGetter *string
// Sub-properties of nested objects or arrays with nested objects
Properties []Property
// If this is a complex map type, this string represents the name of the
// field that the key to the map can be set with
ComplexMapKeyName string
// Reference to the parent resource.
// note: "Properties" will not be available.
resource *Resource
// Reference to the parent property of a sub-property. If the property is
// top-level, this will be unset.
parent *Property
// customName is the Terraform-specific name that overrides title.
customName string
// Ref is the name of the shared reference type.
ref string
// If this property allows forward slashes in its value (only important for
// properties sent in the URL)
forwardSlashAllowed bool
}
// An IdentityGetter is a function to retrieve the value of an "identity" field
// from state.
type IdentityGetter struct {
// If HasError is set to true, the function called by FunctionCall will
// return (v, error)
HasError bool
// Rendered function call to insert into the template. For example,
// d.Get("name").(string)
FunctionCall string
}
// Name is the shortname of a field. For example, "machine_type".
func (p Property) Name() string {
if len(p.customName) > 0 {
return p.customName
}
return p.title
}
// overridePath is the path of a property used in override names. For example,
// "node_config.machine_type".
func (p Property) overridePath() string {
if p.parent != nil {
return p.parent.overridePath() + "." + p.title
}
return p.title
}
// PackageJSONName is the camel-cased shortname of a field as it appears in the
// DCL's json serialization. For example, "machineType".
func (p Property) PackageJSONName() string {
s := p.PackageName
a := []rune(s)
if len(a) == 0 {
return ""
}
a[0] = unicode.ToLower(a[0])
return string(a)
}
// PackagePath is the title-cased path of a type (relative to the resource) for
// use in naming functions. For example, "MachineType" or "NodeConfigPreemptible".
func (p Property) PackagePath() string {
if p.ref != "" {
return p.ref
}
if p.parent != nil {
return p.parent.PackagePath() + p.PackageName
}
return p.PackageName
}
func (p Property) ObjectType() string {
parent := p
// Look up chain to see if we are within a reference
// types within a reference should not use the parent resource's type
for {
if parent.ref != "" {
return p.PackagePath()
}
if parent.parent == nil {
break
}
parent = *parent.parent
}
return fmt.Sprintf("%s%s", p.resource.DCLTitle(), p.PackagePath())
}
func (p Property) IsArray() bool {
return (p.Type.String() == SchemaTypeList || p.Type.String() == SchemaTypeSet) && !p.Type.IsObject() && !p.IsComplexMap()
}
func (t Type) IsSet() bool {
return t.String() == SchemaTypeSet
}
// Complex map is for maps of string --> object that are supported in DCL but
// not in Terraform. We handle this by adding a field in the Terraform schema
// for the key in the map. This must be added via a COMPLEX_MAP_KEY_NAME
// override
func (t Type) IsComplexMap() bool {
if t.typ.AdditionalProperties != nil {
return t.typ.AdditionalProperties.Type != "string"
}
return false
}
// ShouldGenerateNestedSchema returns true if an object's nested schema function should be generated.
func (p Property) ShouldGenerateNestedSchema() bool {
return len(p.Properties) > 0 && !p.Collapsed
}
func (p Property) IsServerGeneratedName() bool {
return p.StateGetter != nil && !p.Settable
}
// DefaultStateGetter returns the line of code to retrieve a field from the `d`
// ResourceData or (TODO:) from a map[string]interface{}
func (p Property) DefaultStateGetter() string {
rawGetter := fmt.Sprintf("d.Get(%q)", p.Name())
return buildGetter(p, rawGetter)
}
func (p Property) ChangeStateGetter() string {
return buildGetter(p, fmt.Sprintf("tpgdclresource.OldValue(d.GetChange(%q))", p.Name()))
}
// Builds a Getter for constructing a shallow
// version of the object for destroy purposes
func (p Property) StateGetterForDestroyTest() string {
pullValueFromState := fmt.Sprintf(`rs.Primary.Attributes["%s"]`, p.Name())
switch p.Type.String() {
case SchemaTypeBool:
return fmt.Sprintf(`dcl.Bool(%s == "true")`, pullValueFromState)
case SchemaTypeString:
if p.Type.IsEnum() {
return fmt.Sprintf("%s.%sEnumRef(%s)", p.resource.Package(), p.ObjectType(), pullValueFromState)
}
if p.Computed {
return fmt.Sprintf("dcl.StringOrNil(%s)", pullValueFromState)
}
return fmt.Sprintf("dcl.String(%s)", pullValueFromState)
}
return ""
}
// Builds a Getter for a property with given raw value
func buildGetter(p Property, rawGetter string) string {
switch p.Type.String() {
case SchemaTypeBool:
return fmt.Sprintf("dcl.Bool(%s.(bool))", rawGetter)
case SchemaTypeString:
if p.Type.IsEnum() {
return fmt.Sprintf("%s.%sEnumRef(%s.(string))", p.resource.Package(), p.ObjectType(), rawGetter)
}
if p.EnumBool {
return fmt.Sprintf("tpgdclresource.ExpandEnumBool(%s.(string))", rawGetter)
}
if p.Computed {
return fmt.Sprintf("dcl.StringOrNil(%s.(string))", rawGetter)
}
return fmt.Sprintf("dcl.String(%s.(string))", rawGetter)
case SchemaTypeFloat:
if p.Computed {
return fmt.Sprintf("dcl.Float64OrNil(%s.(float64))", rawGetter)
}
return fmt.Sprintf("dcl.Float64(%s.(float64))", rawGetter)
case SchemaTypeInt:
if p.Computed {
return fmt.Sprintf("dcl.Int64OrNil(int64(%s.(int)))", rawGetter)
}
return fmt.Sprintf("dcl.Int64(int64(%s.(int)))", rawGetter)
case SchemaTypeMap:
return fmt.Sprintf("tpgresource.CheckStringMap(%s)", rawGetter)
case SchemaTypeList, SchemaTypeSet:
if p.Type.IsEnumArray() {
return fmt.Sprintf("expand%s%sArray(%s)", p.resource.PathType(), p.PackagePath(), rawGetter)
}
if p.Type.IsComplexMap() {
return fmt.Sprintf("expand%s%sMap(%s)", p.resource.PathType(), p.PackagePath(), rawGetter)
}
if p.Type.typ.Items != nil && p.Type.typ.Items.Type == "string" {
return fmt.Sprintf("tpgdclresource.ExpandStringArray(%s)", rawGetter)
}
if p.Type.typ.Items != nil && p.Type.typ.Items.Type == "integer" {
return fmt.Sprintf("tpgdclresource.ExpandIntegerArray(%s)", rawGetter)
}
if p.Type.typ.Items != nil && len(p.Properties) > 0 {
return fmt.Sprintf("expand%s%sArray(%s)", p.resource.PathType(), p.PackagePath(), rawGetter)
}
}
if p.typ.Type == "object" {
return fmt.Sprintf("expand%s%s(%s)", p.resource.PathType(), p.PackagePath(), rawGetter)
}
return "<unknown>"
}
// DefaultStateSetter returns the line of code to set a field in the `d`
// ResourceData or (TODO:) a map[string]interface{}
func (p Property) DefaultStateSetter() string {
switch p.Type.String() {
case SchemaTypeBool:
fallthrough
case SchemaTypeString:
fallthrough
case SchemaTypeInt:
fallthrough
case SchemaTypeFloat:
fallthrough
case SchemaTypeMap:
if p.IsResourceLabels() || p.IsResourceAnnotations() {
return fmt.Sprintf("d.Set(%q, flatten%s%s(res.%s, d))", p.Name(), p.resource.PathType(), p.PackagePath(), p.PackageName)
}
return fmt.Sprintf("d.Set(%q, res.%s)", p.Name(), p.PackageName)
case SchemaTypeList, SchemaTypeSet:
if p.IsComplexMap() {
return fmt.Sprintf("d.Set(%q, flatten%s%sMap(res.%s))", p.Name(), p.resource.PathType(), p.PackagePath(), p.PackageName)
}
if p.typ.Items != nil && ((p.typ.Items.Type == "string" && len(p.typ.Items.Enum) == 0) || p.typ.Items.Type == "integer") {
return fmt.Sprintf("d.Set(%q, res.%s)", p.Name(), p.PackageName)
}
if p.typ.Items != nil && (len(p.Properties) > 0 || len(p.typ.Items.Enum) > 0) {
return fmt.Sprintf("d.Set(%q, flatten%s%sArray(res.%s))", p.Name(), p.resource.PathType(), p.PackagePath(), p.PackageName)
}
}
if p.typ.Type == "object" {
return fmt.Sprintf("d.Set(%q, flatten%s%s(res.%s))", p.Name(), p.resource.PathType(), p.PackagePath(), p.PackageName)
}
return "<unknown>"
}
// ExpandGetter needs to return a snippet of code that produces the DCL-type
// for the field from a map[string]interface{} named obj that represents the
// parent object in Terraform.
func (p Property) ExpandGetter() string {
rawGetter := fmt.Sprintf("obj[%q]", p.Name())
return buildGetter(p, rawGetter)
}
// FlattenGetter needs to return a snippet of code that returns an interface{} which
// can be used in the d.Set() call, given a DCL-type for the parent object named `obj`.
func (p Property) FlattenGetter() string {
return p.flattenGetterWithParent("obj")
}
func (p Property) flattenGetterWithParent(parent string) string {
switch p.Type.String() {
case SchemaTypeBool:
fallthrough
case SchemaTypeString:
fallthrough
case SchemaTypeInt:
fallthrough
case SchemaTypeFloat:
fallthrough
case SchemaTypeMap:
if p.EnumBool {
return fmt.Sprintf("tpgdclresource.FlattenEnumBool(%s.%s)", parent, p.PackageName)
}
return fmt.Sprintf("%s.%s", parent, p.PackageName)
case SchemaTypeList, SchemaTypeSet:
if p.Type.IsEnumArray() {
return fmt.Sprintf("flatten%s%sArray(obj.%s)", p.resource.PathType(), p.PackagePath(), p.PackageName)
}
if p.Type.IsComplexMap() {
return fmt.Sprintf("flatten%s%sMap(%s.%s)", p.resource.PathType(), p.PackagePath(), parent, p.PackageName)
}
if p.Type.typ.Items != nil && p.Type.typ.Items.Type == "integer" {
return fmt.Sprintf("%s.%s", parent, p.PackageName)
}
if p.Type.typ.Items != nil && p.Type.typ.Items.Type == "string" {
return fmt.Sprintf("%s.%s", parent, p.PackageName)
}
if p.Type.typ.Items != nil && len(p.Properties) > 0 {
return fmt.Sprintf("flatten%s%sArray(%s.%s)", p.resource.PathType(), p.PackagePath(), parent, p.PackageName)
}
}
if p.typ.Type == "object" {
return fmt.Sprintf("flatten%s%s(%s.%s)", p.resource.PathType(), p.PackagePath(), parent, p.PackageName)
}
return "<unknown>"
}
func getSchemaExtensionMap(v interface{}) map[interface{}]interface{} {
if v != nil {
return nil
}
ls, ok := v.([]interface{})
if ok && len(ls) > 0 {
return ls[0].(map[interface{}]interface{})
}
return nil
}
func (p Property) DefaultDiffSuppress() *string {
switch p.Type.String() {
case SchemaTypeString:
// Field is reference to another resource
if _, ok := p.typ.Extension["x-dcl-references"]; ok {
dsf := "tpgresource.CompareSelfLinkOrResourceName"
return &dsf
}
}
return nil
}
func (p Property) GetRequiredFileImports() (imports []string) {
if p.ValidateFunc != nil && strings.Contains(*p.ValidateFunc, "validation.") {
imports = append(imports, GoPkgTerraformSdkValidation)
}
return imports
}
func (p Property) DefaultSetHashFunc() *string {
switch p.Type.String() {
case SchemaTypeSet:
if p.ElemIsBasicType {
shf := "schema.HashString"
return &shf
}
shf := fmt.Sprintf("schema.HashResource(%s)", *p.Elem)
return &shf
}
glog.Fatalf("Failed to find valid hash func")
return nil
}
// Objects returns a flatmap of the sub-properties within a Property which are
// objects (eg: have sub-properties themselves).
func (p Property) Objects() (props []Property) {
// if p.Properties is set, p is an object
for _, v := range p.Properties {
if len(v.Properties) != 0 {
if v.ref == "" {
props = append(props, v)
props = append(props, v.Objects()...)
}
}
}
return props
}
func (p Property) IsResourceLabels() bool {
return p.Name() == "labels" && p.parent == nil
}
func (p Property) IsResourceAnnotations() bool {
return p.Name() == "annotations" && p.parent == nil
}
func (p Property) IsTopLevel() bool {
return p.parent == nil
}
func (p Property) ShouldShowUpInSamples() bool {
return (p.Settable && p.Name() != "effective_labels" && p.Name() != "effective_annotations") || p.IsResourceLabels() || p.IsResourceAnnotations()
}
// collapsedProperties returns the input list of properties with nested objects collapsed if needed.
func collapsedProperties(props []Property) (collapsed []Property) {
for _, v := range props {
if len(v.Properties) != 0 && v.Collapsed {
collapsed = append(collapsed, collapsedProperties(v.Properties)...)
} else {
collapsed = append(collapsed, v)
}
}
return collapsed
}
// Alias []string so that append etc. still work, but we can attach a rendering
// function to the object
type ConflictsWith []string
func (c ConflictsWith) String() string {
var quoted []string
for _, s := range c {
quoted = append(quoted, fmt.Sprintf("%q", s))
}
return fmt.Sprintf("[]string{%s}", strings.Join(quoted, ","))
}
// Builds a list of properties from an OpenAPI schema.
func createPropertiesFromSchema(schema *openapi.Schema, typeFetcher *TypeFetcher, overrides Overrides, resource *Resource, parent *Property, location string) (props []Property, err error) {
identityFields := []string{} // always empty if parent != nil
if parent == nil {
identityFields = idParts(resource.ID)
}
// Maps PackageJSONName back to property Name
// for conflict fields
conflictsMap := make(map[string]string)
for k, v := range schema.Properties {
ref := ""
packageName := ""
if pName, ok := v.Extension["x-dcl-go-name"].(string); ok {
packageName = pName
}
if v.Ref != "" {
ref = v.Ref
v, err = typeFetcher.ResolveSchema(v.Ref)
if err != nil {
return nil, err
}
ref = typeFetcher.PackagePathForReference(ref, v.Extension["x-dcl-go-type"].(string))
}
// Sub-properties are referenced by name, and the explicit title value
// won't be set initially.
v.Title = k
if parent == nil && v.Title == "id" {
// If top-level field is named `id`, rename to avoid collision with Terraform id
v.Title = fmt.Sprintf("%s%s", resource.Name(), "Id")
}
p := Property{
title: jsonToSnakeCase(v.Title).snakecase(),
Type: Type{typ: v},
PackageName: packageName,
Description: v.Description,
resource: resource,
parent: parent,
ref: ref,
}
if overrides.PropertyOverride(Exclude, p, location) {
continue
}
do := CustomDefaultDetails{}
doOk, err := overrides.PropertyOverrideWithDetails(CustomDefault, p, &do, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom list size details")
}
if v.Default != "" || doOk {
def := v.Default
if doOk {
def = do.Default
}
d, err := renderDefault(p.Type, def)
if err != nil {
return nil, fmt.Errorf("failed to render default: %v", err)
}
p.Default = &d
}
cn := CustomNameDetails{}
cnOk, err := overrides.PropertyOverrideWithDetails(CustomName, p, &cn, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom name details: %v", err)
}
if cnOk {
p.customName = cn.Name
}
if p.Type.String() == SchemaTypeMap {
e := "&schema.Schema{Type: schema.TypeString}"
p.Elem = &e
p.ElemIsBasicType = true
}
if sens, ok := v.Extension["x-dcl-sensitive"].(bool); ok {
p.Sensitive = sens
}
if v, ok := v.Extension["x-dcl-conflicts"].([]interface{}); ok {
// NOTE: DCL not label x-dcl-conflicts for reused types
// TODO(shuya): handle nested field when b/213503595 got fixed
if parent == nil {
for _, ci := range v {
p.JSONCaseConflictsWith = append(p.JSONCaseConflictsWith, ci.(string))
}
conflictsMap[p.PackageJSONName()] = p.Name()
}
}
// Do this before handling properties so we can check if the parent is readOnly
isSGP := false
if sgp, ok := v.Extension["x-dcl-server-generated-parameter"].(bool); ok {
isSGP = sgp
}
if v.ReadOnly || isSGP || (parent != nil && parent.Computed) {
p.Computed = true
if stringInSlice(p.Name(), identityFields) {
sg := p.DefaultStateGetter()
p.StateGetter = &sg
}
}
p.Parameter, _ = v.Extension["x-dcl-parameter"].(bool)
p.HasLongForm, _ = v.Extension["x-dcl-has-long-form"].(bool)
// Handle object properties
if len(v.Properties) > 0 {
props, err := createPropertiesFromSchema(v, typeFetcher, overrides, resource, &p, location)
if err != nil {
return nil, err
}
p.Properties = props
if !p.Computed {
// Computed fields cannot specify MaxItems
mi := int64(1)
p.MaxItems = &mi
}
e := fmt.Sprintf("%s%sSchema()", resource.PathType(), p.PackagePath())
p.Elem = &e
p.ElemIsBasicType = false
}
// Handle array properties
if v.Items != nil {
ls := CustomListSizeConstraintDetails{}
lsOk, err := overrides.PropertyOverrideWithDetails(CustomListSize, p, &ls, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom list size details")
}
if lsOk {
if ls.Max > 0 {
p.MaxItems = &ls.Max
}
if ls.Min > 0 {
p.MinItems = &ls.Min
}
}
// We end up handling arrays of objects very similarly to nested objects
// themselves
if len(v.Items.Properties) > 0 {
props, err := createPropertiesFromSchema(v.Items, typeFetcher, overrides, resource, &p, location)
if err != nil {
return nil, err
}
p.Properties = props
e := fmt.Sprintf("%s%sSchema()", resource.PathType(), p.PackagePath())
p.Elem = &e
p.ElemIsBasicType = false
} else {
i := Type{typ: v.Items}
e := fmt.Sprintf("&schema.Schema{Type: schema.%s}", i.String())
if _, ok := v.Extension["x-dcl-references"]; ok {
e = fmt.Sprintf("&schema.Schema{Type: schema.%s, DiffSuppressFunc: tpgresource.CompareSelfLinkOrResourceName, }", i.String())
}
p.Elem = &e
p.ElemIsBasicType = true
}
}
// Complex maps are represented as TypeSet but don't have v.Items set.
// Use AdditionalProperties instead, and add an additional `name` field
// that represents the key in the map
if p.Type.IsComplexMap() {
props, err := createPropertiesFromSchema(p.Type.typ.AdditionalProperties, typeFetcher, overrides, resource, &p, location)
if err != nil {
return nil, err
}
cm := ComplexMapKeyDetails{}
cmOk, err := overrides.PropertyOverrideWithDetails(ComplexMapKey, p, &cm, location)
if err != nil {
return nil, fmt.Errorf("failed to decode complex map key name details")
}
if !cmOk {
return nil, fmt.Errorf("failed to find complex map key name for map named: %s", p.Name())
}
keyProp := Property{
title: cm.KeyName,
Type: Type{&openapi.Schema{Type: "string"}},
resource: resource,
parent: &p,
Required: true,
Description: "The name for the key in the map for which this object is mapped to in the API",
}
props = append([]Property{keyProp}, props...)
p.Properties = props
e := fmt.Sprintf("%s%sSchema()", resource.PathType(), p.PackagePath())
p.Elem = &e
p.ElemIsBasicType = false
p.ComplexMapKeyName = cm.KeyName
}
if !p.Computed {
if stringInSlice(v.Title, schema.Required) {
p.Required = true
} else {
p.Optional = true
}
}
cr := CustomSchemaValuesDetails{}
crOk, err := overrides.PropertyOverrideWithDetails(CustomSchemaValues, p, &cr, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom required details")
}
if crOk {
p.Required = cr.Required
p.Optional = cr.Optional
p.Computed = cr.Computed
}
// Handle settable fields. If the field is computed it's not settable but
// if it's also optional (O+C), it is.
if !p.Computed || (p.Optional) {
p.Settable = true
// NOTE: x-kubernetes-immmutable implies that all children of a field
// are actually immutable. However, in practice, DCL specs will label
// every immutable subfield.
if isImmutable, ok := v.Extension["x-kubernetes-immutable"].(bool); ok && isImmutable {
p.ForceNew = true
}
serverDefault, _ := v.Extension["x-dcl-server-default"].(bool)
extractIfEmpty, _ := v.Extension["x-dcl-extract-if-empty"].(bool)
if serverDefault || extractIfEmpty {
p.Computed = true
}
if forwardSlashAllowed, ok := v.Extension["x-dcl-forward-slash-allowed"].(bool); ok && forwardSlashAllowed {
p.forwardSlashAllowed = true
}
// special handling for project/region/zone/other fields with
// provider defaults
if stringInSlice(p.title, []string{"project", "region", "zone"}) || stringInSlice(p.customName, []string{"region", "project", "zone"}) {
p.Optional = true
p.Required = false
p.Computed = true
sg := fmt.Sprintf("dcl.String(%v)", p.Name())
p.StateGetter = &sg
cig := &CustomIdentityGetterDetails{}
cigOk, err := overrides.PropertyOverrideWithDetails(CustomIdentityGetter, p, cig, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom identity getter details")
}
propertyName := p.title
if p.customName != "" {
propertyName = p.customName
}
ig := fmt.Sprintf("tpgresource.Get%s(d, config)", renderSnakeAsTitle(miscellaneousNameSnakeCase(propertyName)))
if cigOk {
ig = fmt.Sprintf("%s(d, config)", cig.Function)
}
p.IdentityGetter = &ig
} else {
sg := p.DefaultStateGetter()
p.StateGetter = &sg
}
}
ss := p.DefaultStateSetter()
p.StateSetter = &ss
if p.Sensitive && p.Settable {
p.StateSetter = nil
}
css := CustomStateSetterDetails{}
cssOk, err := overrides.PropertyOverrideWithDetails(CustomStateSetter, p, &css, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom stateSetter func: %v", err)
}
if cssOk {
p.StateSetter = &css.Function
}
irOk := overrides.PropertyOverride(IgnoreRead, p, location)
if irOk {
p.StateSetter = nil
}
cd := CustomDescriptionDetails{}
cdOk, err := overrides.PropertyOverrideWithDetails(CustomDescription, p, &cd, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom description details: %v", err)
}
if cdOk {
p.Description = cd.Description
}
dsf := CustomDiffSuppressFuncDetails{}
dsfOk, err := overrides.PropertyOverrideWithDetails(DiffSuppressFunc, p, &dsf, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom diff suppress func: %v", err)
}
if dsfOk {
p.DiffSuppressFunc = &dsf.DiffSuppressFunc
} else if !(p.Computed && !p.Optional) {
p.DiffSuppressFunc = p.DefaultDiffSuppress()
}
vf := CustomValidationDetails{}
vfOk, err := overrides.PropertyOverrideWithDetails(CustomValidation, p, &vf, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom validation func: %v", err)
}
if vfOk {
p.ValidateFunc = &vf.Function
}
if p.Type.IsSet() {
shf := SetHashFuncDetails{}
shfOk, err := overrides.PropertyOverrideWithDetails(SetHashFunc, p, &shf, location)
if err != nil {
return nil, fmt.Errorf("failed to decode set hash func: %v", err)
}
if shfOk {
p.SetHashFunc = &shf.Function
} else {
p.SetHashFunc = p.DefaultSetHashFunc()
}
}
cm := CustomConfigModeDetails{}
cmOk, err := overrides.PropertyOverrideWithDetails(CustomConfigMode, p, &cm, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom config mode func: %v", err)
}
if cmOk {
p.ConfigMode = &cm.Mode
}
rd := &RemovedDetails{}
rdOk, err := overrides.PropertyOverrideWithDetails(Removed, p, rd, location)
if err != nil {
return nil, fmt.Errorf("failed to decode removed details")
}
if rdOk {
p.Removed = &rd.Message
}
dd := &DeprecatedDetails{}
ddOk, err := overrides.PropertyOverrideWithDetails(Deprecated, p, dd, location)
if err != nil {
return nil, fmt.Errorf("failed to decode deprecated details")
}
if ddOk {
p.Deprecated = &dd.Message
}
if overrides.PropertyOverride(CollapsedObject, p, location) {
p.Collapsed = true
if p.parent == nil {
collapseSS := fmt.Sprintf("setStateForCollapsedObject(d, %s)", p.flattenGetterWithParent("res"))
p.StateSetter = &collapseSS
}
collapseSG := fmt.Sprintf("expand%s%sCollapsed(d)", p.resource.PathType(), p.PackagePath())
p.StateGetter = &collapseSG
}
// Add any new imports as needed
if ls := p.GetRequiredFileImports(); len(ls) > 0 {
resource.additionalFileImportSet.Add(ls...)
}
csgd := CustomStateGetterDetails{}
csgdOk, err := overrides.PropertyOverrideWithDetails(CustomStateGetter, p, &csgd, location)
if err != nil {
return nil, fmt.Errorf("failed to decode custom state getter details with err %v", err)
}
if csgdOk {
p.StateGetter = &csgd.Function
}
if overrides.PropertyOverride(EnumBool, p, location) {
p.EnumBool = true
p.Type.typ.Type = "string"
var parent string
if p.parent == nil {
parent = "res"
} else {
parent = "obj"
}
enumBoolSS := fmt.Sprintf("d.Set(%q, tpgdclresource.FlattenEnumBool(%s.%s))", p.Name(), parent, p.PackageName)
p.StateSetter = &enumBoolSS
enumBoolSG := fmt.Sprintf("tpgdclresource.ExpandEnumBool(d.Get(%q))", p.Name())
p.StateGetter = &enumBoolSG
}
if overrides.PropertyOverride(GenerateIfNotSet, p, location) {
p.Computed = true
p.Required = false
p.Optional = true
ig := fmt.Sprintf("generateIfNotSet(d, %q, %q)", p.Name(), "tfgen")
p.IdentityGetter = &ig
n := fmt.Sprintf("&%s", p.Name())
p.StateGetter = &n
}
if overrides.PropertyOverride(NamePrefix, p, location) {
p.Computed = true
p.Required = false
p.Optional = true
ig := fmt.Sprintf("generateIfNotSet(d, %q, d.Get(%q).(string))", p.Name(), "name_prefix")
p.IdentityGetter = &ig
n := fmt.Sprintf("&%s", p.Name())
p.StateGetter = &n
// plus, add the "name_prefix" property.
props = append(props, Property{
title: "name_prefix",
Type: p.Type,
resource: resource,
parent: parent,
Optional: true,
Computed: true,
ForceNew: true,
})
}
if p.ref != "" {
resource.ReusedTypes = resource.RegisterReusedType(p)
}
// Add the "effective_labels" property when the current property is top level "labels" or
// add the "effective_annotations" property when the current property is top level "annotations"
if p.IsResourceLabels() || p.IsResourceAnnotations() {
p.Description = fmt.Sprintf("%s\n\n%s", p.Description, get_labels_field_note(p.title))
p.Settable = false
p.StateGetter = nil
props = append(props, build_effective_labels_field(p, resource, parent))
if p.IsResourceLabels() {
props = append(props, build_terraform_labels_field(p, resource, parent))
p.ForceNew = false
}
}
props = append(props, p)
}
// handle conflict fields
for i, _ := range props {
p := &props[i]
if p.JSONCaseConflictsWith != nil {
for _, cf := range p.JSONCaseConflictsWith {
if val, ok := conflictsMap[cf]; ok {
p.ConflictsWith = append(p.ConflictsWith, val)
} else {
return nil, fmt.Errorf("Error generating conflict fields. %s is not labeled as a conflict field in DCL", cf)
}
}
}
}
// sort the properties so they're in a nice order
sort.SliceStable(props, propComparator(props))
return props, nil
}
func build_effective_labels_field(p Property, resource *Resource, parent *Property) Property {
title := fmt.Sprintf("effective_%s", p.title)
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.", p.title, p.title)
stateSetter := fmt.Sprintf("d.Set(%q, res.%s)", title, p.PackageName)
effectiveLabels := Property{
title: title,
Type: p.Type,
Description: description,
resource: resource,
parent: parent,
Optional: false,
Computed: true,
ForceNew: p.ForceNew, // Add ForceNew property if labels field has it
PackageName: p.PackageName,
Settable: true,
StateSetter: &stateSetter,
}
stateGetter := effectiveLabels.DefaultStateGetter()
effectiveLabels.StateGetter = &stateGetter
return effectiveLabels
}
func build_terraform_labels_field(p Property, resource *Resource, parent *Property) Property {
title := fmt.Sprintf("terraform_%s", p.title)
description := fmt.Sprintf("The combination of %s configured directly on the resource and default %s configured on the provider.", p.title, p.title)
stateSetter := fmt.Sprintf("d.Set(%q, flatten%sTerraform%s(res.%s, d))", title, p.resource.PathType(), p.PackagePath(), p.PackageName)
return Property{
title: title,
Type: p.Type,
Description: description,
resource: resource,
parent: parent,
Computed: true,
PackageName: p.PackageName,
StateSetter: &stateSetter,
}
}
func get_labels_field_note(title string) string {
return fmt.Sprintf("**Note**: This field is non-authoritative, and will only manage the %s present in your configuration.\nPlease refer to the field `effective_%s` for all of the %s present on the resource.", title, title, title)
}