Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
小 白蛋
Nomad
Commits
332b701a
Commit
332b701a
authored
6 years ago
by
Alex Dadgar
Browse files
Options
Download
Email Patches
Plain Diff
hcl interface to cty function
parent
827dbabc
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
plugins/shared/util.go
+69
-0
plugins/shared/util.go
plugins/shared/util_test.go
+692
-0
plugins/shared/util_test.go
with
761 additions
and
0 deletions
+761
-0
plugins/shared/util.go
0 → 100644
+
69
-
0
View file @
332b701a
package
shared
import
(
"bytes"
"fmt"
hjson
"github.com/hashicorp/hcl2/hcl/json"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/ugorji/go/codec"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
)
// ParseHclInterface is used to convert an interface value representing a hcl2
// body and return the interpolated value.
func
ParseHclInterface
(
val
interface
{},
spec
hcldec
.
Spec
,
ctx
*
hcl
.
EvalContext
)
(
cty
.
Value
,
hcl
.
Diagnostics
)
{
// Encode to json
var
buf
bytes
.
Buffer
enc
:=
codec
.
NewEncoder
(
&
buf
,
structs
.
JsonHandle
)
err
:=
enc
.
Encode
(
val
)
if
err
!=
nil
{
// Convert to a hcl diagnostics message
return
cty
.
NilVal
,
hcl
.
Diagnostics
([]
*
hcl
.
Diagnostic
{
{
Severity
:
hcl
.
DiagError
,
Summary
:
"Failed to JSON encode value"
,
Detail
:
fmt
.
Sprintf
(
"JSON encoding failed: %v"
,
err
),
}})
}
// Parse the json as hcl2
hclFile
,
diag
:=
hjson
.
Parse
(
buf
.
Bytes
(),
""
)
if
diag
.
HasErrors
()
{
return
cty
.
NilVal
,
diag
}
value
,
decDiag
:=
hcldec
.
Decode
(
hclFile
.
Body
,
spec
,
ctx
)
diag
=
diag
.
Extend
(
decDiag
)
if
diag
.
HasErrors
()
{
return
cty
.
NilVal
,
diag
}
return
value
,
diag
}
// GetStdlibFuncs returns the set of stdlib functions.
func
GetStdlibFuncs
()
map
[
string
]
function
.
Function
{
return
map
[
string
]
function
.
Function
{
"abs"
:
stdlib
.
AbsoluteFunc
,
"coalesce"
:
stdlib
.
CoalesceFunc
,
"concat"
:
stdlib
.
ConcatFunc
,
"hasindex"
:
stdlib
.
HasIndexFunc
,
"int"
:
stdlib
.
IntFunc
,
"jsondecode"
:
stdlib
.
JSONDecodeFunc
,
"jsonencode"
:
stdlib
.
JSONEncodeFunc
,
"length"
:
stdlib
.
LengthFunc
,
"lower"
:
stdlib
.
LowerFunc
,
"max"
:
stdlib
.
MaxFunc
,
"min"
:
stdlib
.
MinFunc
,
"reverse"
:
stdlib
.
ReverseFunc
,
"strlen"
:
stdlib
.
StrlenFunc
,
"substr"
:
stdlib
.
SubstrFunc
,
"upper"
:
stdlib
.
UpperFunc
,
}
}
This diff is collapsed.
Click to expand it.
plugins/shared/util_test.go
0 → 100644
+
692
-
0
View file @
332b701a
package
shared
import
(
"testing"
"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"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
"github.com/zclconf/go-cty/cty"
"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
{
Name
:
"image"
,
Type
:
cty
.
String
,
Required
:
true
,
},
"args"
:
&
hcldec
.
AttrSpec
{
Name
:
"args"
,
Type
:
cty
.
List
(
cty
.
String
),
},
"pids_limit"
:
&
hcldec
.
AttrSpec
{
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
)),
},
"devices"
:
&
hcldec
.
BlockListSpec
{
TypeName
:
"devices"
,
Nested
:
hcldec
.
ObjectSpec
(
map
[
string
]
hcldec
.
Spec
{
"host_path"
:
&
hcldec
.
AttrSpec
{
Name
:
"host_path"
,
Type
:
cty
.
String
,
},
"container_path"
:
&
hcldec
.
AttrSpec
{
Name
:
"container_path"
,
Type
:
cty
.
String
,
},
"cgroup_permissions"
:
&
hcldec
.
DefaultSpec
{
Primary
:
&
hcldec
.
AttrSpec
{
Name
:
"cgroup_permissions"
,
Type
:
cty
.
String
,
},
Default
:
&
hcldec
.
LiteralSpec
{
Value
:
cty
.
StringVal
(
""
),
},
},
}),
},
},
)
)
type
dockerConfig
struct
{
Image
string
`cty:"image"`
Args
[]
string
`cty:"args"`
PidsLimit
*
int64
`cty:"pids_limit"`
PortMap
[]
map
[
string
]
string
`cty:"port_map"`
Devices
[]
DockerDevice
`cty:"devices"`
}
type
DockerDevice
struct
{
HostPath
string
`cty:"host_path"`
ContainerPath
string
`cty:"container_path"`
CgroupPermissions
string
`cty:"cgroup_permissions"`
}
func
hclConfigToInterface
(
t
*
testing
.
T
,
config
string
)
interface
{}
{
t
.
Helper
()
// Parse as we do in the jobspec parser
root
,
err
:=
hcl
.
Parse
(
config
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to hcl parse the config: %v"
,
err
)
}
// Top-level item should be a list
list
,
ok
:=
root
.
Node
.
(
*
ast
.
ObjectList
)
if
!
ok
{
t
.
Fatalf
(
"root should be an object"
)
}
var
m
map
[
string
]
interface
{}
if
err
:=
hcl
.
DecodeObject
(
&
m
,
list
.
Items
[
0
]);
err
!=
nil
{
t
.
Fatalf
(
"failed to decode object: %v"
,
err
)
}
var
m2
map
[
string
]
interface
{}
if
err
:=
mapstructure
.
WeakDecode
(
m
,
&
m2
);
err
!=
nil
{
t
.
Fatalf
(
"failed to weak decode object: %v"
,
err
)
}
return
m2
[
"config"
]
}
func
jsonConfigToInterface
(
t
*
testing
.
T
,
config
string
)
interface
{}
{
t
.
Helper
()
// Decode from json
dec
:=
codec
.
NewDecoderBytes
([]
byte
(
config
),
structs
.
JsonHandle
)
var
m
map
[
string
]
interface
{}
err
:=
dec
.
Decode
(
&
m
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to decode: %v"
,
err
)
}
return
m
[
"Config"
]
}
func
TestParseHclInterface_Hcl
(
t
*
testing
.
T
)
{
defaultCtx
:=
&
hcl2
.
EvalContext
{
Functions
:
GetStdlibFuncs
(),
}
variableCtx
:=
&
hcl2
.
EvalContext
{
Functions
:
GetStdlibFuncs
(),
Variables
:
map
[
string
]
cty
.
Value
{
"NOMAD_ALLOC_INDEX"
:
cty
.
NumberIntVal
(
2
),
"NOMAD_META_hello"
:
cty
.
StringVal
(
"world"
),
},
}
// XXX Useful for determining what cty thinks the type is
//implied, err := gocty.ImpliedType(&dockerConfig{})
//if err != nil {
//t.Fatalf("implied type failed: %v", err)
//}
//t.Logf("Implied type: %v", implied.GoString())
cases
:=
[]
struct
{
name
string
config
interface
{}
spec
hcldec
.
Spec
ctx
*
hcl2
.
EvalContext
expected
interface
{}
expectedType
interface
{}
}{
{
name
:
"single string attr"
,
config
:
hclConfigToInterface
(
t
,
`
config {
image = "redis:3.2"
}`
),
spec
:
dockerSpec
,
ctx
:
defaultCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
{
name
:
"single string attr json"
,
config
:
jsonConfigToInterface
(
t
,
`
{
"Config": {
"image": "redis:3.2"
}
}`
),
spec
:
dockerSpec
,
ctx
:
defaultCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
// ------------------------------------------------
{
name
:
"number attr"
,
config
:
hclConfigToInterface
(
t
,
`
config {
image = "redis:3.2"
pids_limit = 2
}`
),
spec
:
dockerSpec
,
ctx
:
defaultCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
PidsLimit
:
helper
.
Int64ToPtr
(
2
),
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
{
name
:
"number attr json"
,
config
:
jsonConfigToInterface
(
t
,
`
{
"Config": {
"image": "redis:3.2",
"pids_limit": "2"
}
}`
),
spec
:
dockerSpec
,
ctx
:
defaultCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
PidsLimit
:
helper
.
Int64ToPtr
(
2
),
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
// ------------------------------------------------
{
name
:
"number attr interpolated"
,
config
:
hclConfigToInterface
(
t
,
`
config {
image = "redis:3.2"
pids_limit = "${2 + 2}"
}`
),
spec
:
dockerSpec
,
ctx
:
defaultCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
PidsLimit
:
helper
.
Int64ToPtr
(
4
),
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
{
name
:
"number attr interploated json"
,
config
:
jsonConfigToInterface
(
t
,
`
{
"Config": {
"image": "redis:3.2",
"pids_limit": "${2 + 2}"
}
}`
),
spec
:
dockerSpec
,
ctx
:
defaultCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
PidsLimit
:
helper
.
Int64ToPtr
(
4
),
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
// ------------------------------------------------
{
name
:
"multi attr"
,
config
:
hclConfigToInterface
(
t
,
`
config {
image = "redis:3.2"
args = ["foo", "bar"]
}`
),
spec
:
dockerSpec
,
ctx
:
defaultCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
Args
:
[]
string
{
"foo"
,
"bar"
},
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
{
name
:
"multi attr json"
,
config
:
jsonConfigToInterface
(
t
,
`
{
"Config": {
"image": "redis:3.2",
"args": ["foo", "bar"]
}
}`
),
spec
:
dockerSpec
,
ctx
:
defaultCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
Args
:
[]
string
{
"foo"
,
"bar"
},
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
// ------------------------------------------------
{
name
:
"multi attr variables"
,
config
:
hclConfigToInterface
(
t
,
`
config {
image = "redis:3.2"
args = ["${NOMAD_META_hello}", "${NOMAD_ALLOC_INDEX}"]
pids_limit = "${NOMAD_ALLOC_INDEX + 2}"
}`
),
spec
:
dockerSpec
,
ctx
:
variableCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
Args
:
[]
string
{
"world"
,
"2"
},
PidsLimit
:
helper
.
Int64ToPtr
(
4
),
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
{
name
:
"multi attr variables json"
,
config
:
jsonConfigToInterface
(
t
,
`
{
"Config": {
"image": "redis:3.2",
"args": ["foo", "bar"]
}
}`
),
spec
:
dockerSpec
,
ctx
:
defaultCtx
,
expected
:
&
dockerConfig
{
Image
:
"redis:3.2"
,
Args
:
[]
string
{
"foo"
,
"bar"
},
Devices
:
[]
DockerDevice
{},
},
expectedType
:
&
dockerConfig
{},
},
// ------------------------------------------------
{
name
:
"port_map"
,
config
:
hclConfigToInterface
(
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
:
"port_map 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: "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
,
`
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"
,
Devices
:
[]
DockerDevice
{
{
HostPath
:
"/dev/sda1"
,
ContainerPath
:
"/dev/xvdc"
,
CgroupPermissions
:
"r"
,
},
{
HostPath
:
"/dev/sda2"
,
ContainerPath
:
"/dev/xvdd"
,
},
},
},
expectedType
:
&
dockerConfig
{},
},
{
name
:
"devices json"
,
config
:
jsonConfigToInterface
(
t
,
`
{
"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"
,
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
)
{
t
.
Logf
(
"Val: % #v"
,
pretty
.
Formatter
(
c
.
config
))
// Parse the interface
ctyValue
,
diag
:=
ParseHclInterface
(
c
.
config
,
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
)
})
}
}
// -------------------------------------------------------------------------
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
)
})
}
}
This diff is collapsed.
Click to expand it.
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment
Menu
Projects
Groups
Snippets
Help