github.com/crossplane/upjet@v1.3.0/pkg/types/conversion/tfjson/tfjson.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package tfjson
     6  
     7  import (
     8  	tfjson "github.com/hashicorp/terraform-json"
     9  	schemav2 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    10  	"github.com/pkg/errors"
    11  	"github.com/zclconf/go-cty/cty"
    12  )
    13  
    14  // GetV2ResourceMap converts input resource schemas with
    15  // "terraform-json" representation to terraform-plugin-sdk representation which
    16  // is what Upjet expects today.
    17  //
    18  // What we are trying to achieve here is to convert a lower level
    19  // representation of resource schema map, e.g. output of `terraform providers schema -json`
    20  // to plugin sdk representation. This is mostly the opposite of what the
    21  // following method is doing: https://github.com/hashicorp/terraform-plugin-sdk/blob/7e0a333644f1971a936995677b7a106140a0659f/helper/schema/core_schema.go#L43
    22  //
    23  // Ideally, we should not rely on plugin SDK types in Upjet at all but only
    24  // work with types in https://github.com/hashicorp/terraform-json which is
    25  // there exactly for this purpose, an external representation of Terraform
    26  // schemas. This conversion aims to be an intermediate step for that ultimate
    27  // goal.
    28  func GetV2ResourceMap(resourceSchemas map[string]*tfjson.Schema) map[string]*schemav2.Resource {
    29  	v2map := make(map[string]*schemav2.Resource, len(resourceSchemas))
    30  	for k, v := range resourceSchemas {
    31  		v2map[k] = v2ResourceFromTFJSONSchema(v)
    32  	}
    33  	return v2map
    34  }
    35  
    36  func v2ResourceFromTFJSONSchema(s *tfjson.Schema) *schemav2.Resource {
    37  	v2Res := &schemav2.Resource{SchemaVersion: int(s.Version)}
    38  	if s.Block == nil {
    39  		return v2Res
    40  	}
    41  
    42  	toSchemaMap := make(map[string]*schemav2.Schema, len(s.Block.Attributes)+len(s.Block.NestedBlocks))
    43  
    44  	for k, v := range s.Block.Attributes {
    45  		toSchemaMap[k] = tfJSONAttributeToV2Schema(v)
    46  	}
    47  	for k, v := range s.Block.NestedBlocks {
    48  		// CRUD timeouts are not part of the generated MR API,
    49  		// they cannot be dynamically configured and they are determined by either
    50  		// the underlying Terraform resource configuration or the upjet resource
    51  		// configuration. Please also see config.Resource.OperationTimeouts.
    52  		if k == schemav2.TimeoutsConfigKey {
    53  			continue
    54  		}
    55  		toSchemaMap[k] = tfJSONBlockTypeToV2Schema(v)
    56  	}
    57  
    58  	v2Res.Schema = toSchemaMap
    59  	v2Res.Description = s.Block.Description
    60  	v2Res.DeprecationMessage = deprecatedMessage(s.Block.Deprecated)
    61  	return v2Res
    62  }
    63  
    64  func tfJSONAttributeToV2Schema(attr *tfjson.SchemaAttribute) *schemav2.Schema {
    65  	v2sch := &schemav2.Schema{
    66  		Optional:    attr.Optional,
    67  		Required:    attr.Required,
    68  		Description: attr.Description,
    69  		Computed:    attr.Computed,
    70  		Deprecated:  deprecatedMessage(attr.Deprecated),
    71  		Sensitive:   attr.Sensitive,
    72  	}
    73  	if err := schemaV2TypeFromCtyType(attr.AttributeType, v2sch); err != nil {
    74  		panic(err)
    75  	}
    76  	return v2sch
    77  }
    78  
    79  func tfJSONBlockTypeToV2Schema(nb *tfjson.SchemaBlockType) *schemav2.Schema { //nolint:gocyclo
    80  	v2sch := &schemav2.Schema{
    81  		MinItems: int(nb.MinItems),
    82  		MaxItems: int(nb.MaxItems),
    83  	}
    84  	// Note(turkenh): Schema representation returned by the cli for block types
    85  	// does not have optional or computed fields. So, we are trying to infer
    86  	// those fields by doing the opposite of what is done here:
    87  	// https://github.com/hashicorp/terraform-plugin-sdk/blob/6461ac6e9044a44157c4e2c8aec0f1ab7efc2055/helper/schema/core_schema.go#L204
    88  	v2sch.Computed = false
    89  	v2sch.Optional = false
    90  	if nb.MinItems == 0 {
    91  		v2sch.Optional = true
    92  	}
    93  	if nb.MinItems == 0 && nb.MaxItems == 0 {
    94  		v2sch.Computed = true
    95  	}
    96  
    97  	switch nb.NestingMode { //nolint:exhaustive
    98  	case tfjson.SchemaNestingModeSet:
    99  		v2sch.Type = schemav2.TypeSet
   100  	case tfjson.SchemaNestingModeList:
   101  		v2sch.Type = schemav2.TypeList
   102  	case tfjson.SchemaNestingModeMap:
   103  		v2sch.Type = schemav2.TypeMap
   104  	case tfjson.SchemaNestingModeSingle:
   105  		v2sch.Type = schemav2.TypeList
   106  		v2sch.MinItems = 0
   107  		v2sch.Required = hasRequiredChild(nb)
   108  		v2sch.Optional = !v2sch.Required
   109  		if v2sch.Required {
   110  			v2sch.MinItems = 1
   111  		}
   112  		v2sch.MaxItems = 1
   113  	default:
   114  		panic("unhandled nesting mode: " + nb.NestingMode)
   115  	}
   116  
   117  	if nb.Block == nil {
   118  		return v2sch
   119  	}
   120  
   121  	v2sch.Description = nb.Block.Description
   122  	v2sch.Deprecated = deprecatedMessage(nb.Block.Deprecated)
   123  
   124  	res := &schemav2.Resource{}
   125  	res.Schema = make(map[string]*schemav2.Schema, len(nb.Block.Attributes)+len(nb.Block.NestedBlocks))
   126  	for key, attr := range nb.Block.Attributes {
   127  		res.Schema[key] = tfJSONAttributeToV2Schema(attr)
   128  	}
   129  	for key, block := range nb.Block.NestedBlocks {
   130  		// Please note that unlike the resource-level CRUD timeout configuration
   131  		// blocks (as mentioned above), we will generate the timeouts parameters
   132  		// for any nested configuration blocks, *if they exist*.
   133  		// We can prevent them here, but they are different than the resource's
   134  		// top-level CRUD timeouts, so we have opted to generate them.
   135  		res.Schema[key] = tfJSONBlockTypeToV2Schema(block)
   136  	}
   137  	v2sch.Elem = res
   138  	return v2sch
   139  }
   140  
   141  // checks whether the given tfjson.SchemaBlockType has any required children.
   142  // Children which are themselves blocks (nested blocks) are
   143  // checked recursively.
   144  func hasRequiredChild(nb *tfjson.SchemaBlockType) bool {
   145  	if nb.Block == nil {
   146  		return false
   147  	}
   148  	for _, a := range nb.Block.Attributes {
   149  		if a == nil {
   150  			continue
   151  		}
   152  		if a.Required {
   153  			return true
   154  		}
   155  	}
   156  	for _, b := range nb.Block.NestedBlocks {
   157  		if b == nil {
   158  			continue
   159  		}
   160  		if hasRequiredChild(b) {
   161  			return true
   162  		}
   163  	}
   164  	return false
   165  }
   166  
   167  func schemaV2TypeFromCtyType(typ cty.Type, schema *schemav2.Schema) error { //nolint:gocyclo
   168  	configMode := schemav2.SchemaConfigModeAuto
   169  
   170  	switch {
   171  	case typ.IsPrimitiveType():
   172  		schema.Type = primitiveToV2SchemaType(typ)
   173  	case typ.IsCollectionType():
   174  		var elemType any
   175  		et := typ.ElementType()
   176  		switch {
   177  		case et.IsPrimitiveType():
   178  			elemType = &schemav2.Schema{
   179  				Type:     primitiveToV2SchemaType(et),
   180  				Computed: schema.Computed,
   181  				Optional: schema.Optional,
   182  			}
   183  		case et.IsCollectionType():
   184  			elemType = &schemav2.Schema{
   185  				Type:     collectionToV2SchemaType(et),
   186  				Computed: schema.Computed,
   187  				Optional: schema.Optional,
   188  			}
   189  			if err := schemaV2TypeFromCtyType(et, elemType.(*schemav2.Schema)); err != nil {
   190  				return err
   191  			}
   192  		case et.IsObjectType():
   193  			configMode = schemav2.SchemaConfigModeAttr
   194  			res := &schemav2.Resource{}
   195  			res.Schema = make(map[string]*schemav2.Schema, len(et.AttributeTypes()))
   196  			for key, attrTyp := range et.AttributeTypes() {
   197  				sch := &schemav2.Schema{
   198  					Computed: schema.Computed,
   199  					Optional: schema.Optional,
   200  				}
   201  				if et.AttributeOptional(key) {
   202  					sch.Optional = true
   203  				}
   204  
   205  				if err := schemaV2TypeFromCtyType(attrTyp, sch); err != nil {
   206  					return err
   207  				}
   208  				res.Schema[key] = sch
   209  			}
   210  			elemType = res
   211  		default:
   212  			return errors.Errorf("unexpected cty.Type %s", typ.GoString())
   213  		}
   214  		schema.ConfigMode = configMode
   215  		schema.Type = collectionToV2SchemaType(typ)
   216  		schema.Elem = elemType
   217  	case typ.IsTupleType():
   218  		return errors.New("cannot convert cty TupleType to schema v2 type")
   219  	case typ.Equals(cty.DynamicPseudoType):
   220  		return errors.New("cannot convert cty DynamicPseudoType to schema v2 type")
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  func primitiveToV2SchemaType(typ cty.Type) schemav2.ValueType {
   227  	switch {
   228  	case typ.Equals(cty.String):
   229  		return schemav2.TypeString
   230  	case typ.Equals(cty.Number):
   231  		// TODO(turkenh): Figure out handling floats with IntOrString on type
   232  		//  builder side
   233  		return schemav2.TypeFloat
   234  	case typ.Equals(cty.Bool):
   235  		return schemav2.TypeBool
   236  	}
   237  	return schemav2.TypeInvalid
   238  }
   239  
   240  func collectionToV2SchemaType(typ cty.Type) schemav2.ValueType {
   241  	switch {
   242  	case typ.IsSetType():
   243  		return schemav2.TypeSet
   244  	case typ.IsListType():
   245  		return schemav2.TypeList
   246  	case typ.IsMapType():
   247  		return schemav2.TypeMap
   248  	}
   249  	return schemav2.TypeInvalid
   250  }
   251  
   252  func deprecatedMessage(deprecated bool) string {
   253  	if deprecated {
   254  		return "deprecated"
   255  	}
   256  	return ""
   257  }