package hclspecutils import ( "testing" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/plugins/shared/hclspec" "github.com/stretchr/testify/require" "github.com/zclconf/go-cty/cty" ) type testConversions struct { Name string Input *hclspec.Spec Expected hcldec.Spec ExpectedError string } func testSpecConversions(t *testing.T, cases []testConversions) { t.Helper() for _, c := range cases { t.Run(c.Name, func(t *testing.T) { act, diag := Convert(c.Input) if diag.HasErrors() { if c.ExpectedError == "" { t.Fatalf("Convert %q failed: %v", c.Name, diag.Error()) } require.Contains(t, diag.Error(), c.ExpectedError) } else if c.ExpectedError != "" { t.Fatalf("Expected error %q", c.ExpectedError) } require.EqualValues(t, c.Expected, act) }) } } func TestDec_Convert_Object(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "Object w/ only attributes", Input: &hclspec.Spec{ Block: &hclspec.Spec_Object{ Object: &hclspec.Object{ Attributes: map[string]*hclspec.Spec{ "foo": { Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Type: "string", Required: false, }, }, }, "bar": { Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Type: "number", Required: true, }, }, }, "baz": { Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Type: "bool", }, }, }, }, }, }, }, Expected: hcldec.ObjectSpec(map[string]hcldec.Spec{ "foo": &hcldec.AttrSpec{ Name: "foo", Type: cty.String, Required: false, }, "bar": &hcldec.AttrSpec{ Name: "bar", Type: cty.Number, Required: true, }, "baz": &hcldec.AttrSpec{ Name: "baz", Type: cty.Bool, Required: false, }, }), }, } testSpecConversions(t, tests) } func TestDec_Convert_Array(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "array basic", Input: &hclspec.Spec{ Block: &hclspec.Spec_Array{ Array: &hclspec.Array{ Values: []*hclspec.Spec{ { Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Required: true, Type: "string", }, }, }, { Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "bar", Required: true, Type: "string", }, }, }, }, }, }, }, Expected: hcldec.TupleSpec{ &hcldec.AttrSpec{ Name: "foo", Type: cty.String, Required: true, }, &hcldec.AttrSpec{ Name: "bar", Type: cty.String, Required: true, }, }, }, } testSpecConversions(t, tests) } func TestDec_Convert_Attr(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "attr basic type", Input: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Required: true, Type: "string", }, }, }, Expected: &hcldec.AttrSpec{ Name: "foo", Type: cty.String, Required: true, }, }, { Name: "attr object type", Input: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Required: true, Type: "object({name1 = string, name2 = bool})", }, }, }, Expected: &hcldec.AttrSpec{ Name: "foo", Type: cty.Object(map[string]cty.Type{ "name1": cty.String, "name2": cty.Bool, }), Required: true, }, }, { Name: "attr no name", Input: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Required: true, Type: "string", }, }, }, ExpectedError: "Missing name in attribute spec", }, } testSpecConversions(t, tests) } func TestDec_Convert_Block(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "block with attr", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockValue{ BlockValue: &hclspec.Block{ Name: "test", Required: true, Nested: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", }, }, }, }, }, }, Expected: &hcldec.BlockSpec{ TypeName: "test", Required: true, Nested: &hcldec.AttrSpec{ Name: "foo", Type: cty.String, Required: false, }, }, }, { Name: "block with nested block", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockValue{ BlockValue: &hclspec.Block{ Name: "test", Required: true, Nested: &hclspec.Spec{ Block: &hclspec.Spec_BlockValue{ BlockValue: &hclspec.Block{ Name: "test", Required: true, Nested: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", }, }, }, }, }, }, }, }, }, Expected: &hcldec.BlockSpec{ TypeName: "test", Required: true, Nested: &hcldec.BlockSpec{ TypeName: "test", Required: true, Nested: &hcldec.AttrSpec{ Name: "foo", Type: cty.String, Required: false, }, }, }, }, } testSpecConversions(t, tests) } func TestDec_Convert_BlockAttrs(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "block attr", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockAttrs{ BlockAttrs: &hclspec.BlockAttrs{ Name: "test", Type: "string", Required: true, }, }, }, Expected: &hcldec.BlockAttrsSpec{ TypeName: "test", ElementType: cty.String, Required: true, }, }, { Name: "block list no name", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockAttrs{ BlockAttrs: &hclspec.BlockAttrs{ Type: "string", Required: true, }, }, }, ExpectedError: "Missing name in block_attrs spec", }, } testSpecConversions(t, tests) } func TestDec_Convert_BlockList(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "block list with attr", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockList{ BlockList: &hclspec.BlockList{ Name: "test", MinItems: 1, MaxItems: 3, Nested: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", }, }, }, }, }, }, Expected: &hcldec.BlockListSpec{ TypeName: "test", MinItems: 1, MaxItems: 3, Nested: &hcldec.AttrSpec{ Name: "foo", Type: cty.String, Required: false, }, }, }, { Name: "block list no name", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockList{ BlockList: &hclspec.BlockList{ MinItems: 1, MaxItems: 3, Nested: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", }, }, }, }, }, }, ExpectedError: "Missing name in block_list spec", }, } testSpecConversions(t, tests) } func TestDec_Convert_BlockSet(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "block set with attr", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockSet{ BlockSet: &hclspec.BlockSet{ Name: "test", MinItems: 1, MaxItems: 3, Nested: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", }, }, }, }, }, }, Expected: &hcldec.BlockSetSpec{ TypeName: "test", MinItems: 1, MaxItems: 3, Nested: &hcldec.AttrSpec{ Name: "foo", Type: cty.String, Required: false, }, }, }, { Name: "block set missing name", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockSet{ BlockSet: &hclspec.BlockSet{ MinItems: 1, MaxItems: 3, Nested: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", }, }, }, }, }, }, ExpectedError: "Missing name in block_set spec", }, } testSpecConversions(t, tests) } func TestDec_Convert_BlockMap(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "block map with attr", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockMap{ BlockMap: &hclspec.BlockMap{ Name: "test", Labels: []string{"key1", "key2"}, Nested: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", }, }, }, }, }, }, Expected: &hcldec.BlockMapSpec{ TypeName: "test", LabelNames: []string{"key1", "key2"}, Nested: &hcldec.AttrSpec{ Name: "foo", Type: cty.String, Required: false, }, }, }, { Name: "block map missing name", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockMap{ BlockMap: &hclspec.BlockMap{ Labels: []string{"key1", "key2"}, Nested: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", }, }, }, }, }, }, ExpectedError: "Missing name in block_map spec", }, { Name: "block map missing labels", Input: &hclspec.Spec{ Block: &hclspec.Spec_BlockMap{ BlockMap: &hclspec.BlockMap{ Name: "foo", Nested: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", }, }, }, }, }, }, ExpectedError: "Invalid block label name list", }, } testSpecConversions(t, tests) } func TestDec_Convert_Default(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "default attr", Input: &hclspec.Spec{ Block: &hclspec.Spec_Default{ Default: &hclspec.Default{ Primary: &hclspec.Spec{ Block: &hclspec.Spec_Attr{ Attr: &hclspec.Attr{ Name: "foo", Type: "string", Required: true, }, }, }, Default: &hclspec.Spec{ Block: &hclspec.Spec_Literal{ Literal: &hclspec.Literal{ Value: "\"hi\"", }, }, }, }, }, }, Expected: &hcldec.DefaultSpec{ Primary: &hcldec.AttrSpec{ Name: "foo", Type: cty.String, Required: true, }, Default: &hcldec.LiteralSpec{ Value: cty.StringVal("hi"), }, }, }, } testSpecConversions(t, tests) } func TestDec_Convert_Literal(t *testing.T) { ci.Parallel(t) tests := []testConversions{ { Name: "bool: true", Input: &hclspec.Spec{ Block: &hclspec.Spec_Literal{ Literal: &hclspec.Literal{ Value: "true", }, }, }, Expected: &hcldec.LiteralSpec{ Value: cty.BoolVal(true), }, }, { Name: "bool: false", Input: &hclspec.Spec{ Block: &hclspec.Spec_Literal{ Literal: &hclspec.Literal{ Value: "false", }, }, }, Expected: &hcldec.LiteralSpec{ Value: cty.BoolVal(false), }, }, { Name: "string", Input: &hclspec.Spec{ Block: &hclspec.Spec_Literal{ Literal: &hclspec.Literal{ Value: "\"hi\"", }, }, }, Expected: &hcldec.LiteralSpec{ Value: cty.StringVal("hi"), }, }, { Name: "string w/ func", Input: &hclspec.Spec{ Block: &hclspec.Spec_Literal{ Literal: &hclspec.Literal{ Value: "reverse(\"hi\")", }, }, }, Expected: &hcldec.LiteralSpec{ Value: cty.StringVal("ih"), }, }, { Name: "list string", Input: &hclspec.Spec{ Block: &hclspec.Spec_Literal{ Literal: &hclspec.Literal{ Value: "[\"hi\", \"bye\"]", }, }, }, Expected: &hcldec.LiteralSpec{ Value: cty.TupleVal([]cty.Value{cty.StringVal("hi"), cty.StringVal("bye")}), }, }, } testSpecConversions(t, tests) }