diff --git a/pkg/rpc/instance/rpc_test.go b/pkg/rpc/instance/rpc_test.go index b371be660..133142ccf 100644 --- a/pkg/rpc/instance/rpc_test.go +++ b/pkg/rpc/instance/rpc_test.go @@ -3,6 +3,8 @@ package instance import ( "encoding/json" "errors" + "io/ioutil" + "path" "path/filepath" "testing" @@ -10,8 +12,6 @@ import ( rpc_server "github.com/docker/infrakit/pkg/rpc/server" "github.com/docker/infrakit/pkg/spi/instance" "github.com/stretchr/testify/require" - "io/ioutil" - "path" ) type testPlugin struct { diff --git a/pkg/template/fetch_test.go b/pkg/template/fetch_test.go new file mode 100644 index 000000000..aeb0ce94e --- /dev/null +++ b/pkg/template/fetch_test.go @@ -0,0 +1,73 @@ +package template + +import ( + "encoding/json" + "io/ioutil" + "path" + "path/filepath" + "testing" + + rpc "github.com/docker/infrakit/pkg/rpc/instance" + rpc_server "github.com/docker/infrakit/pkg/rpc/server" + "github.com/docker/infrakit/pkg/spi/instance" + "github.com/stretchr/testify/require" +) + +type testPlugin struct { + // Validate performs local validation on a provision request. + DoValidate func(req json.RawMessage) error + + // Provision creates a new instance based on the spec. + DoProvision func(spec instance.Spec) (*instance.ID, error) + + // Destroy terminates an existing instance. + DoDestroy func(instance instance.ID) error + + // DescribeInstances returns descriptions of all instances matching all of the provided tags. + DoDescribeInstances func(tags map[string]string) ([]instance.Description, error) +} + +func (t *testPlugin) Validate(req json.RawMessage) error { + return t.DoValidate(req) +} +func (t *testPlugin) Provision(spec instance.Spec) (*instance.ID, error) { + return t.DoProvision(spec) +} +func (t *testPlugin) Destroy(instance instance.ID) error { + return t.DoDestroy(instance) +} +func (t *testPlugin) DescribeInstances(tags map[string]string) ([]instance.Description, error) { + return t.DoDescribeInstances(tags) +} + +func tempSocket() string { + dir, err := ioutil.TempDir("", "infrakit-test-") + if err != nil { + panic(err) + } + + return path.Join(dir, "instance-impl-test") +} + +func TestFetchSocket(t *testing.T) { + socketPath := tempSocket() + dir := filepath.Dir(socketPath) + host := filepath.Base(socketPath) + + url := "unix://" + host + "/info/api.json" + + server, err := rpc_server.StartPluginAtPath(socketPath, rpc.PluginServer(&testPlugin{})) + require.NoError(t, err) + + buff, err := fetch(url, Options{SocketDir: dir}) + require.NoError(t, err) + + decoded, err := FromJSON(buff) + require.NoError(t, err) + + result, err := QueryObject("Implements[].Name | [0]", decoded) + require.NoError(t, err) + require.Equal(t, "Instance", result) + + server.Stop() +} diff --git a/pkg/template/funcs.go b/pkg/template/funcs.go index f378034c8..b8f0187b0 100644 --- a/pkg/template/funcs.go +++ b/pkg/template/funcs.go @@ -9,51 +9,75 @@ import ( "github.com/jmespath/go-jmespath" ) -// DefaultFuncs returns a list of default functions for binding in the template -func (t *Template) DefaultFuncs() map[string]interface{} { - return map[string]interface{}{ - "unixtime": func() interface{} { - return time.Now().Unix() - }, +// QueryObject applies a JMESPath query specified by the expression, against the target object. +func QueryObject(exp string, target interface{}) (interface{}, error) { + query, err := jmespath.Compile(exp) + if err != nil { + return nil, err + } + return query.Search(target) +} - "var": func(name, doc string, v ...interface{}) interface{} { - if found, has := t.binds[name]; has { - return found - } - return v // default - }, +// SplitLines splits the input into a string slice. +func SplitLines(o interface{}) ([]string, error) { + ret := []string{} + switch o := o.(type) { + case string: + return strings.Split(o, "\n"), nil + case []byte: + return strings.Split(string(o), "\n"), nil + } + return ret, fmt.Errorf("not-supported-value-type") +} - "global": func(name string, v interface{}) interface{} { - t.binds[name] = v - return "" - }, +// FromJSON decode the input JSON encoded as string or byte slice into a map. +func FromJSON(o interface{}) (interface{}, error) { + ret := map[string]interface{}{} + switch o := o.(type) { + case string: + err := json.Unmarshal([]byte(o), &ret) + return ret, err + case []byte: + err := json.Unmarshal(o, &ret) + return ret, err + } + return ret, fmt.Errorf("not-supported-value-type") +} - "q": func(q string, o interface{}) (interface{}, error) { - query, err := jmespath.Compile(q) - if err != nil { - return nil, err - } - return query.Search(o) - }, +// ToJSON encodes the input struct into a JSON string. +func ToJSON(o interface{}) (string, error) { + buff, err := json.MarshalIndent(o, "", " ") + return string(buff), err +} - "jsonEncode": func(o interface{}) (string, error) { - buff, err := json.MarshalIndent(o, "", " ") - return string(buff), err - }, +// FromMap decodes map into raw struct +func FromMap(m map[string]interface{}, raw interface{}) error { + // The safest way, but the slowest, is to just marshal and unmarshal back + buff, err := ToJSON(m) + if err != nil { + return err + } + return json.Unmarshal([]byte(buff), raw) +} - "jsonDecode": func(o interface{}) (interface{}, error) { - ret := map[string]interface{}{} - switch o := o.(type) { - case string: - err := json.Unmarshal([]byte(o), &ret) - return ret, err - case []byte: - err := json.Unmarshal(o, &ret) - return ret, err - } - return ret, fmt.Errorf("not-supported-value-type") - }, +// ToMap encodes the input as a map +func ToMap(raw interface{}) (map[string]interface{}, error) { + buff, err := ToJSON(raw) + if err != nil { + return nil, err + } + out, err := FromJSON(buff) + return out.(map[string]interface{}), err +} + +// UnixTime returns a timestamp in unix time +func UnixTime() interface{} { + return time.Now().Unix() +} +// DefaultFuncs returns a list of default functions for binding in the template +func (t *Template) DefaultFuncs() map[string]interface{} { + return map[string]interface{}{ "include": func(p string, opt ...interface{}) (string, error) { var o interface{} if len(opt) > 0 { @@ -78,15 +102,22 @@ func (t *Template) DefaultFuncs() map[string]interface{} { return included.Render(o) }, - "lines": func(o interface{}) ([]string, error) { - ret := []string{} - switch o := o.(type) { - case string: - return strings.Split(o, "\n"), nil - case []byte: - return strings.Split(string(o), "\n"), nil + "var": func(name, doc string, v ...interface{}) interface{} { + if found, has := t.binds[name]; has { + return found } - return ret, fmt.Errorf("not-supported-value-type") + return v // default }, + + "global": func(name string, v interface{}) interface{} { + t.binds[name] = v + return "" + }, + + "q": QueryObject, + "unixtime": UnixTime, + "lines": SplitLines, + "to_json": ToJSON, + "from_json": FromJSON, } } diff --git a/pkg/template/funcs_test.go b/pkg/template/funcs_test.go new file mode 100644 index 000000000..5610fd330 --- /dev/null +++ b/pkg/template/funcs_test.go @@ -0,0 +1,282 @@ +package template + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +type testParameter struct { + ParameterKey string + ParameterValue interface{} +} + +type testResource struct { + ResourceType string + LogicalResourceID string + ResourceTypePtr *string +} + +type testCloud struct { + Parameters []testParameter + Resources []testResource + ResourcePtrs []*testResource + ResourceList []interface{} +} + +func TestQueryObjectEncodeDecode(t *testing.T) { + + param1 := testParameter{ + ParameterKey: "key1", + ParameterValue: "value1", + } + + result, err := QueryObject("Parameters[?ParameterKey=='key1'] | [0]", + testCloud{ + Parameters: []testParameter{ + param1, + { + ParameterKey: "key2", + ParameterValue: "value2", + }, + }, + }) + require.NoError(t, err) + + encoded, err := ToJSON(param1) + require.NoError(t, err) + + encoded2, err := ToJSON(result) + require.Equal(t, encoded, encoded2) + + decoded, err := FromJSON(encoded) + require.NoError(t, err) + + decoded2, err := FromJSON([]byte(encoded2)) + require.NoError(t, err) + + require.Equal(t, decoded, decoded2) +} + +func TestQueryObject(t *testing.T) { + + instanceType := testParameter{ + ParameterKey: "instance-type", + ParameterValue: "m2xlarge", + } + ami := testParameter{ + ParameterKey: "ami", + ParameterValue: "ami-1234", + } + + vpc := testResource{ + ResourceType: "AWS::EC2::VPC", + LogicalResourceID: "Vpc", + } + subnet := testResource{ + ResourceType: "AWS::EC2::Subnet", + LogicalResourceID: "Subnet", + } + cloud := testCloud{ + Parameters: []testParameter{ + instanceType, + ami, + }, + Resources: []testResource{ + vpc, + subnet, + }, + } + + { + result, err := QueryObject("Resources[?ResourceType=='AWS::EC2::Subnet'] | [0]", cloud) + require.NoError(t, err) + + encoded, err := ToJSON(subnet) + require.NoError(t, err) + + encoded2, err := ToJSON(result) + require.Equal(t, encoded, encoded2) + } + { + result, err := QueryObject("Resources", cloud) + require.NoError(t, err) + + encoded, err := ToJSON([]testResource{vpc, subnet}) + require.NoError(t, err) + + encoded2, err := ToJSON(result) + require.Equal(t, encoded, encoded2) + } + { + result, err := QueryObject("Resources[?ResourceType=='AWS::EC2::VPC'] | [0].LogicalResourceID", cloud) + require.NoError(t, err) + require.Equal(t, "Vpc", result) + } + +} + +func TestQueryObjectPtrs(t *testing.T) { + + vpc := testResource{ + ResourceType: "AWS::EC2::VPC", + LogicalResourceID: "Vpc", + } + subnet := testResource{ + ResourceType: "AWS::EC2::Subnet", + LogicalResourceID: "Subnet", + } + cloudTyped := testCloud{ + ResourcePtrs: []*testResource{ + &vpc, + &subnet, + }, + } + + doc, err := ToJSON(cloudTyped) + require.NoError(t, err) + cloud, err := FromJSON(doc) + require.NoError(t, err) + + { + result, err := QueryObject("ResourcePtrs[?ResourceType=='AWS::EC2::Subnet'] | [0]", cloudTyped) + require.NoError(t, err) + + buff, _ := ToJSON(result) + actual := testResource{} + err = json.Unmarshal([]byte(buff), &actual) + require.NoError(t, err) + require.Equal(t, subnet, actual) + } + { + result, err := QueryObject("ResourcePtrs[?ResourceType=='AWS::EC2::VPC'] | [0].LogicalResourceID", cloud) + require.NoError(t, err) + require.Equal(t, "Vpc", result) + } + +} + +func TestQueryObjectInterfaces(t *testing.T) { + + vpc := testResource{ + ResourceType: "AWS::EC2::VPC", + LogicalResourceID: "Vpc", + } + subnet := testResource{ + ResourceType: "AWS::EC2::Subnet", + LogicalResourceID: "Subnet", + } + cloudTyped := testCloud{ + ResourceList: []interface{}{ + &vpc, + &subnet, + }, + } + + doc, err := ToJSON(cloudTyped) + require.NoError(t, err) + cloud, err := FromJSON(doc) + require.NoError(t, err) + + { + // query trhe map version of it + result, err := QueryObject("ResourceList[?ResourceType=='AWS::EC2::Subnet'] | [0]", cloud) + require.NoError(t, err) + + buff, _ := ToJSON(result) + actual := testResource{} + err = json.Unmarshal([]byte(buff), &actual) + require.NoError(t, err) + require.Equal(t, subnet, actual) + } + { + result, err := QueryObject("ResourceList[?ResourceType=='AWS::EC2::VPC'] | [0].LogicalResourceID", cloudTyped) + require.NoError(t, err) + require.Equal(t, "Vpc", result) + } + +} + +func strPtr(s string) *string { + out := s + return &out +} + +func TestQueryObjectStrPtrs(t *testing.T) { + + vpc := testResource{ + ResourceTypePtr: strPtr("AWS::EC2::VPC"), + LogicalResourceID: "Vpc", + } + subnet := testResource{ + ResourceTypePtr: strPtr("AWS::EC2::Subnet"), + LogicalResourceID: "Subnet", + } + cloudTyped := testCloud{ + ResourceList: []interface{}{ + &vpc, + &subnet, + }, + } + + cloud, err := ToMap(cloudTyped) + require.NoError(t, err) + + { + // query the struct version + result, err := QueryObject("ResourceList[?ResourceTypePtr=='AWS::EC2::Subnet'] | [0]", cloudTyped) + require.NoError(t, err) + require.Nil(t, result) // JMESPath cannot handle *string fields. + } + { + // query the map version of it + result, err := QueryObject("ResourceList[?ResourceTypePtr=='AWS::EC2::Subnet'] | [0]", cloud) + require.NoError(t, err) + + buff, _ := ToJSON(result) + actual := testResource{} + err = json.Unmarshal([]byte(buff), &actual) + require.NoError(t, err) + require.Equal(t, subnet, actual) + } + { + result, err := QueryObject("ResourceList[?ResourceTypePtr=='AWS::EC2::VPC'] | [0].LogicalResourceID", cloud) + require.NoError(t, err) + require.Equal(t, "Vpc", result) + } + +} + +func TestMapEncodeDecode(t *testing.T) { + + vpc := testResource{ + ResourceTypePtr: strPtr("AWS::EC2::VPC"), + LogicalResourceID: "Vpc", + } + subnet := testResource{ + ResourceTypePtr: strPtr("AWS::EC2::Subnet"), + LogicalResourceID: "Subnet", + } + cloudTyped := testCloud{ + ResourceList: []interface{}{ + &vpc, + &subnet, + }, + } + + cloud, err := ToMap(cloudTyped) + require.NoError(t, err) + + parsed := testCloud{} + err = FromMap(cloud, &parsed) + require.NoError(t, err) + + expect, err := ToMap(cloudTyped) + require.NoError(t, err) + + actual, err := ToMap(parsed) + require.NoError(t, err) + + require.Equal(t, expect, actual) +} diff --git a/pkg/template/integration_test.go b/pkg/template/integration_test.go index aa7a1707b..5ed19a701 100644 --- a/pkg/template/integration_test.go +++ b/pkg/template/integration_test.go @@ -84,11 +84,11 @@ echo "this is common/setup.sh" "test" : "test1", "description" : "simple template to test the various template functions", {{/* Load from from ./ using relative path notation. Then split into lines and json encode */}} - "userData" : {{ include "script.tpl" . | lines | jsonEncode }}, + "userData" : {{ include "script.tpl" . | lines | to_json }}, {{/* Load from an URL */}} "sample" : {{ include "https://httpbin.org/get" }}, {{/* Load from URL and then parse as JSON then select an attribute */}} - "originIp" : "{{ include "https://httpbin.org/get" | jsonDecode | q "origin" }}" + "originIp" : "{{ include "https://httpbin.org/get" | from_json | q "origin" }}" }`, "plugin/script.tpl": ` diff --git a/pkg/template/template_test.go b/pkg/template/template_test.go index 2b63291f9..61f33364c 100644 --- a/pkg/template/template_test.go +++ b/pkg/template/template_test.go @@ -9,7 +9,7 @@ import ( func TestRunTemplateWithJMESPath(t *testing.T) { // Example from http://jmespath.org/ - str := `{{ q "locations[?state == 'WA'].name | sort(@) | {WashingtonCities: join(', ', @)}" . | jsonEncode}}` + str := `{{ q "locations[?state == 'WA'].name | sort(@) | {WashingtonCities: join(', ', @)}" . | to_json}}` tpl, err := NewTemplate("str://"+str, Options{}) require.NoError(t, err) @@ -37,7 +37,7 @@ func TestVarAndGlobal(t *testing.T) { { "test" : "hello", "val" : true, - "result" : {{var "washington-cities" "A json with washington cities" | jsonEncode}} + "result" : {{var "washington-cities" "A json with washington cities" | to_json}} } ` diff --git a/vendor.conf b/vendor.conf index a84026980..8760991bc 100644 --- a/vendor.conf +++ b/vendor.conf @@ -7,31 +7,31 @@ # package github.com/docker/infrakit -github.com/aokoli/goutils 1.0.0 -github.com/Microsoft/go-winio 0.3.6 -github.com/Sirupsen/logrus v0.11.0-5-gabc6f20 -github.com/davecgh/go-spew v1.0.0-9-g346938d -github.com/docker/distribution v2.6.0-rc.1-4-g7694c31 -github.com/docker/docker 3a68292 +github.com/aokoli/goutils 1.0.0 +github.com/Microsoft/go-winio 0.3.6 +github.com/Sirupsen/logrus v0.11.0-5-gabc6f20 +github.com/davecgh/go-spew v1.0.0-9-g346938d +github.com/docker/distribution v2.6.0-rc.1-4-g7694c31 +github.com/docker/docker 3a68292 github.com/docker/go-connections v0.2.1-10-gf512407 -github.com/docker/go-units v0.3.1-8-g8a7beac -github.com/golang/mock bd3c8e8 -github.com/gorilla/handlers v1.1-12-ge1b2144 -github.com/gorilla/mux 757bef9 -github.com/gorilla/rpc 22c016f +github.com/docker/go-units v0.3.1-8-g8a7beac +github.com/golang/mock bd3c8e8 +github.com/gorilla/handlers v1.1-12-ge1b2144 +github.com/gorilla/mux 757bef9 +github.com/gorilla/rpc 22c016f github.com/inconshreveable/mousetrap 76626ae -github.com/jmespath/go-jmespath 0.2.2-14-gbd40a43 -github.com/Masterminds/sprig 2.8.0 -github.com/nightlyone/lockfile 1d49c98 -github.com/opencontainers/runc v1.0.0-rc2-137-g43c4300 -github.com/pkg/errors v0.8.0-2-g248dadf -github.com/pmezard/go-difflib v1.0.0 -github.com/satori/go.uuid v1.1.0 -github.com/spf13/afero 06b7e5f -github.com/spf13/cobra 6e91dde -github.com/spf13/pflag 5ccb023 -github.com/stretchr/testify v1.1.4-4-g976c720 -golang.org/x/net 0dd7c8d -golang.org/x/sys b699b70 -golang.org/x/text a263ba8 -gopkg.in/tylerb/graceful.v1 v1.2.13 +github.com/jmespath/go-jmespath 0.2.2-14-gbd40a43 +github.com/Masterminds/sprig 2.8.0 +github.com/nightlyone/lockfile 1d49c98 +github.com/opencontainers/runc v1.0.0-rc2-137-g43c4300 +github.com/pkg/errors v0.8.0-2-g248dadf +github.com/pmezard/go-difflib v1.0.0 +github.com/satori/go.uuid v1.1.0 +github.com/spf13/afero 06b7e5f +github.com/spf13/cobra 6e91dde +github.com/spf13/pflag 5ccb023 +github.com/stretchr/testify v1.1.4-4-g976c720 +golang.org/x/net 0dd7c8d +golang.org/x/sys b699b70 +golang.org/x/text a263ba8 +gopkg.in/tylerb/graceful.v1 v1.2.13