Merge pull request #3167 from rrjjvv/variable-typing
Allow variables to be explicitly typed (and enforced)
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
package bake
|
package bake
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
hcl "github.com/hashicorp/hcl/v2"
|
hcl "github.com/hashicorp/hcl/v2"
|
||||||
@@ -1645,6 +1647,695 @@ func TestHCLIndexOfFunc(t *testing.T) {
|
|||||||
require.Empty(t, c.Targets[1].Tags[1])
|
require.Empty(t, c.Targets[1].Tags[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVarTypingSpec(t *testing.T) {
|
||||||
|
templ := `
|
||||||
|
variable "FOO" {
|
||||||
|
type = %s
|
||||||
|
}
|
||||||
|
target "default" {
|
||||||
|
}`
|
||||||
|
|
||||||
|
// not exhaustive, but the common ones
|
||||||
|
for _, s := range []string{
|
||||||
|
"bool", "number", "string", "any",
|
||||||
|
"list(string)", "set(string)", "tuple([string, number])",
|
||||||
|
} {
|
||||||
|
dt := fmt.Sprintf(templ, s)
|
||||||
|
_, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range []string{
|
||||||
|
"boolean", // no synonyms/aliases
|
||||||
|
"BOOL", // case matters
|
||||||
|
`lower("bool")`, // must be literals
|
||||||
|
} {
|
||||||
|
dt := fmt.Sprintf(templ, s)
|
||||||
|
_, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
||||||
|
require.ErrorContains(t, err, "not a valid type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultVarTypeEnforcement(t *testing.T) {
|
||||||
|
// To help prove a given default doesn't just pass the type check, but *is* that type,
|
||||||
|
// we use argValue to provide an expression that would work only on that type.
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
varType string
|
||||||
|
varDefault any
|
||||||
|
argValue string
|
||||||
|
wantValue string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "number (happy)",
|
||||||
|
varType: "number",
|
||||||
|
varDefault: 99,
|
||||||
|
argValue: "FOO + 1",
|
||||||
|
wantValue: "100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "numeric string compatible with number",
|
||||||
|
varType: "number",
|
||||||
|
varDefault: `"99"`,
|
||||||
|
argValue: "FOO + 1",
|
||||||
|
wantValue: "100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean (happy)",
|
||||||
|
varType: "bool",
|
||||||
|
varDefault: true,
|
||||||
|
argValue: "and(FOO, true)",
|
||||||
|
wantValue: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "numeric boolean compatible with boolean",
|
||||||
|
varType: "bool",
|
||||||
|
varDefault: `"true"`,
|
||||||
|
argValue: "and(FOO, true)",
|
||||||
|
wantValue: "true",
|
||||||
|
},
|
||||||
|
// should be representative of flagrant primitive type mismatches; not worth listing all possibilities?
|
||||||
|
{
|
||||||
|
name: "non-numeric string default incompatible with number",
|
||||||
|
varType: "number",
|
||||||
|
varDefault: `"oops"`,
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list of numbers (happy)",
|
||||||
|
varType: "list(number)",
|
||||||
|
varDefault: "[2,3]",
|
||||||
|
argValue: `join("", [for v in FOO: v + 1])`,
|
||||||
|
wantValue: "34",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list of numbers with numeric strings okay",
|
||||||
|
varType: "list(number)",
|
||||||
|
varDefault: `["2","3"]`,
|
||||||
|
argValue: `join("", [for v in FOO: v + 1])`,
|
||||||
|
wantValue: "34",
|
||||||
|
},
|
||||||
|
// represent flagrant mismatches for list types
|
||||||
|
{
|
||||||
|
name: "non-numeric strings in numeric list rejected",
|
||||||
|
varType: "list(number)",
|
||||||
|
varDefault: `["oops"]`,
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
argValue := tt.argValue
|
||||||
|
if argValue == "" {
|
||||||
|
argValue = "FOO"
|
||||||
|
}
|
||||||
|
dt := fmt.Sprintf(`
|
||||||
|
variable "FOO" {
|
||||||
|
type = %s
|
||||||
|
default = %v
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
foo = %s
|
||||||
|
}
|
||||||
|
}`, tt.varType, tt.varDefault, argValue)
|
||||||
|
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
||||||
|
if tt.wantError {
|
||||||
|
require.ErrorContains(t, err, "invalid type")
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
if tt.wantValue != "" {
|
||||||
|
require.Equal(t, 1, len(c.Targets))
|
||||||
|
require.Equal(t, ptrstr(tt.wantValue), c.Targets[0].Args["foo"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultVarTypeWithAttrValuesEnforcement(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
attrValue any
|
||||||
|
varType string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "attribute literal which matches var type",
|
||||||
|
attrValue: `"hello"`,
|
||||||
|
varType: "string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attribute literal which coerces to var type",
|
||||||
|
attrValue: `"99"`,
|
||||||
|
varType: "number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attribute from function which coerces to var type",
|
||||||
|
attrValue: `substr("99 bottles", 0, 2)`,
|
||||||
|
varType: "number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attribute from function returning non-coercible value",
|
||||||
|
attrValue: `split(",", "1,2,3foo")`,
|
||||||
|
varType: "list(number)",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatch",
|
||||||
|
attrValue: 99,
|
||||||
|
varType: "bool",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attribute correctly typed via function",
|
||||||
|
attrValue: `split(",", "1,2,3")`,
|
||||||
|
varType: "list(number)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dt := fmt.Sprintf(`
|
||||||
|
BAR = %v
|
||||||
|
variable "FOO" {
|
||||||
|
type = %s
|
||||||
|
default = BAR
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
}`, tt.attrValue, tt.varType)
|
||||||
|
_, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
||||||
|
if tt.wantError {
|
||||||
|
require.ErrorContains(t, err, "invalid type")
|
||||||
|
require.ErrorContains(t, err, "FOO default value")
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypedVarOverrides(t *testing.T) {
|
||||||
|
const unsuitableValueType = "Unsuitable value type"
|
||||||
|
const unsupportedType = "unsupported type"
|
||||||
|
const failedToParseElement = "failed to parse element"
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
varType string
|
||||||
|
override string
|
||||||
|
argValue string
|
||||||
|
wantValue string
|
||||||
|
wantErrorMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "boolean",
|
||||||
|
varType: "bool",
|
||||||
|
override: "true",
|
||||||
|
wantValue: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number",
|
||||||
|
varType: "number",
|
||||||
|
override: "99",
|
||||||
|
wantValue: "99",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unquoted string accepted",
|
||||||
|
varType: "string",
|
||||||
|
override: "hello",
|
||||||
|
wantValue: "hello",
|
||||||
|
},
|
||||||
|
// an environment variable with a quoted string would most likely be intended
|
||||||
|
// to be a string whose first and last characters are quotes
|
||||||
|
{
|
||||||
|
name: "quoted string keeps quotes in value",
|
||||||
|
varType: "string",
|
||||||
|
override: `"hello"`,
|
||||||
|
wantValue: `"hello"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "any",
|
||||||
|
varType: "any",
|
||||||
|
override: "[1,2]",
|
||||||
|
wantValue: "[1,2]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "any never convert to complex types",
|
||||||
|
varType: "any",
|
||||||
|
override: "[1,2]",
|
||||||
|
argValue: "length(FOO)",
|
||||||
|
wantErrorMsg: "collection must be a list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "proper CSV list of strings",
|
||||||
|
varType: "list(string)",
|
||||||
|
override: "hi,there",
|
||||||
|
argValue: `join("-", FOO)`,
|
||||||
|
wantValue: "hi-there",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CSV of unquoted strings okay",
|
||||||
|
varType: "list(string)",
|
||||||
|
override: `hi,there`,
|
||||||
|
argValue: `join("-", FOO)`,
|
||||||
|
wantValue: "hi-there",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CSV list of numbers",
|
||||||
|
varType: "list(number)",
|
||||||
|
override: "3,1,4",
|
||||||
|
argValue: `join("-", [for v in FOO: v + 1])`,
|
||||||
|
wantValue: "4-2-5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CSV set of numbers",
|
||||||
|
varType: "set(number)",
|
||||||
|
override: "3,1,4",
|
||||||
|
// anecdotally sets are sorted but may not be guaranteed
|
||||||
|
argValue: `join("-", [for v in sort(FOO): v + 1])`,
|
||||||
|
wantValue: "2-4-5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CSV map of numbers",
|
||||||
|
varType: "map(number)",
|
||||||
|
override: "foo:1,bar:2",
|
||||||
|
argValue: `join("-", sort(values(FOO)))`,
|
||||||
|
wantValue: "1-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CSV tuple",
|
||||||
|
varType: "tuple([number,string])",
|
||||||
|
override: `99,bottles`,
|
||||||
|
argValue: `format("%d %s", FOO[0], FOO[1])`,
|
||||||
|
wantValue: "99 bottles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CSV tuple elements with wrong type",
|
||||||
|
varType: "tuple([number,string])",
|
||||||
|
override: `99,100`,
|
||||||
|
wantErrorMsg: unsuitableValueType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid CSV value",
|
||||||
|
varType: "list(string)",
|
||||||
|
override: `"hello,world`,
|
||||||
|
wantErrorMsg: "from CSV",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object not supported",
|
||||||
|
varType: "object({message: string})",
|
||||||
|
override: "does not matter",
|
||||||
|
wantErrorMsg: unsupportedType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list of non-primitives not supported",
|
||||||
|
varType: "list(list(number))",
|
||||||
|
override: "1,2",
|
||||||
|
wantErrorMsg: unsupportedType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set of non-primitives not supported",
|
||||||
|
varType: "set(set(number))",
|
||||||
|
override: "1,2",
|
||||||
|
wantErrorMsg: unsupportedType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tuple of non-primitives not supported",
|
||||||
|
varType: "tuple([list(number)])",
|
||||||
|
// Intentionally a different override than other similar tests; tuple is unique in that
|
||||||
|
// multiple types are involved and length matters. In the real world, it's probably more
|
||||||
|
// likely a user would accidentally omit or add an item than trying to use non-primitives,
|
||||||
|
// so the length check comes first.
|
||||||
|
override: "1",
|
||||||
|
wantErrorMsg: unsupportedType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map of non-primitives not supported",
|
||||||
|
varType: "map(list(number))",
|
||||||
|
override: "foo:1,2",
|
||||||
|
wantErrorMsg: unsupportedType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid map k/v parsing",
|
||||||
|
varType: "map(string)",
|
||||||
|
// TODO fragile; will fail in a different manner without first k/v pair
|
||||||
|
override: `a:b,foo:"bar`,
|
||||||
|
wantErrorMsg: "as CSV",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list with invalidly parsed elements",
|
||||||
|
varType: "list(number)",
|
||||||
|
override: "1,1z",
|
||||||
|
wantErrorMsg: failedToParseElement,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set with invalidly parsed elements",
|
||||||
|
varType: "set(number)",
|
||||||
|
override: "1,1z",
|
||||||
|
wantErrorMsg: failedToParseElement,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tuple with invalidly parsed elements",
|
||||||
|
varType: "tuple([number])",
|
||||||
|
override: "1z",
|
||||||
|
wantErrorMsg: failedToParseElement,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map with invalidly parsed elements",
|
||||||
|
varType: "map(number)",
|
||||||
|
override: "foo:1z",
|
||||||
|
wantErrorMsg: failedToParseElement,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map with bad value format",
|
||||||
|
varType: "map(number)",
|
||||||
|
override: "foo:1:1",
|
||||||
|
wantErrorMsg: "expected one k/v pair",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "primitive with bad value format",
|
||||||
|
varType: "number",
|
||||||
|
override: "1z",
|
||||||
|
wantErrorMsg: "failed to parse",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
argValue := tt.argValue
|
||||||
|
if argValue == "" {
|
||||||
|
argValue = "FOO"
|
||||||
|
}
|
||||||
|
dt := fmt.Sprintf(`
|
||||||
|
variable "FOO" {
|
||||||
|
type = %s
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
foo = %s
|
||||||
|
}
|
||||||
|
}`, tt.varType, argValue)
|
||||||
|
t.Setenv("FOO", tt.override)
|
||||||
|
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
||||||
|
if tt.wantErrorMsg != "" {
|
||||||
|
require.ErrorContains(t, err, tt.wantErrorMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
if tt.wantValue != "" {
|
||||||
|
require.Equal(t, 1, len(c.Targets))
|
||||||
|
require.Equal(t, tt.wantValue, *c.Targets[0].Args["foo"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypedVarOverrides_JSON(t *testing.T) {
|
||||||
|
const unsuitableValueType = "Unsuitable value type"
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
varType string
|
||||||
|
override string
|
||||||
|
argValue string
|
||||||
|
wantValue string
|
||||||
|
wantErrorMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "boolean",
|
||||||
|
varType: "bool",
|
||||||
|
override: "true",
|
||||||
|
wantValue: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number",
|
||||||
|
varType: "number",
|
||||||
|
override: "99",
|
||||||
|
wantValue: "99",
|
||||||
|
},
|
||||||
|
// no shortcuts in JSON mode
|
||||||
|
{
|
||||||
|
name: "unquoted string is error",
|
||||||
|
varType: "string",
|
||||||
|
override: "hello",
|
||||||
|
wantErrorMsg: "from JSON",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string",
|
||||||
|
varType: "string",
|
||||||
|
override: `"hello"`,
|
||||||
|
wantValue: "hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "any",
|
||||||
|
varType: "any",
|
||||||
|
override: "[1,2]",
|
||||||
|
wantValue: "[1,2]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "any never convert to complex types",
|
||||||
|
varType: "any",
|
||||||
|
override: "[1,2]",
|
||||||
|
argValue: "length(FOO)",
|
||||||
|
wantErrorMsg: "collection must be a list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list of strings",
|
||||||
|
varType: "list(string)",
|
||||||
|
override: `["hi","there"]`,
|
||||||
|
argValue: `join("-", FOO)`,
|
||||||
|
wantValue: "hi-there",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list of numbers",
|
||||||
|
varType: "list(number)",
|
||||||
|
override: "[3, 1, 4]",
|
||||||
|
argValue: `join("-", [for v in FOO: v + 1])`,
|
||||||
|
wantValue: "4-2-5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map of numbers",
|
||||||
|
varType: "map(number)",
|
||||||
|
override: `{"foo": 1, "bar": 2}`,
|
||||||
|
argValue: `join("-", sort(values(FOO)))`,
|
||||||
|
wantValue: "1-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid JSON map of numbers",
|
||||||
|
varType: "map(number)",
|
||||||
|
override: `{"foo": "oops", "bar": 2}`,
|
||||||
|
// in lieu of something like ErrorMatches, this is the best single phrase
|
||||||
|
wantErrorMsg: "from JSON",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tuple",
|
||||||
|
varType: "tuple([number,string])",
|
||||||
|
override: `[99, "bottles"]`,
|
||||||
|
argValue: `format("%d %s", FOO[0], FOO[1])`,
|
||||||
|
wantValue: "99 bottles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tuple elements with wrong type",
|
||||||
|
varType: "tuple([number,string])",
|
||||||
|
override: `[99, 100]`,
|
||||||
|
wantErrorMsg: unsuitableValueType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JSON object",
|
||||||
|
varType: `object({messages: list(string)})`,
|
||||||
|
override: `{"messages": ["hi", "there"]}`,
|
||||||
|
argValue: `join("-", FOO["messages"])`,
|
||||||
|
wantValue: "hi-there",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
argValue := tt.argValue
|
||||||
|
if argValue == "" {
|
||||||
|
argValue = "FOO"
|
||||||
|
}
|
||||||
|
dt := fmt.Sprintf(`
|
||||||
|
variable "FOO" {
|
||||||
|
type = %s
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
foo = %s
|
||||||
|
}
|
||||||
|
}`, tt.varType, argValue)
|
||||||
|
t.Setenv("FOO_JSON", tt.override)
|
||||||
|
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
||||||
|
if tt.wantErrorMsg != "" {
|
||||||
|
require.ErrorContains(t, err, tt.wantErrorMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
if tt.wantValue != "" {
|
||||||
|
require.Equal(t, 1, len(c.Targets))
|
||||||
|
require.Equal(t, tt.wantValue, *c.Targets[0].Args["foo"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONOverridePriority(t *testing.T) {
|
||||||
|
t.Run("JSON override ignored when same user var exists", func(t *testing.T) {
|
||||||
|
dt := []byte(`
|
||||||
|
variable "FOO" {
|
||||||
|
type = list(number)
|
||||||
|
}
|
||||||
|
variable "FOO_JSON" {
|
||||||
|
type = list(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
foo = FOO
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
// env FOO_JSON is the CSV override of var FOO_JSON, not a JSON override of FOO
|
||||||
|
t.Setenv("FOO", "[1,2]")
|
||||||
|
t.Setenv("FOO_JSON", "[3,4]")
|
||||||
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
||||||
|
require.ErrorContains(t, err, "failed to convert")
|
||||||
|
require.ErrorContains(t, err, "from CSV")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON override ignored when same builtin var exists", func(t *testing.T) {
|
||||||
|
dt := []byte(`
|
||||||
|
variable "FOO" {
|
||||||
|
type = list(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
foo = length(FOO)
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
t.Setenv("FOO", "1,2")
|
||||||
|
t.Setenv("FOO_JSON", "[3,4,5]")
|
||||||
|
c, _, err := ParseFiles(
|
||||||
|
[]File{{Name: "docker-bake.hcl", Data: dt}},
|
||||||
|
map[string]string{"FOO_JSON": "whatever"},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(c.Targets))
|
||||||
|
require.Equal(t, "2", *c.Targets[0].Args["foo"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// this is implied/exercised in other tests, but repeated for completeness
|
||||||
|
t.Run("JSON override ignored if var is untyped", func(t *testing.T) {
|
||||||
|
dt := []byte(`
|
||||||
|
variable "FOO" {
|
||||||
|
default = [1, 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
foo = length(FOO)
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
t.Setenv("FOO_JSON", "[3,4]")
|
||||||
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
||||||
|
require.ErrorContains(t, err, "unsupported type")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("override-ish variable has regular CSV override", func(t *testing.T) {
|
||||||
|
dt := []byte(`
|
||||||
|
variable "FOO_JSON" {
|
||||||
|
type = list(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
foo = length(FOO_JSON)
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
// despite the name, it's still CSV
|
||||||
|
t.Setenv("FOO_JSON", "10,11,12")
|
||||||
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(c.Targets))
|
||||||
|
require.Equal(t, "3", *c.Targets[0].Args["foo"])
|
||||||
|
|
||||||
|
t.Setenv("FOO_JSON", "[10,11,12]")
|
||||||
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
||||||
|
require.ErrorContains(t, err, "from CSV")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("override-ish variable has own JSON override", func(t *testing.T) {
|
||||||
|
dt := []byte(`
|
||||||
|
variable "FOO_JSON" {
|
||||||
|
type = list(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
foo = length(FOO_JSON)
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
t.Setenv("FOO_JSON_JSON", "[4,5,6]")
|
||||||
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(c.Targets))
|
||||||
|
require.Equal(t, "3", *c.Targets[0].Args["foo"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON override trumps CSV when no var name conflict", func(t *testing.T) {
|
||||||
|
dt := []byte(`
|
||||||
|
variable "FOO" {
|
||||||
|
type = list(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
foo = length(FOO)
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
t.Setenv("FOO", "1,2")
|
||||||
|
t.Setenv("FOO_JSON", "[3,4,5]")
|
||||||
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(c.Targets))
|
||||||
|
require.Equal(t, "3", *c.Targets[0].Args["foo"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON override works with lowercase vars", func(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("Windows case-insensitivity")
|
||||||
|
}
|
||||||
|
dt := []byte(`
|
||||||
|
variable "foo" {
|
||||||
|
type = number
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
args = {
|
||||||
|
bar = foo
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
// may seem reasonable, but not supported (on case-sensitive systems)
|
||||||
|
t.Setenv("foo_json", "9000")
|
||||||
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(c.Targets))
|
||||||
|
// a variable with no value has always resulted in an empty string
|
||||||
|
require.Equal(t, "", *c.Targets[0].Args["bar"])
|
||||||
|
|
||||||
|
t.Setenv("foo_JSON", "42")
|
||||||
|
c, err = ParseFile(dt, "docker-bake.hcl")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(c.Targets))
|
||||||
|
require.Equal(t, "42", *c.Targets[0].Args["bar"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func ptrstr(s any) *string {
|
func ptrstr(s any) *string {
|
||||||
var n *string
|
var n *string
|
||||||
if reflect.ValueOf(s).Kind() == reflect.String {
|
if reflect.ValueOf(s).Kind() == reflect.String {
|
||||||
|
|||||||
+225
-27
@@ -14,11 +14,16 @@ import (
|
|||||||
"github.com/docker/buildx/bake/hclparser/gohcl"
|
"github.com/docker/buildx/bake/hclparser/gohcl"
|
||||||
"github.com/docker/buildx/util/userfunc"
|
"github.com/docker/buildx/util/userfunc"
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tonistiigi/go-csvvalue"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
"github.com/zclconf/go-cty/cty/convert"
|
"github.com/zclconf/go-cty/cty/convert"
|
||||||
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const jsonEnvOverrideSuffix = "_JSON"
|
||||||
|
|
||||||
type Opt struct {
|
type Opt struct {
|
||||||
LookupVar func(string) (string, bool)
|
LookupVar func(string) (string, bool)
|
||||||
Vars map[string]string
|
Vars map[string]string
|
||||||
@@ -27,6 +32,7 @@ type Opt struct {
|
|||||||
|
|
||||||
type variable struct {
|
type variable struct {
|
||||||
Name string `json:"-" hcl:"name,label"`
|
Name string `json:"-" hcl:"name,label"`
|
||||||
|
Type hcl.Expression `json:"type,omitempty" hcl:"type,optional"`
|
||||||
Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"`
|
Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"`
|
||||||
Description string `json:"description,omitempty" hcl:"description,optional"`
|
Description string `json:"description,omitempty" hcl:"description,optional"`
|
||||||
Validations []*variableValidation `json:"validation,omitempty" hcl:"validation,block"`
|
Validations []*variableValidation `json:"validation,omitempty" hcl:"validation,block"`
|
||||||
@@ -267,57 +273,91 @@ func (p *parser) resolveValue(ectx *hcl.EvalContext, name string) (err error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// built-in vars aren't intended to be overridden and are statically typed as strings;
|
||||||
|
// no sense sending them through type checks or waiting to return them
|
||||||
|
if val, ok := p.opt.Vars[name]; ok {
|
||||||
|
vv := cty.StringVal(val)
|
||||||
|
v = &vv
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var diags hcl.Diagnostics
|
||||||
|
varType := cty.DynamicPseudoType
|
||||||
def, ok := p.attrs[name]
|
def, ok := p.attrs[name]
|
||||||
if _, builtin := p.opt.Vars[name]; !ok && !builtin {
|
if !ok {
|
||||||
vr, ok := p.vars[name]
|
vr, ok := p.vars[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.Wrapf(errUndefined{}, "variable %q does not exist", name)
|
return errors.Wrapf(errUndefined{}, "variable %q does not exist", name)
|
||||||
}
|
}
|
||||||
def = vr.Default
|
def = vr.Default
|
||||||
ectx = p.ectx
|
ectx = p.ectx
|
||||||
|
varType, diags = typeConstraint(vr.Type)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if def == nil {
|
if def == nil {
|
||||||
val, ok := p.opt.Vars[name]
|
// lack of specified value is considered to have an empty string value,
|
||||||
if !ok {
|
// but any overrides get type checked
|
||||||
val, _ = p.opt.LookupVar(name)
|
if _, ok, _ := p.valueHasOverride(name, false); !ok {
|
||||||
|
vv := cty.StringVal("")
|
||||||
|
v = &vv
|
||||||
|
return
|
||||||
}
|
}
|
||||||
vv := cty.StringVal(val)
|
|
||||||
v = &vv
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if diags := p.loadDeps(ectx, def.Expr, nil, true); diags.HasErrors() {
|
var vv cty.Value
|
||||||
return diags
|
if def != nil {
|
||||||
}
|
if diags := p.loadDeps(ectx, def.Expr, nil, true); diags.HasErrors() {
|
||||||
vv, diags := def.Expr.Value(ectx)
|
return diags
|
||||||
if diags.HasErrors() {
|
}
|
||||||
return diags
|
vv, diags = def.Expr.Value(ectx)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
vv, err = convert.Convert(vv, varType)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "invalid type %s for variable %s default value", varType.FriendlyName(), name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not entirely true... this doesn't differentiate between a user that specified 'any'
|
||||||
|
// and a user that specified nothing. But the result is the same; both are treated as strings.
|
||||||
|
typeSpecified := !varType.Equals(cty.DynamicPseudoType)
|
||||||
|
envv, hasEnv, jsonEnv := p.valueHasOverride(name, typeSpecified)
|
||||||
_, isVar := p.vars[name]
|
_, isVar := p.vars[name]
|
||||||
|
|
||||||
if envv, ok := p.opt.LookupVar(name); ok && isVar {
|
if hasEnv && isVar {
|
||||||
switch {
|
switch {
|
||||||
case vv.Type().Equals(cty.Bool):
|
case typeSpecified && jsonEnv:
|
||||||
b, err := strconv.ParseBool(envv)
|
vv, err = ctyjson.Unmarshal([]byte(envv), varType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to parse %s as bool", name)
|
return errors.Wrapf(err, "failed to convert variable %s from JSON", name)
|
||||||
}
|
}
|
||||||
vv = cty.BoolVal(b)
|
case supportedCSVType(varType): // typing explicitly specified for selected complex types
|
||||||
case vv.Type().Equals(cty.String), vv.Type().Equals(cty.DynamicPseudoType):
|
vv, err = valueFromCSV(name, envv, varType)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to convert variable %s from CSV", name)
|
||||||
|
}
|
||||||
|
case typeSpecified && varType.IsPrimitiveType():
|
||||||
|
vv, err = convertPrimitive(name, envv, varType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case typeSpecified:
|
||||||
|
// e.g., an 'object' not provided as JSON (which can't be expressed in the default CSV format)
|
||||||
|
return errors.Errorf("unsupported type %s for variable %s", varType.FriendlyName(), name)
|
||||||
|
case def == nil: // no default from which to infer typing
|
||||||
vv = cty.StringVal(envv)
|
vv = cty.StringVal(envv)
|
||||||
case vv.Type().Equals(cty.Number):
|
case vv.Type().Equals(cty.DynamicPseudoType):
|
||||||
n, err := strconv.ParseFloat(envv, 64)
|
vv = cty.StringVal(envv)
|
||||||
if err == nil && (math.IsNaN(n) || math.IsInf(n, 0)) {
|
case vv.Type().IsPrimitiveType():
|
||||||
err = errors.Errorf("invalid number value")
|
vv, err = convertPrimitive(name, envv, vv.Type())
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to parse %s as number", name)
|
return err
|
||||||
}
|
}
|
||||||
vv = cty.NumberVal(big.NewFloat(n))
|
|
||||||
default:
|
default:
|
||||||
// TODO: support lists with csv values
|
|
||||||
return errors.Errorf("unsupported type %s for variable %s", vv.Type().FriendlyName(), name)
|
return errors.Errorf("unsupported type %s for variable %s", vv.Type().FriendlyName(), name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,6 +365,29 @@ func (p *parser) resolveValue(ectx *hcl.EvalContext, name string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// valueHasOverride returns a possible override value if one was specified, and whether it should
|
||||||
|
// be treated as a JSON value.
|
||||||
|
//
|
||||||
|
// A plain/CSV override is the default; this consolidates the logic around how a JSON-specific override
|
||||||
|
// is specified and when it will be honored when there are naming conflicts or ambiguity.
|
||||||
|
func (p *parser) valueHasOverride(name string, favorJSON bool) (string, bool, bool) {
|
||||||
|
jsonEnv := false
|
||||||
|
envv, hasEnv := p.opt.LookupVar(name)
|
||||||
|
// If no plain override exists (!hasEnv) or JSON overrides are explicitly favored (favorJSON),
|
||||||
|
// check for a JSON-specific override with the "_JSON" suffix.
|
||||||
|
if !hasEnv || favorJSON {
|
||||||
|
jsonVarName := name + jsonEnvOverrideSuffix
|
||||||
|
_, builtin := p.opt.Vars[jsonVarName]
|
||||||
|
if _, ok := p.vars[jsonVarName]; !ok && !builtin {
|
||||||
|
if j, ok := p.opt.LookupVar(jsonVarName); ok {
|
||||||
|
envv = j
|
||||||
|
hasEnv, jsonEnv = true, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return envv, hasEnv, jsonEnv
|
||||||
|
}
|
||||||
|
|
||||||
// resolveBlock force evaluates a block, storing the result in the parser. If a
|
// resolveBlock force evaluates a block, storing the result in the parser. If a
|
||||||
// target schema is provided, only the attributes and blocks present in the
|
// target schema is provided, only the attributes and blocks present in the
|
||||||
// schema will be evaluated.
|
// schema will be evaluated.
|
||||||
@@ -907,6 +970,141 @@ func Parse(b hcl.Body, opt Opt, val any) (*ParseMeta, hcl.Diagnostics) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// typeConstraint wraps typeexpr.TypeConstraint to differentiate between errors in the
|
||||||
|
// specification and errors due to being cty.NullVal (not provided).
|
||||||
|
func typeConstraint(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
|
||||||
|
t, diag := typeexpr.TypeConstraint(expr)
|
||||||
|
if !diag.HasErrors() {
|
||||||
|
return t, diag
|
||||||
|
}
|
||||||
|
// if had errors, it could be because the expression is 'nil', i.e., unspecified
|
||||||
|
if v, err := expr.Value(nil); err == nil {
|
||||||
|
if v.IsNull() {
|
||||||
|
return cty.DynamicPseudoType, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// even if the evaluation resulted in error, the original (error) diagnostics are likely more useful
|
||||||
|
return t, diag
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertPrimitive converts a single string primitive value to a given cty.Type.
|
||||||
|
func convertPrimitive(name, value string, target cty.Type) (cty.Value, error) {
|
||||||
|
switch {
|
||||||
|
case target.Equals(cty.String):
|
||||||
|
return cty.StringVal(value), nil
|
||||||
|
case target.Equals(cty.Bool):
|
||||||
|
b, err := strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return cty.NilVal, errors.Wrapf(err, "failed to parse %s as bool", name)
|
||||||
|
}
|
||||||
|
return cty.BoolVal(b), nil
|
||||||
|
case target.Equals(cty.Number):
|
||||||
|
n, err := strconv.ParseFloat(value, 64)
|
||||||
|
if err == nil && (math.IsNaN(n) || math.IsInf(n, 0)) {
|
||||||
|
err = errors.Errorf("invalid number value")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return cty.NilVal, errors.Wrapf(err, "failed to parse %s as number", name)
|
||||||
|
}
|
||||||
|
return cty.NumberVal(big.NewFloat(n)), nil
|
||||||
|
default:
|
||||||
|
return cty.NilVal, errors.Errorf("%s of type %s is not a primitive", name, target.FriendlyName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedCSVType reports whether the given cty.Type might be convertible from a CSV string via valueFromCSV.
|
||||||
|
func supportedCSVType(t cty.Type) bool {
|
||||||
|
return t.IsListType() || t.IsSetType() || t.IsTupleType() || t.IsMapType()
|
||||||
|
}
|
||||||
|
|
||||||
|
// valueFromCSV takes CSV value and converts it to cty.Type.
|
||||||
|
//
|
||||||
|
// This currently supports conversion to cty.List and cty.Set.
|
||||||
|
// It also contains preliminary support for cty.Map (the other collection type).
|
||||||
|
// While not considered a collection type, it also tentatively supports cty.Tuple.
|
||||||
|
func valueFromCSV(name, value string, target cty.Type) (cty.Value, error) {
|
||||||
|
fields, err := csvvalue.Fields(value, nil)
|
||||||
|
if err != nil {
|
||||||
|
return cty.NilVal, errors.Wrapf(err, "failed to parse %s as CSV", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// used for lists and set, which require identical processing and differ only in return type
|
||||||
|
singleTypeConvert := func(t cty.Type) ([]cty.Value, error) {
|
||||||
|
var elems []cty.Value
|
||||||
|
for _, f := range fields {
|
||||||
|
v, err := convertPrimitive(name, f, t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to parse element of type %s", target.FriendlyName())
|
||||||
|
}
|
||||||
|
elems = append(elems, v)
|
||||||
|
}
|
||||||
|
return elems, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case target.IsListType():
|
||||||
|
if !target.ElementType().IsPrimitiveType() {
|
||||||
|
return cty.NilVal, errors.Errorf("unsupported type %s for CSV specification", target.FriendlyName())
|
||||||
|
}
|
||||||
|
elems, err := singleTypeConvert(target.ElementType())
|
||||||
|
if err != nil {
|
||||||
|
return cty.NilVal, err
|
||||||
|
}
|
||||||
|
return cty.ListVal(elems), nil
|
||||||
|
case target.IsSetType():
|
||||||
|
if !target.ElementType().IsPrimitiveType() {
|
||||||
|
return cty.NilVal, errors.Errorf("unsupported type %s for CSV specification", target.FriendlyName())
|
||||||
|
}
|
||||||
|
elems, err := singleTypeConvert(target.ElementType())
|
||||||
|
if err != nil {
|
||||||
|
return cty.NilVal, err
|
||||||
|
}
|
||||||
|
return cty.SetVal(elems), nil
|
||||||
|
case target.IsTupleType():
|
||||||
|
tupleTypes := target.TupleElementTypes()
|
||||||
|
if len(tupleTypes) != len(fields) {
|
||||||
|
return cty.NilVal, errors.Errorf("%s expects %d elements but only %d provided", target.FriendlyName(), len(tupleTypes), len(fields))
|
||||||
|
}
|
||||||
|
var elems []cty.Value
|
||||||
|
for i, f := range fields {
|
||||||
|
tt := tupleTypes[i]
|
||||||
|
if !tt.IsPrimitiveType() {
|
||||||
|
return cty.NilVal, errors.Errorf("unsupported type %s for CSV specification", target.FriendlyName())
|
||||||
|
}
|
||||||
|
v, err := convertPrimitive(name, f, tt)
|
||||||
|
if err != nil {
|
||||||
|
return cty.NilVal, errors.Wrapf(err, "failed to parse element of type %s", target.FriendlyName())
|
||||||
|
}
|
||||||
|
elems = append(elems, v)
|
||||||
|
}
|
||||||
|
return cty.TupleVal(elems), nil
|
||||||
|
case target.IsMapType():
|
||||||
|
if !target.ElementType().IsPrimitiveType() {
|
||||||
|
return cty.NilVal, errors.Errorf("unsupported type %s for CSV specification", target.FriendlyName())
|
||||||
|
}
|
||||||
|
p := csvvalue.Parser{Comma: ':'}
|
||||||
|
var kvSlice []string
|
||||||
|
m := make(map[string]cty.Value)
|
||||||
|
for _, f := range fields {
|
||||||
|
kvSlice, err = p.Fields(f, kvSlice)
|
||||||
|
if err != nil {
|
||||||
|
return cty.NilVal, errors.Wrapf(err, "failed to parse %s as k/v for variable %s", f, name)
|
||||||
|
}
|
||||||
|
if len(kvSlice) != 2 {
|
||||||
|
return cty.NilVal, errors.Errorf("expected one k/v pair but got %d pieces from %s", len(kvSlice), f)
|
||||||
|
}
|
||||||
|
v, err := convertPrimitive(name, kvSlice[1], target.ElementType())
|
||||||
|
if err != nil {
|
||||||
|
return cty.NilVal, errors.Wrapf(err, "failed to parse element from type %s", target.FriendlyName())
|
||||||
|
}
|
||||||
|
m[kvSlice[0]] = v
|
||||||
|
}
|
||||||
|
return cty.MapVal(m), nil
|
||||||
|
default:
|
||||||
|
return cty.NilVal, errors.Errorf("unsupported type %s for CSV specification", target.FriendlyName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// wrapErrorDiagnostic wraps an error into a hcl.Diagnostics object.
|
// wrapErrorDiagnostic wraps an error into a hcl.Diagnostics object.
|
||||||
// If the error is already an hcl.Diagnostics object, it is returned as is.
|
// If the error is already an hcl.Diagnostics object, it is returned as is.
|
||||||
func wrapErrorDiagnostic(message string, err error, subject *hcl.Range, context *hcl.Range) hcl.Diagnostics {
|
func wrapErrorDiagnostic(message string, err error, subject *hcl.Range, context *hcl.Range) hcl.Diagnostics {
|
||||||
|
|||||||
Reference in New Issue
Block a user