block attr spec

This commit is contained in:
Alex Dadgar 2018-08-10 11:14:12 -07:00
parent d6b291b00d
commit 42b432d18d
4 changed files with 300 additions and 351 deletions

View file

@ -5,10 +5,8 @@ import (
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/hcl2/gohcl"
hcl2 "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl2/hclparse"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/kr/pretty"
@ -19,12 +17,6 @@ import (
"github.com/zclconf/go-cty/cty/gocty"
)
/*
Martin suggests writing a function that takes a spec and map[string]interface{}
and essentially fixes the []map[string]interface{} -> map[string]interface{}
*/
var (
dockerSpec hcldec.Spec = hcldec.ObjectSpec(map[string]hcldec.Spec{
"image": &hcldec.AttrSpec{
@ -40,11 +32,9 @@ var (
Name: "pids_limit",
Type: cty.Number,
},
"port_map": &hcldec.AttrSpec{
Name: "port_map",
// This should be a block. cty.Map(cty.String)
Type: cty.List(cty.Map(cty.String)),
"port_map": &hcldec.BlockAttrsSpec{
TypeName: "port_map",
ElementType: cty.String,
},
"devices": &hcldec.BlockListSpec{
@ -77,7 +67,7 @@ type dockerConfig struct {
Image string `cty:"image"`
Args []string `cty:"args"`
PidsLimit *int64 `cty:"pids_limit"`
PortMap []map[string]string `cty:"port_map"`
PortMap map[string]string `cty:"port_map"`
Devices []DockerDevice `cty:"devices"`
}
@ -188,7 +178,6 @@ func TestParseHclInterface_Hcl(t *testing.T) {
},
expectedType: &dockerConfig{},
},
// ------------------------------------------------
{
name: "number attr",
config: hclConfigToInterface(t, `
@ -223,7 +212,6 @@ func TestParseHclInterface_Hcl(t *testing.T) {
},
expectedType: &dockerConfig{},
},
// ------------------------------------------------
{
name: "number attr interpolated",
config: hclConfigToInterface(t, `
@ -258,7 +246,6 @@ func TestParseHclInterface_Hcl(t *testing.T) {
},
expectedType: &dockerConfig{},
},
// ------------------------------------------------
{
name: "multi attr",
config: hclConfigToInterface(t, `
@ -293,7 +280,6 @@ func TestParseHclInterface_Hcl(t *testing.T) {
},
expectedType: &dockerConfig{},
},
// ------------------------------------------------
{
name: "multi attr variables",
config: hclConfigToInterface(t, `
@ -330,7 +316,6 @@ func TestParseHclInterface_Hcl(t *testing.T) {
},
expectedType: &dockerConfig{},
},
// ------------------------------------------------
{
name: "port_map",
config: hclConfigToInterface(t, `
@ -345,11 +330,10 @@ func TestParseHclInterface_Hcl(t *testing.T) {
ctx: defaultCtx,
expected: &dockerConfig{
Image: "redis:3.2",
PortMap: []map[string]string{
{
PortMap: map[string]string{
"foo": "db",
"bar": "db2",
}},
},
Devices: []DockerDevice{},
},
expectedType: &dockerConfig{},
@ -370,44 +354,14 @@ func TestParseHclInterface_Hcl(t *testing.T) {
ctx: defaultCtx,
expected: &dockerConfig{
Image: "redis:3.2",
PortMap: []map[string]string{
{
PortMap: map[string]string{
"foo": "db",
"bar": "db2",
}},
},
Devices: []DockerDevice{},
},
expectedType: &dockerConfig{},
},
// ------------------------------------------------
/*
{
name: "port_map non-list json",
config: jsonConfigToInterface(t, `
{
"Config": {
"image": "redis:3.2",
"port_map": {
"foo": "db",
"bar": "db2"
}
}
}`),
spec: dockerSpec,
ctx: defaultCtx,
expected: &dockerConfig{
Image: "redis:3.2",
PortMap: []map[string]string{
{
"foo": "db",
"bar": "db2",
}},
Devices: []DockerDevice{},
},
expectedType: &dockerConfig{},
},
*/
// ------------------------------------------------
{
name: "devices",
config: hclConfigToInterface(t, `
@ -480,7 +434,6 @@ func TestParseHclInterface_Hcl(t *testing.T) {
},
expectedType: &dockerConfig{},
},
// ------------------------------------------------
}
for _, c := range cases {
@ -503,190 +456,3 @@ func TestParseHclInterface_Hcl(t *testing.T) {
})
}
}
// -------------------------------------------------------------------------
var (
dockerSpec2 hcldec.Spec = hcldec.ObjectSpec(map[string]hcldec.Spec{
"image": &hcldec.AttrSpec{
Name: "image",
Type: cty.String,
Required: true,
},
"args": &hcldec.AttrSpec{
Name: "args",
Type: cty.List(cty.String),
},
//"port_map": &hcldec.AttrSpec{
//Name: "port_map",
//Type: cty.List(cty.Map(cty.String)),
//},
//"devices": &hcldec.AttrSpec{
//Name: "devices",
//Type: cty.List(cty.Object(map[string]cty.Type{
//"host_path": cty.String,
//"container_path": cty.String,
//"cgroup_permissions": cty.String,
//})),
//Type: cty.Tuple([]cty.Type{cty.Object(map[string]cty.Type{
//"host_path": cty.String,
//"container_path": cty.String,
//"cgroup_permissions": cty.String,
//})}),
//},
},
)
)
func configToHcl2Interface(t *testing.T, config string) interface{} {
t.Helper()
// Parse as we do in the jobspec parser
file, diag := hclparse.NewParser().ParseHCL([]byte(config), "config")
if diag.HasErrors() {
t.Fatalf("failed to hcl parse the config: %v", diag.Error())
}
//t.Logf("Body: % #v", pretty.Formatter(file.Body))
var c struct {
m map[string]interface{}
}
implied, partial := gohcl.ImpliedBodySchema(&c)
t.Logf("partial=%v implied=% #v", partial, pretty.Formatter(implied))
contents, diag := file.Body.Content(implied)
if diag.HasErrors() {
t.Fatalf("failed to get contents: %v", diag.Error())
}
t.Fatalf("content=% #v", pretty.Formatter(contents))
//defaultCtx := &hcl2.EvalContext{
//Functions: GetStdlibFuncs(),
//}
return nil
}
func TestParseHclInterface_Hcl2(t *testing.T) {
t.SkipNow()
defaultCtx := &hcl2.EvalContext{
Functions: GetStdlibFuncs(),
}
cases := []struct {
name string
config string
spec hcldec.Spec
ctx *hcl2.EvalContext
expected interface{}
expectedType interface{}
}{
{
name: "single attr",
config: `
image = "redis:3.2"
`,
spec: dockerSpec2,
ctx: defaultCtx,
expected: &dockerConfig{
Image: "redis:3.2",
},
expectedType: &dockerConfig{},
},
//{
//name: "multi attr",
//config: `
//config {
//image = "redis:3.2"
//args = ["foo", "bar"]
//}`,
//spec: dockerSpec2,
//ctx: defaultCtx,
//expected: &dockerConfig{
//Image: "redis:3.2",
//Args: []string{"foo", "bar"},
//},
//expectedType: &dockerConfig{},
//},
//{
//name: "port_map",
//config: `
//config {
//image = "redis:3.2"
//port_map {
//foo = "db"
//}
//}`,
//spec: dockerSpec,
//ctx: defaultCtx,
//expected: &dockerConfig{
//Image: "redis:3.2",
//PortMap: []map[string]string{{"foo": "db"}},
//},
//expectedType: &dockerConfig{},
//},
//{
//name: "devices",
//config: `
//config {
//image = "redis:3.2"
//devices = [
//{
//host_path = "/dev/sda1"
//container_path = "/dev/xvdc"
//cgroup_permissions = "r"
//},
//{
//host_path = "/dev/sda2"
//container_path = "/dev/xvdd"
//}
//]
//}`,
//spec: dockerSpec,
//ctx: defaultCtx,
//expected: &dockerConfig{
//Image: "redis:3.2",
//Args: []string{"foo", "bar"},
//Devices: []DockerDevice{
//{
//HostPath: "/dev/sda1",
//ContainerPath: "/dev/xvdc",
//CgroupPermissions: "r",
//},
//{
//HostPath: "/dev/sda2",
//ContainerPath: "/dev/xvdd",
//},
//},
//},
//expectedType: &dockerConfig{},
//},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
// Convert the config to a value
v := configToHcl2Interface(t, c.config)
t.Logf("value: % #v", pretty.Formatter(v))
// Parse the interface
ctyValue, diag := ParseHclInterface(v, c.spec, c.ctx)
if diag.HasErrors() {
for _, err := range diag.Errs() {
t.Error(err)
}
t.FailNow()
}
// Convert cty-value to go structs
require.NoError(t, gocty.FromCtyValue(ctyValue, c.expectedType))
require.EqualValues(t, c.expected, c.expectedType)
})
}
}

View file

@ -3,6 +3,7 @@ package hcldec
import (
"bytes"
"fmt"
"sort"
"github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
@ -765,6 +766,163 @@ func (s *BlockMapSpec) sourceRange(content *hcl.BodyContent, blockLabels []block
return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)
}
// A BlockAttrsSpec is a Spec that interprets a single block as if it were
// a map of some element type. That is, each attribute within the block
// becomes a key in the resulting map and the attribute's value becomes the
// element value, after conversion to the given element type. The resulting
// value is a cty.Map of the given element type.
//
// This spec imposes a validation constraint that there be exactly one block
// of the given type name and that this block may contain only attributes. The
// block does not accept any labels.
//
// This is an alternative to an AttrSpec of a map type for situations where
// block syntax is desired. Note that block syntax does not permit dynamic
// keys, construction of the result via a "for" expression, etc. In most cases
// an AttrSpec is preferred if the desired result is a map whose keys are
// chosen by the user rather than by schema.
type BlockAttrsSpec struct {
TypeName string
ElementType cty.Type
Required bool
}
func (s *BlockAttrsSpec) visitSameBodyChildren(cb visitFunc) {
// leaf node
}
// blockSpec implementation
func (s *BlockAttrsSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
return []hcl.BlockHeaderSchema{
{
Type: s.TypeName,
LabelNames: nil,
},
}
}
// blockSpec implementation
func (s *BlockAttrsSpec) nestedSpec() Spec {
// This is an odd case: we aren't actually going to apply a nested spec
// in this case, since we're going to interpret the body directly as
// attributes, but we need to return something non-nil so that the
// decoder will recognize this as a block spec. We won't actually be
// using this for anything at decode time.
return noopSpec{}
}
// specNeedingVariables implementation
func (s *BlockAttrsSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
block, _ := s.findBlock(content)
if block == nil {
return nil
}
var vars []hcl.Traversal
attrs, diags := block.Body.JustAttributes()
if diags.HasErrors() {
return nil
}
for _, attr := range attrs {
vars = append(vars, attr.Expr.Variables()...)
}
// We'll return the variables references in source order so that any
// error messages that result are also in source order.
sort.Slice(vars, func(i, j int) bool {
return vars[i].SourceRange().Start.Byte < vars[j].SourceRange().Start.Byte
})
return vars
}
func (s *BlockAttrsSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
block, other := s.findBlock(content)
if block == nil {
if s.Required {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Missing %s block", s.TypeName),
Detail: fmt.Sprintf(
"A block of type %q is required here.", s.TypeName,
),
Subject: &content.MissingItemRange,
})
}
return cty.NullVal(cty.Map(s.ElementType)), diags
}
if other != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Duplicate %s block", s.TypeName),
Detail: fmt.Sprintf(
"Only one block of type %q is allowed. Previous definition was at %s.",
s.TypeName, block.DefRange.String(),
),
Subject: &other.DefRange,
})
}
attrs, attrDiags := block.Body.JustAttributes()
diags = append(diags, attrDiags...)
if len(attrs) == 0 {
return cty.MapValEmpty(s.ElementType), diags
}
vals := make(map[string]cty.Value, len(attrs))
for name, attr := range attrs {
attrVal, attrDiags := attr.Expr.Value(ctx)
diags = append(diags, attrDiags...)
attrVal, err := convert.Convert(attrVal, s.ElementType)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid attribute value",
Detail: fmt.Sprintf("Invalid value for attribute of %q block: %s.", s.TypeName, err),
Subject: attr.Expr.Range().Ptr(),
})
attrVal = cty.UnknownVal(s.ElementType)
}
vals[name] = attrVal
}
return cty.MapVal(vals), diags
}
func (s *BlockAttrsSpec) impliedType() cty.Type {
return cty.Map(s.ElementType)
}
func (s *BlockAttrsSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
block, _ := s.findBlock(content)
if block == nil {
return content.MissingItemRange
}
return block.DefRange
}
func (s *BlockAttrsSpec) findBlock(content *hcl.BodyContent) (block *hcl.Block, other *hcl.Block) {
for _, candidate := range content.Blocks {
if candidate.Type != s.TypeName {
continue
}
if block != nil {
return block, candidate
}
block = candidate
}
return block, nil
}
// A BlockLabelSpec is a Spec that returns a cty.String representing the
// label of the block its given body belongs to, if indeed its given body
// belongs to a block. It is a programming error to use this in a non-block
@ -1038,3 +1196,28 @@ func (s *TransformFuncSpec) sourceRange(content *hcl.BodyContent, blockLabels []
// not super-accurate, because there's nothing better to return.
return s.Wrapped.sourceRange(content, blockLabels)
}
// noopSpec is a placeholder spec that does nothing, used in situations where
// a non-nil placeholder spec is required. It is not exported because there is
// no reason to use it directly; it is always an implementation detail only.
type noopSpec struct {
}
func (s noopSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return cty.NullVal(cty.DynamicPseudoType), nil
}
func (s noopSpec) impliedType() cty.Type {
return cty.DynamicPseudoType
}
func (s noopSpec) visitSameBodyChildren(cb visitFunc) {
// nothing to do
}
func (s noopSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// No useful range for a noopSpec, and nobody should be calling this anyway.
return hcl.Range{
Filename: "noopSpec",
}
}

