From 72ab3c3c11ad9010cd725d7d1a81749c28e82c08 Mon Sep 17 00:00:00 2001 From: mclacore Date: Wed, 2 Oct 2024 16:10:50 -0700 Subject: [PATCH 1/7] nesting default values inline --- .../testdata/opentofu/simple/schema.json | 232 +++++++++++------- .../testdata/opentofu/simple/variables.tf | 44 ++-- pkg/opentofu/tofutoschema.go | 76 ++++-- 3 files changed, 219 insertions(+), 133 deletions(-) diff --git a/pkg/opentofu/testdata/opentofu/simple/schema.json b/pkg/opentofu/testdata/opentofu/simple/schema.json index 51f66b9..7866c1a 100644 --- a/pkg/opentofu/testdata/opentofu/simple/schema.json +++ b/pkg/opentofu/testdata/opentofu/simple/schema.json @@ -1,97 +1,139 @@ { - "required": [ - "nodescription", - "testbool", - "testemptybool", - "testlist", - "testmap", - "testnumber", - "testobject", - "testset", - "teststring" - ], - "properties": { - "teststring": { - "title": "teststring", - "type": "string", - "description": "An example string variable", - "default": "string value" - }, - "testnumber": { - "title": "testnumber", - "type": "number", - "description": "An example number variable", - "default": 20 - }, - "testbool": { - "title": "testbool", - "type": "boolean", - "description": "An example bool variable", - "default": false - }, - "testemptybool": { - "title": "testemptybool", - "type": "boolean", - "description": "An example empty bool variable", - "default": false - }, - "testobject": { - "title": "testobject", - "type": "object", - "properties": { - "name": { - "title": "name", - "type": "string" - }, - "address": { - "title": "address", - "type": "string" - }, - "age": { - "title": "age", - "type": "number" - } - }, - "required": [ - "name" - ], - "description": "An example object variable", - "default": { - "name": "Bob", - "address": "123 Bob St." - } - }, - "testlist": { - "title": "testlist", - "type": "array", - "description": "An example list variable", - "items": { - "type": "string" - } - }, - "testset": { - "title": "testset", - "type": "array", - "uniqueItems": true, - "description": "An example set variable", - "items": { - "type": "string" - } - }, - "testmap": { - "title": "testmap", - "type": "object", - "description": "An example map variable", - "propertyNames": { - "pattern": "^.*$" - }, - "additionalProperties": { - "type": "string" - } - }, - "nodescription": { - "title": "nodescription", - "type": "string" - } - } + "required": [ + "nodescription", + "testbool", + "testemptybool", + "testlist", + "testmap", + "testnestedobject", + "testnumber", + "testobject", + "testset", + "teststring" + ], + "properties": { + "teststring": { + "title": "teststring", + "type": "string", + "description": "An example string variable", + "default": "string value" + }, + "testnumber": { + "title": "testnumber", + "type": "number", + "description": "An example number variable", + "default": 20 + }, + "testbool": { + "title": "testbool", + "type": "boolean", + "description": "An example bool variable", + "default": false + }, + "testemptybool": { + "title": "testemptybool", + "type": "boolean", + "description": "An example empty bool variable", + "default": false + }, + "testobject": { + "title": "testobject", + "type": "object", + "properties": { + "name": { + "title": "name", + "type": "string" + }, + "address": { + "title": "address", + "type": "string" + }, + "age": { + "title": "age", + "type": "number" + } + }, + "required": [ + "name" + ], + "description": "An example object variable", + "default": { + "name": "Bob", + "address": "123 Bob St." + } + }, + "testnestedobject": { + "title": "testnestedobject", + "description": "An example nested object variable", + "type": "object", + "properties": { + "name": { + "title": "name", + "type": "string" + }, + "address": { + "title": "address", + "type": "string", + "default": "123 Bob St." + }, + "age": { + "title": "age", + "type": "number", + "default": 30 + }, + "dead": { + "title": "dead", + "type": "boolean", + "default": false + }, + "phones": { + "title": "phones", + "type": "object", + "properties": { + "home": { + "title": "home", + "type": "string" + }, + "work": { + "title": "work", + "type": "string", + "default": "123-456-7891" + } + } + } + } + }, + "testlist": { + "title": "testlist", + "type": "array", + "description": "An example list variable", + "items": { + "type": "string" + } + }, + "testset": { + "title": "testset", + "type": "array", + "uniqueItems": true, + "description": "An example set variable", + "items": { + "type": "string" + } + }, + "testmap": { + "title": "testmap", + "type": "object", + "description": "An example map variable", + "propertyNames": { + "pattern": "^.*$" + }, + "additionalProperties": { + "type": "string" + } + }, + "nodescription": { + "title": "nodescription", + "type": "string" + } + } } diff --git a/pkg/opentofu/testdata/opentofu/simple/variables.tf b/pkg/opentofu/testdata/opentofu/simple/variables.tf index d40b2b3..bdfa352 100644 --- a/pkg/opentofu/testdata/opentofu/simple/variables.tf +++ b/pkg/opentofu/testdata/opentofu/simple/variables.tf @@ -1,24 +1,24 @@ variable "teststring" { - type = string - description = "An example string variable" - default = "string value" + type = string + description = "An example string variable" + default = "string value" } variable "testnumber" { - type = number - description = "An example number variable" - default = 20 + type = number + description = "An example number variable" + default = 20 } variable "testbool" { - type = bool - description = "An example bool variable" - default = false + type = bool + description = "An example bool variable" + default = false } variable "testemptybool" { - type = bool - description = "An example empty bool variable" + type = bool + description = "An example empty bool variable" } variable "testobject" { @@ -29,24 +29,38 @@ variable "testobject" { }) description = "An example object variable" default = { - name = "Bob" + name = "Bob" address = "123 Bob St." } sensitive = true } +variable "testnestedobject" { + type = object({ + name = string + address = optional(string, "123 Bob St.") + age = optional(number, 30) + dead = optional(bool, false) + phones = object({ + home = string + work = optional(string, "123-456-7891") + }) + }) + description = "An example nested object variable" +} + variable "testlist" { - type = list(string) + type = list(string) description = "An example list variable" } variable "testset" { - type = set(string) + type = set(string) description = "An example set variable" } variable "testmap" { - type = map(string) + type = map(string) description = "An example map variable" } diff --git a/pkg/opentofu/tofutoschema.go b/pkg/opentofu/tofutoschema.go index e697d5c..23fbf3b 100644 --- a/pkg/opentofu/tofutoschema.go +++ b/pkg/opentofu/tofutoschema.go @@ -38,11 +38,11 @@ func TofuToSchema(modulePath string) (*schema.Schema, error) { func variableToSchema(variable *tfconfig.Variable) (*schema.Schema, error) { schema := new(schema.Schema) - variableType, err := variableTypeStringToCtyType(variable.Type) + variableType, def, err := variableTypeStringToCtyType(variable.Type) if err != nil { return nil, err } - err = hydrateSchemaFromNameAndType(variable.Name, variableType, schema) + err = hydrateSchemaFromNameAndType(variable.Name, variableType, schema, def) if err != nil { return nil, err } @@ -57,55 +57,85 @@ func variableToSchema(variable *tfconfig.Variable) (*schema.Schema, error) { schema.Default = false } + // fmt.Printf("Variable type: %v\nVariable name: %v\n", variable.Type, variable.Name) + return schema, nil } -func variableTypeStringToCtyType(variableType string) (cty.Type, error) { +func variableTypeStringToCtyType(variableType string) (cty.Type, *typeexpr.Defaults, error) { expr, diags := hclsyntax.ParseExpression([]byte(variableType), "", hcl.Pos{Line: 1, Column: 1}) if len(diags) != 0 { - return cty.NilType, errors.New(diags.Error()) + return cty.NilType, nil, errors.New(diags.Error()) } - ty, diags := typeexpr.TypeConstraint(expr) + ty, def, diags := typeexpr.TypeConstraintWithDefaults(expr) if len(diags) != 0 { - return cty.NilType, errors.New(diags.Error()) + return cty.NilType, nil, errors.New(diags.Error()) } - return ty, nil + return ty, def, nil } -func hydrateSchemaFromNameAndType(name string, ty cty.Type, schema *schema.Schema) error { +func hydrateSchemaFromNameAndType(name string, ty cty.Type, schema *schema.Schema, def *typeexpr.Defaults) error { if ty.IsPrimitiveType() { - hydratePrimitiveSchema(name, ty, schema) + hydratePrimitiveSchema(name, ty, schema, def) } else if ty.IsMapType() { - hydrateMapSchema(name, ty, schema) + hydrateMapSchema(name, ty, schema, def) } else if ty.IsObjectType() { - hydrateObjectSchema(name, ty, schema) + hydrateObjectSchema(name, ty, schema, def) } else if ty.IsListType() { - hydrateArraySchema(name, ty, schema) + hydrateArraySchema(name, ty, schema, def) } else if ty.IsSetType() { - hydrateSetSchema(name, ty, schema) + hydrateSetSchema(name, ty, schema, def) } return nil } -func hydratePrimitiveSchema(name string, ty cty.Type, schema *schema.Schema) { +func hydratePrimitiveSchema(name string, ty cty.Type, schema *schema.Schema, def *typeexpr.Defaults) { schema.Title = name switch ty { case cty.String: schema.Type = "string" + if def != nil { + if defVal, exists := def.DefaultValues[name]; exists { + schema.Default = defVal.AsString() + } + } case cty.Bool: schema.Type = "boolean" + if def != nil { + if defVal, exists := def.DefaultValues[name]; exists { + if defVal.True() { + schema.Default = true + } else { + schema.Default = false + } + } + } case cty.Number: schema.Type = "number" + if def != nil { + if defVal, exists := def.DefaultValues[name]; exists { + defNum, _ := defVal.AsBigFloat().Float64() + schema.Default = defNum + } + } } } -func hydrateObjectSchema(name string, ty cty.Type, sch *schema.Schema) { +func hydrateObjectSchema(name string, ty cty.Type, sch *schema.Schema, def *typeexpr.Defaults) { sch.Title = name sch.Type = "object" sch.Properties = orderedmap.New[string, *schema.Schema]() for attName, attType := range ty.AttributeTypes() { + var nestDef *typeexpr.Defaults + if def != nil { + if defVal, exists := def.Children[attName]; exists { + nestDef = defVal + } else { + nestDef = nil + } + } attributeSchema := new(schema.Schema) - hydrateSchemaFromNameAndType(attName, attType, attributeSchema) + hydrateSchemaFromNameAndType(attName, attType, attributeSchema, nestDef) sch.Properties.Set(attName, attributeSchema) if !ty.AttributeOptional(attName) { sch.Required = append(sch.Required, attName) @@ -114,25 +144,25 @@ func hydrateObjectSchema(name string, ty cty.Type, sch *schema.Schema) { slices.Sort(sch.Required) } -func hydrateMapSchema(name string, ty cty.Type, sch *schema.Schema) { +func hydrateMapSchema(name string, ty cty.Type, sch *schema.Schema, def *typeexpr.Defaults) { sch.Title = name sch.Type = "object" sch.PropertyNames = &schema.Schema{ Pattern: "^.*$", } sch.AdditionalProperties = new(schema.Schema) - hydrateSchemaFromNameAndType("", ty.ElementType(), sch.AdditionalProperties.(*schema.Schema)) + hydrateSchemaFromNameAndType("", ty.ElementType(), sch.AdditionalProperties.(*schema.Schema), def) } -func hydrateArraySchema(name string, ty cty.Type, sch *schema.Schema) { +func hydrateArraySchema(name string, ty cty.Type, sch *schema.Schema, def *typeexpr.Defaults) { sch.Title = name sch.Type = "array" sch.Items = new(schema.Schema) - hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items) + hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items, def) } -func hydrateSetSchema(name string, ty cty.Type, sch *schema.Schema) { - hydrateArraySchema(name, ty, sch) +func hydrateSetSchema(name string, ty cty.Type, sch *schema.Schema, def *typeexpr.Defaults) { + hydrateArraySchema(name, ty, sch, def) sch.UniqueItems = true - hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items) + hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items, def) } From cca8d4726a44e9e6db906c600b5d580b96495b17 Mon Sep 17 00:00:00 2001 From: chrisghill Date: Wed, 2 Oct 2024 23:36:29 -0600 Subject: [PATCH 2/7] mostly working, need to fix 'nil' on missing values in default object --- .../testdata/opentofu/simple/schema.json | 54 ++++++++ .../testdata/opentofu/simple/variables.tf | 21 ++- pkg/opentofu/tofutoschema.go | 121 ++++++++++-------- 3 files changed, 142 insertions(+), 54 deletions(-) diff --git a/pkg/opentofu/testdata/opentofu/simple/schema.json b/pkg/opentofu/testdata/opentofu/simple/schema.json index 7866c1a..0dc681c 100644 --- a/pkg/opentofu/testdata/opentofu/simple/schema.json +++ b/pkg/opentofu/testdata/opentofu/simple/schema.json @@ -66,6 +66,9 @@ "title": "testnestedobject", "description": "An example nested object variable", "type": "object", + "required": [ + "name" + ], "properties": { "name": { "title": "name", @@ -89,6 +92,12 @@ "phones": { "title": "phones", "type": "object", + "default": { + "home": "987-654-3210" + }, + "required": [ + "home" + ], "properties": { "home": { "title": "home", @@ -100,6 +109,51 @@ "default": "123-456-7891" } } + }, + "children": { + "title": "children", + "type": "array", + "default": [{ + "name": "bob", + "occupation": { + "company": "none", + "experience": 2 + } + }], + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "title": "name", + "type": "string" + }, + "occupation": { + "title": "occupation", + "type": "object", + "default": { + "company": "Massdriver", + "experience": 1 + }, + "required": [ + "company" + ], + "properties": { + "company": { + "title": "company", + "type": "string" + }, + "experience": { + "title": "experience", + "type": "number", + "default": 0 + } + } + } + } + } } } }, diff --git a/pkg/opentofu/testdata/opentofu/simple/variables.tf b/pkg/opentofu/testdata/opentofu/simple/variables.tf index bdfa352..e681ab8 100644 --- a/pkg/opentofu/testdata/opentofu/simple/variables.tf +++ b/pkg/opentofu/testdata/opentofu/simple/variables.tf @@ -41,10 +41,29 @@ variable "testnestedobject" { address = optional(string, "123 Bob St.") age = optional(number, 30) dead = optional(bool, false) - phones = object({ + phones = optional(object({ home = string work = optional(string, "123-456-7891") + }), { + home = "987-654-3210" }) + children = optional(list(object({ + name = string + occupation = optional(object({ + company = string + experience = optional(number, 0) + }), { + company = "Massdriver" + experience = 1 + }) + })), [{ + name = "bob" + occupation = { + company = "none", + experience = 2 + } + }] + ) }) description = "An example nested object variable" } diff --git a/pkg/opentofu/tofutoschema.go b/pkg/opentofu/tofutoschema.go index 23fbf3b..4733406 100644 --- a/pkg/opentofu/tofutoschema.go +++ b/pkg/opentofu/tofutoschema.go @@ -1,7 +1,9 @@ package opentofu import ( + "encoding/json" "errors" + "fmt" "slices" "github.com/hashicorp/hcl/v2" @@ -11,6 +13,7 @@ import ( "github.com/massdriver-cloud/terraform-config-inspect/tfconfig" orderedmap "github.com/wk8/go-ordered-map/v2" "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" ) func TofuToSchema(modulePath string) (*schema.Schema, error) { @@ -38,11 +41,20 @@ func TofuToSchema(modulePath string) (*schema.Schema, error) { func variableToSchema(variable *tfconfig.Variable) (*schema.Schema, error) { schema := new(schema.Schema) - variableType, def, err := variableTypeStringToCtyType(variable.Type) + variableType, defaults, err := variableTypeStringToCtyType(variable.Type) if err != nil { return nil, err } - err = hydrateSchemaFromNameAndType(variable.Name, variableType, schema, def) + var topLevelDefault *typeexpr.Defaults + if defaults != nil { + // To simplify the logic of recursively walking the Defaults structure in objects types, + // we make the extracted Defaults a Child of a dummy "top level" node + topLevelDefault = new(typeexpr.Defaults) + topLevelDefault.Children = map[string]*typeexpr.Defaults{ + variable.Name: defaults, + } + } + err = hydrateSchemaFromNameAndType(variable.Name, variableType, schema, topLevelDefault) if err != nil { return nil, err } @@ -74,68 +86,46 @@ func variableTypeStringToCtyType(variableType string) (cty.Type, *typeexpr.Defau return ty, def, nil } -func hydrateSchemaFromNameAndType(name string, ty cty.Type, schema *schema.Schema, def *typeexpr.Defaults) error { +func hydrateSchemaFromNameAndType(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) error { + sch.Title = name + + if defaults != nil { + if defVal, exists := defaults.DefaultValues[name]; exists { + sch.Default = ctyValueToInterface(defVal) + } + } + if ty.IsPrimitiveType() { - hydratePrimitiveSchema(name, ty, schema, def) + hydratePrimitiveSchema(ty, sch) } else if ty.IsMapType() { - hydrateMapSchema(name, ty, schema, def) + hydrateMapSchema(name, ty, sch, defaults) } else if ty.IsObjectType() { - hydrateObjectSchema(name, ty, schema, def) + hydrateObjectSchema(name, ty, sch, defaults) } else if ty.IsListType() { - hydrateArraySchema(name, ty, schema, def) + hydrateArraySchema(name, ty, sch, defaults) } else if ty.IsSetType() { - hydrateSetSchema(name, ty, schema, def) + hydrateSetSchema(name, ty, sch, defaults) } return nil } -func hydratePrimitiveSchema(name string, ty cty.Type, schema *schema.Schema, def *typeexpr.Defaults) { - schema.Title = name +func hydratePrimitiveSchema(ty cty.Type, sch *schema.Schema) { switch ty { case cty.String: - schema.Type = "string" - if def != nil { - if defVal, exists := def.DefaultValues[name]; exists { - schema.Default = defVal.AsString() - } - } + sch.Type = "string" case cty.Bool: - schema.Type = "boolean" - if def != nil { - if defVal, exists := def.DefaultValues[name]; exists { - if defVal.True() { - schema.Default = true - } else { - schema.Default = false - } - } - } + sch.Type = "boolean" case cty.Number: - schema.Type = "number" - if def != nil { - if defVal, exists := def.DefaultValues[name]; exists { - defNum, _ := defVal.AsBigFloat().Float64() - schema.Default = defNum - } - } + sch.Type = "number" } } -func hydrateObjectSchema(name string, ty cty.Type, sch *schema.Schema, def *typeexpr.Defaults) { - sch.Title = name +func hydrateObjectSchema(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) { sch.Type = "object" sch.Properties = orderedmap.New[string, *schema.Schema]() for attName, attType := range ty.AttributeTypes() { - var nestDef *typeexpr.Defaults - if def != nil { - if defVal, exists := def.Children[attName]; exists { - nestDef = defVal - } else { - nestDef = nil - } - } attributeSchema := new(schema.Schema) - hydrateSchemaFromNameAndType(attName, attType, attributeSchema, nestDef) + hydrateSchemaFromNameAndType(attName, attType, attributeSchema, getDefaultChildren(name, defaults)) sch.Properties.Set(attName, attributeSchema) if !ty.AttributeOptional(attName) { sch.Required = append(sch.Required, attName) @@ -144,25 +134,50 @@ func hydrateObjectSchema(name string, ty cty.Type, sch *schema.Schema, def *type slices.Sort(sch.Required) } -func hydrateMapSchema(name string, ty cty.Type, sch *schema.Schema, def *typeexpr.Defaults) { - sch.Title = name +func hydrateMapSchema(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) { sch.Type = "object" sch.PropertyNames = &schema.Schema{ Pattern: "^.*$", } sch.AdditionalProperties = new(schema.Schema) - hydrateSchemaFromNameAndType("", ty.ElementType(), sch.AdditionalProperties.(*schema.Schema), def) + hydrateSchemaFromNameAndType("", ty.ElementType(), sch.AdditionalProperties.(*schema.Schema), getDefaultChildren(name, defaults)) } -func hydrateArraySchema(name string, ty cty.Type, sch *schema.Schema, def *typeexpr.Defaults) { - sch.Title = name +func hydrateArraySchema(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) { sch.Type = "array" sch.Items = new(schema.Schema) - hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items, def) + hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items, getDefaultChildren(name, defaults)) } -func hydrateSetSchema(name string, ty cty.Type, sch *schema.Schema, def *typeexpr.Defaults) { - hydrateArraySchema(name, ty, sch, def) +func hydrateSetSchema(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) { + hydrateArraySchema(name, ty, sch, defaults) sch.UniqueItems = true - hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items, def) + hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items, getDefaultChildren(name, defaults)) +} + +func ctyValueToInterface(val cty.Value) interface{} { + valJSON, err := ctyjson.Marshal(val, val.Type()) + if err != nil { + // Should never happen, since all possible known + // values have a JSON mapping. + panic(fmt.Errorf("failed to serialize default value as JSON: %s", err)) + } + var def interface{} + err = json.Unmarshal(valJSON, &def) + if err != nil { + // Again should never happen, because valJSON is + // guaranteed valid by ctyjson.Marshal. + panic(fmt.Errorf("failed to re-parse default value from JSON: %s", err)) + } + return def +} + +func getDefaultChildren(name string, defaults *typeexpr.Defaults) *typeexpr.Defaults { + var children *typeexpr.Defaults + if defaults != nil { + if attDefaultVal, exists := defaults.Children[name]; exists { + children = attDefaultVal + } + } + return children } From 6822ed140aa8c5bc2841399449d023282d50b3ad Mon Sep 17 00:00:00 2001 From: chrisghill Date: Thu, 3 Oct 2024 10:55:23 -0600 Subject: [PATCH 3/7] small tweaks --- cmd/root.go | 1 + pkg/opentofu/tofutoschema.go | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 5428dfc..64fec27 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -32,6 +32,7 @@ var rootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { + rootCmd.AddCommand(NewCmdBicep()) rootCmd.AddCommand(NewCmdHelm()) rootCmd.AddCommand(NewCmdOpenTofu()) rootCmd.AddCommand(NewCmdValidate()) diff --git a/pkg/opentofu/tofutoschema.go b/pkg/opentofu/tofutoschema.go index 4733406..7cb1776 100644 --- a/pkg/opentofu/tofutoschema.go +++ b/pkg/opentofu/tofutoschema.go @@ -45,10 +45,10 @@ func variableToSchema(variable *tfconfig.Variable) (*schema.Schema, error) { if err != nil { return nil, err } + // To simplify the logic of recursively walking the Defaults structure in objects types, + // we make the extracted Defaults a Child of a dummy "top level" node var topLevelDefault *typeexpr.Defaults if defaults != nil { - // To simplify the logic of recursively walking the Defaults structure in objects types, - // we make the extracted Defaults a Child of a dummy "top level" node topLevelDefault = new(typeexpr.Defaults) topLevelDefault.Children = map[string]*typeexpr.Defaults{ variable.Name: defaults, @@ -69,8 +69,6 @@ func variableToSchema(variable *tfconfig.Variable) (*schema.Schema, error) { schema.Default = false } - // fmt.Printf("Variable type: %v\nVariable name: %v\n", variable.Type, variable.Name) - return schema, nil } @@ -181,3 +179,15 @@ func getDefaultChildren(name string, defaults *typeexpr.Defaults) *typeexpr.Defa } return children } + +func removeNullKeys(foo map[string]interface{}) { + for key, value := range foo { + if value == nil { + delete(foo, key) + continue + } + if bar, ok := foo[key].(map[string]interface{}); ok { + removeNullKeys(bar) + } + } +} From 5db6bdf9cd0779b0d5b345f4c4afad5f77c4e991 Mon Sep 17 00:00:00 2001 From: mclacore Date: Thu, 3 Oct 2024 14:26:37 -0700 Subject: [PATCH 4/7] fixed bug with default object * updated tests to include more test cases * fixed bug where default object was being assumed to have values that only exist within in-line field declarations --- .../testdata/opentofu/simple/schema.json | 23 +++++++--- .../testdata/opentofu/simple/variables.tf | 19 ++++---- pkg/opentofu/tofutoschema.go | 45 ++++++++++--------- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/pkg/opentofu/testdata/opentofu/simple/schema.json b/pkg/opentofu/testdata/opentofu/simple/schema.json index 0dc681c..037a068 100644 --- a/pkg/opentofu/testdata/opentofu/simple/schema.json +++ b/pkg/opentofu/testdata/opentofu/simple/schema.json @@ -113,13 +113,16 @@ "children": { "title": "children", "type": "array", - "default": [{ - "name": "bob", - "occupation": { - "company": "none", - "experience": 2 + "default": [ + { + "name": "bob", + "occupation": { + "company": "none", + "experience": 2, + "manager": true + } } - }], + ], "items": { "type": "object", "required": [ @@ -135,7 +138,8 @@ "type": "object", "default": { "company": "Massdriver", - "experience": 1 + "experience": 1, + "manager": false }, "required": [ "company" @@ -149,6 +153,11 @@ "title": "experience", "type": "number", "default": 0 + }, + "manager": { + "title": "manager", + "type": "boolean", + "default": false } } } diff --git a/pkg/opentofu/testdata/opentofu/simple/variables.tf b/pkg/opentofu/testdata/opentofu/simple/variables.tf index e681ab8..182f718 100644 --- a/pkg/opentofu/testdata/opentofu/simple/variables.tf +++ b/pkg/opentofu/testdata/opentofu/simple/variables.tf @@ -41,28 +41,31 @@ variable "testnestedobject" { address = optional(string, "123 Bob St.") age = optional(number, 30) dead = optional(bool, false) - phones = optional(object({ + phones = optional(object({ home = string work = optional(string, "123-456-7891") - }), { + }), { home = "987-654-3210" }) children = optional(list(object({ - name = string + name = string occupation = optional(object({ company = string - experience = optional(number, 0) - }), { + experience = optional(number, 0), + manager = optional(bool, false) + }), { company = "Massdriver" experience = 1 + manager = false }) - })), [{ + })), [{ name = "bob" occupation = { company = "none", - experience = 2 + experience = 2, + manager = true } - }] + }] ) }) description = "An example nested object variable" diff --git a/pkg/opentofu/tofutoschema.go b/pkg/opentofu/tofutoschema.go index 7cb1776..9d2fd9e 100644 --- a/pkg/opentofu/tofutoschema.go +++ b/pkg/opentofu/tofutoschema.go @@ -54,7 +54,7 @@ func variableToSchema(variable *tfconfig.Variable) (*schema.Schema, error) { variable.Name: defaults, } } - err = hydrateSchemaFromNameAndType(variable.Name, variableType, schema, topLevelDefault) + err = hydrateSchemaFromNameTypeAndDefaults(schema, variable.Name, variableType, topLevelDefault) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func variableTypeStringToCtyType(variableType string) (cty.Type, *typeexpr.Defau return ty, def, nil } -func hydrateSchemaFromNameAndType(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) error { +func hydrateSchemaFromNameTypeAndDefaults(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) error { sch.Title = name if defaults != nil { @@ -96,13 +96,13 @@ func hydrateSchemaFromNameAndType(name string, ty cty.Type, sch *schema.Schema, if ty.IsPrimitiveType() { hydratePrimitiveSchema(ty, sch) } else if ty.IsMapType() { - hydrateMapSchema(name, ty, sch, defaults) + hydrateMapSchema(sch, name, ty, defaults) } else if ty.IsObjectType() { - hydrateObjectSchema(name, ty, sch, defaults) + hydrateObjectSchema(sch, name, ty, defaults) } else if ty.IsListType() { - hydrateArraySchema(name, ty, sch, defaults) + hydrateArraySchema(sch, name, ty, defaults) } else if ty.IsSetType() { - hydrateSetSchema(name, ty, sch, defaults) + hydrateSetSchema(sch, name, ty, defaults) } return nil } @@ -118,12 +118,12 @@ func hydratePrimitiveSchema(ty cty.Type, sch *schema.Schema) { } } -func hydrateObjectSchema(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) { +func hydrateObjectSchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) { sch.Type = "object" sch.Properties = orderedmap.New[string, *schema.Schema]() for attName, attType := range ty.AttributeTypes() { attributeSchema := new(schema.Schema) - hydrateSchemaFromNameAndType(attName, attType, attributeSchema, getDefaultChildren(name, defaults)) + hydrateSchemaFromNameTypeAndDefaults(attributeSchema, attName, attType, getDefaultChildren(name, defaults)) sch.Properties.Set(attName, attributeSchema) if !ty.AttributeOptional(attName) { sch.Required = append(sch.Required, attName) @@ -132,25 +132,25 @@ func hydrateObjectSchema(name string, ty cty.Type, sch *schema.Schema, defaults slices.Sort(sch.Required) } -func hydrateMapSchema(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) { +func hydrateMapSchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) { sch.Type = "object" sch.PropertyNames = &schema.Schema{ Pattern: "^.*$", } sch.AdditionalProperties = new(schema.Schema) - hydrateSchemaFromNameAndType("", ty.ElementType(), sch.AdditionalProperties.(*schema.Schema), getDefaultChildren(name, defaults)) + hydrateSchemaFromNameTypeAndDefaults(sch.AdditionalProperties.(*schema.Schema), "", ty.ElementType(), getDefaultChildren(name, defaults)) } -func hydrateArraySchema(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) { +func hydrateArraySchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) { sch.Type = "array" sch.Items = new(schema.Schema) - hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items, getDefaultChildren(name, defaults)) + hydrateSchemaFromNameTypeAndDefaults(sch.Items, "", ty.ElementType(), getDefaultChildren(name, defaults)) } -func hydrateSetSchema(name string, ty cty.Type, sch *schema.Schema, defaults *typeexpr.Defaults) { - hydrateArraySchema(name, ty, sch, defaults) +func hydrateSetSchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) { + hydrateArraySchema(sch, name, ty, defaults) sch.UniqueItems = true - hydrateSchemaFromNameAndType("", ty.ElementType(), sch.Items, getDefaultChildren(name, defaults)) + hydrateSchemaFromNameTypeAndDefaults(sch.Items, "", ty.ElementType(), getDefaultChildren(name, defaults)) } func ctyValueToInterface(val cty.Value) interface{} { @@ -167,6 +167,7 @@ func ctyValueToInterface(val cty.Value) interface{} { // guaranteed valid by ctyjson.Marshal. panic(fmt.Errorf("failed to re-parse default value from JSON: %s", err)) } + removeNullKeys(def) return def } @@ -180,14 +181,18 @@ func getDefaultChildren(name string, defaults *typeexpr.Defaults) *typeexpr.Defa return children } -func removeNullKeys(foo map[string]interface{}) { - for key, value := range foo { +func removeNullKeys(defVal interface{}) { + assertedDefVal, ok := defVal.(map[string]interface{}) + if !ok { + return + } + for key, value := range assertedDefVal { if value == nil { - delete(foo, key) + delete(assertedDefVal, key) continue } - if bar, ok := foo[key].(map[string]interface{}); ok { - removeNullKeys(bar) + if valObj, ok := assertedDefVal[key].(map[string]interface{}); ok { + removeNullKeys(valObj) } } } From dde5818594d4352a3e2f962beda32f7ba8dfafb3 Mon Sep 17 00:00:00 2001 From: mclacore Date: Thu, 3 Oct 2024 14:31:16 -0700 Subject: [PATCH 5/7] Renaming def to defaults --- pkg/opentofu/tofutoschema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/opentofu/tofutoschema.go b/pkg/opentofu/tofutoschema.go index 9d2fd9e..e9b86c0 100644 --- a/pkg/opentofu/tofutoschema.go +++ b/pkg/opentofu/tofutoschema.go @@ -77,11 +77,11 @@ func variableTypeStringToCtyType(variableType string) (cty.Type, *typeexpr.Defau if len(diags) != 0 { return cty.NilType, nil, errors.New(diags.Error()) } - ty, def, diags := typeexpr.TypeConstraintWithDefaults(expr) + ty, defaults, diags := typeexpr.TypeConstraintWithDefaults(expr) if len(diags) != 0 { return cty.NilType, nil, errors.New(diags.Error()) } - return ty, def, nil + return ty, defaults, nil } func hydrateSchemaFromNameTypeAndDefaults(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) error { From f8d2eda6971cce3a53012161d38ab0612f0a5213 Mon Sep 17 00:00:00 2001 From: mclacore Date: Thu, 3 Oct 2024 15:34:09 -0700 Subject: [PATCH 6/7] Reordering inputs to match other hydrate helper functions --- pkg/opentofu/tofutoschema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/opentofu/tofutoschema.go b/pkg/opentofu/tofutoschema.go index e9b86c0..935037f 100644 --- a/pkg/opentofu/tofutoschema.go +++ b/pkg/opentofu/tofutoschema.go @@ -94,7 +94,7 @@ func hydrateSchemaFromNameTypeAndDefaults(sch *schema.Schema, name string, ty ct } if ty.IsPrimitiveType() { - hydratePrimitiveSchema(ty, sch) + hydratePrimitiveSchema(sch, ty) } else if ty.IsMapType() { hydrateMapSchema(sch, name, ty, defaults) } else if ty.IsObjectType() { @@ -107,7 +107,7 @@ func hydrateSchemaFromNameTypeAndDefaults(sch *schema.Schema, name string, ty ct return nil } -func hydratePrimitiveSchema(ty cty.Type, sch *schema.Schema) { +func hydratePrimitiveSchema(sch *schema.Schema, ty cty.Type) { switch ty { case cty.String: sch.Type = "string" From a31dafacf21097810be456c0cf88a9e252e887d3 Mon Sep 17 00:00:00 2001 From: mclacore Date: Thu, 3 Oct 2024 15:50:19 -0700 Subject: [PATCH 7/7] add comment for removenullkeys --- pkg/opentofu/tofutoschema.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/opentofu/tofutoschema.go b/pkg/opentofu/tofutoschema.go index 935037f..e27cfe7 100644 --- a/pkg/opentofu/tofutoschema.go +++ b/pkg/opentofu/tofutoschema.go @@ -181,6 +181,8 @@ func getDefaultChildren(name string, defaults *typeexpr.Defaults) *typeexpr.Defa return children } +// if fields are missing from the default value for an object in the HCL, they are set to null +// we want to remove these fields from the default instead of creating a null default in the schema func removeNullKeys(defVal interface{}) { assertedDefVal, ok := defVal.(map[string]interface{}) if !ok {