View file

@ -49,7 +49,7 @@ func MismatchMessage(got, want cty.Type) string {
default:
// If we have nothing better to say, we'll just state what was required.
return want.FriendlyNameForConstraint() + " required"
return want.FriendlyNameForConstraint() + " required; got %v" + got.GoString()
}
}

16
vendor/vendor.json vendored
View file

@ -110,7 +110,7 @@
{"path":"github.com/go-ole/go-ole","checksumSHA1":"IvHj/4iR2nYa/S3cB2GXoyDG/xQ=","comment":"v1.2.0-4-g5005588","revision":"085abb85892dc1949567b726dff00fa226c60c45","revisionTime":"2017-07-12T17:44:59Z"},
{"path":"github.com/go-ole/go-ole/oleutil","comment":"v1.2.0-4-g5005588","revision":"50055884d646dd9434f16bbb5c9801749b9bafe4"},
{"path":"github.com/gogo/protobuf/proto","checksumSHA1":"I460dM/HmGE9DWimQvd1hJkYosU=","revision":"616a82ed12d78d24d4839363e8f3c5d3f20627cf","revisionTime":"2017-11-09T18:15:19Z"},
{"path":"github.com/golang/protobuf/proto","checksumSHA1":"Pyou8mceOASSFxc7GeXZuVdSMi0=","origin":"github.com/hashicorp/nomad/vendor/github.com/golang/protobuf/proto","revision":"b4deda0973fb4c70b50d226b1af49f3da59f5265","revisionTime":"2018-04-30T18:52:41Z","version":"v1","versionExact":"v1.1.0"},
{"path":"github.com/golang/protobuf/proto","checksumSHA1":"Pyou8mceOASSFxc7GeXZuVdSMi0=","revision":"b4deda0973fb4c70b50d226b1af49f3da59f5265","revisionTime":"2018-04-30T18:52:41Z","version":"v1","versionExact":"v1.1.0"},
{"path":"github.com/golang/protobuf/ptypes","checksumSHA1":"/s0InJhSrxhTpqw5FIKgSMknCfM=","revision":"b4deda0973fb4c70b50d226b1af49f3da59f5265","revisionTime":"2018-04-30T18:52:41Z","version":"v1","versionExact":"v1.1.0"},
{"path":"github.com/golang/protobuf/ptypes/any","checksumSHA1":"3eqU9o+NMZSLM/coY5WDq7C1uKg=","revision":"b4deda0973fb4c70b50d226b1af49f3da59f5265","revisionTime":"2018-04-30T18:52:41Z","version":"v1","versionExact":"v1.1.0"},
{"path":"github.com/golang/protobuf/ptypes/duration","checksumSHA1":"ZIF0rnVzNLluFPqUebtJrVonMr4=","revision":"b4deda0973fb4c70b50d226b1af49f3da59f5265","revisionTime":"2018-04-30T18:52:41Z","version":"v1","versionExact":"v1.1.0"},
@ -180,13 +180,13 @@
{"path":"github.com/hashicorp/hcl/json/parser","checksumSHA1":"138aCV5n8n7tkGYMsMVQQnnLq+0=","revision":"6e968a3fcdcbab092f5307fd0d85479d5af1e4dc","revisionTime":"2016-11-01T18:00:25Z"},
{"path":"github.com/hashicorp/hcl/json/scanner","checksumSHA1":"YdvFsNOMSWMLnY6fcliWQa0O5Fw=","revision":"6e968a3fcdcbab092f5307fd0d85479d5af1e4dc","revisionTime":"2016-11-01T18:00:25Z"},
{"path":"github.com/hashicorp/hcl/json/token","checksumSHA1":"fNlXQCQEnb+B3k5UDL/r15xtSJY=","revision":"6e968a3fcdcbab092f5307fd0d85479d5af1e4dc","revisionTime":"2016-11-01T18:00:25Z"},
{"path":"github.com/hashicorp/hcl2/ext/userfunc","checksumSHA1":"N2+7qc9e8zYkNy1itC+kWTKBTIo=","revision":"77c0b55a597ce9ff855c699dba2a99c1632690e1","revisionTime":"2018-08-01T15:45:22Z"},
{"path":"github.com/hashicorp/hcl2/gohcl","checksumSHA1":"BRJaQcKriVKEirVC7YxBxPufQF0=","revision":"77c0b55a597ce9ff855c699dba2a99c1632690e1","revisionTime":"2018-08-01T15:45:22Z"},
{"path":"github.com/hashicorp/hcl2/hcl","checksumSHA1":"LotrMqeWeTv/rNOGUHRs9iVBjoQ=","revision":"77c0b55a597ce9ff855c699dba2a99c1632690e1","revisionTime":"2018-08-01T15:45:22Z"},
{"path":"github.com/hashicorp/hcl2/hcl/hclsyntax","checksumSHA1":"RNoOVGaFtYqaPMyARZuHc2OejDs=","revision":"77c0b55a597ce9ff855c699dba2a99c1632690e1","revisionTime":"2018-08-01T15:45:22Z"},
{"path":"github.com/hashicorp/hcl2/hcl/json","checksumSHA1":"4Cr8I/nepYf4eRCl5hiazPf+afs=","revision":"77c0b55a597ce9ff855c699dba2a99c1632690e1","revisionTime":"2018-08-01T15:45:22Z"},
{"path":"github.com/hashicorp/hcl2/hcldec","checksumSHA1":"iIVMnRuvfOy/tJ1zE9rVcjD/01A=","revision":"77c0b55a597ce9ff855c699dba2a99c1632690e1","revisionTime":"2018-08-01T15:45:22Z"},
{"path":"github.com/hashicorp/hcl2/hclparse","checksumSHA1":"IzmftuG99BqNhbFGhxZaGwtiMtM=","revision":"77c0b55a597ce9ff855c699dba2a99c1632690e1","revisionTime":"2018-08-01T15:45:22Z"},
{"path":"github.com/hashicorp/hcl2/ext/userfunc","checksumSHA1":"N2+7qc9e8zYkNy1itC+kWTKBTIo=","revision":"6743a2254ba3d642b7d3a0be506259a0842819ac","revisionTime":"2018-08-10T01:10:00Z"},
{"path":"github.com/hashicorp/hcl2/gohcl","checksumSHA1":"BRJaQcKriVKEirVC7YxBxPufQF0=","revision":"6743a2254ba3d642b7d3a0be506259a0842819ac","revisionTime":"2018-08-10T01:10:00Z"},
{"path":"github.com/hashicorp/hcl2/hcl","checksumSHA1":"LotrMqeWeTv/rNOGUHRs9iVBjoQ=","revision":"6743a2254ba3d642b7d3a0be506259a0842819ac","revisionTime":"2018-08-10T01:10:00Z"},
{"path":"github.com/hashicorp/hcl2/hcl/hclsyntax","checksumSHA1":"RNoOVGaFtYqaPMyARZuHc2OejDs=","revision":"6743a2254ba3d642b7d3a0be506259a0842819ac","revisionTime":"2018-08-10T01:10:00Z"},
{"path":"github.com/hashicorp/hcl2/hcl/json","checksumSHA1":"4Cr8I/nepYf4eRCl5hiazPf+afs=","revision":"6743a2254ba3d642b7d3a0be506259a0842819ac","revisionTime":"2018-08-10T01:10:00Z"},
{"path":"github.com/hashicorp/hcl2/hcldec","checksumSHA1":"wQ3hLj4s+5jN6LePSpT0XTTvdXA=","revision":"6743a2254ba3d642b7d3a0be506259a0842819ac","revisionTime":"2018-08-10T01:10:00Z"},
{"path":"github.com/hashicorp/hcl2/hclparse","checksumSHA1":"IzmftuG99BqNhbFGhxZaGwtiMtM=","revision":"6743a2254ba3d642b7d3a0be506259a0842819ac","revisionTime":"2018-08-10T01:10:00Z"},
{"path":"github.com/hashicorp/logutils","revision":"0dc08b1671f34c4250ce212759ebd880f743d883"},
{"path":"github.com/hashicorp/memberlist","checksumSHA1":"1zk7IeGClUqBo+Phsx89p7fQ/rQ=","revision":"23ad4b7d7b38496cd64c241dfd4c60b7794c254a","revisionTime":"2017-02-08T21:15:06Z"},
{"path":"github.com/hashicorp/net-rpc-msgpackrpc","revision":"a14192a58a694c123d8fe5481d4a4727d6ae82f3"},