diff --git a/cmd/cli/manager.go b/cmd/cli/manager.go index 285f63db3..5e57308d2 100644 --- a/cmd/cli/manager.go +++ b/cmd/cli/manager.go @@ -8,6 +8,7 @@ import ( "github.com/docker/infrakit/pkg/discovery" "github.com/docker/infrakit/pkg/manager" "github.com/docker/infrakit/pkg/plugin" + metadata_template "github.com/docker/infrakit/pkg/plugin/metadata/template" "github.com/docker/infrakit/pkg/rpc/client" group_plugin "github.com/docker/infrakit/pkg/rpc/group" manager_rpc "github.com/docker/infrakit/pkg/rpc/manager" @@ -91,6 +92,21 @@ func managerCommand(plugins func() discovery.Plugins) *cobra.Command { if err != nil { return err } + + engine.WithFunctions(func() []template.Function { + return []template.Function{ + { + Name: "metadata", + Description: []string{ + "Metadata function takes a path of the form \"plugin_name/path/to/data\"", + "and calls GET on the plugin with the path \"path/to/data\".", + "It's identical to the CLI command infrakit metadata cat ...", + }, + Func: metadata_template.MetadataFunc(plugins), + }, + } + }) + view, err := engine.Render(nil) if err != nil { return err diff --git a/cmd/cli/metadata.go b/cmd/cli/metadata.go index b7bec50cc..e84354664 100644 --- a/cmd/cli/metadata.go +++ b/cmd/cli/metadata.go @@ -213,18 +213,21 @@ func metadataCommand(plugins func() discovery.Plugins) *cobra.Command { return err } - value, err := match.Get(path.Shift(1)) - if err == nil { - if value != nil { - str := value.String() - if s, err := strconv.Unquote(value.String()); err == nil { - str = s + if path.Len() == 1 { + fmt.Printf("%v\n", match != nil) + } else { + value, err := match.Get(path.Shift(1)) + if err == nil { + if value != nil { + str := value.String() + if s, err := strconv.Unquote(value.String()); err == nil { + str = s + } + fmt.Println(str) } - fmt.Println(str) + } else { + log.Warningln("Cannot metadata cat on plugin", *first, "err=", err) } - - } else { - log.Warningln("Cannot metadata cat on plugin", *first, "err=", err) } } } diff --git a/cmd/cli/template.go b/cmd/cli/template.go index 6e6034d37..ce1906471 100644 --- a/cmd/cli/template.go +++ b/cmd/cli/template.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strings" log "github.com/Sirupsen/logrus" "github.com/docker/infrakit/pkg/discovery" @@ -12,6 +13,7 @@ import ( func templateCommand(plugins func() discovery.Plugins) *cobra.Command { + globals := []string{} templateURL := "" cmd := &cobra.Command{ Use: "template", @@ -27,6 +29,18 @@ func templateCommand(plugins func() discovery.Plugins) *cobra.Command { } // Add functions + for _, global := range globals { + kv := strings.Split(global, "=") + if len(kv) != 2 { + continue + } + key := strings.Trim(kv[0], " \t\n") + val := strings.Trim(kv[1], " \t\n") + if key != "" && val != "" { + engine.Global(key, val) + } + } + engine.WithFunctions(func() []template.Function { return []template.Function{ { @@ -50,6 +64,7 @@ func templateCommand(plugins func() discovery.Plugins) *cobra.Command { }, } cmd.Flags().StringVar(&templateURL, "url", "", "URL for the template") + cmd.Flags().StringSliceVar(&globals, "global", []string{}, "key=value pairs of 'global' values") return cmd } diff --git a/examples/flavor/swarm/flavor.go b/examples/flavor/swarm/flavor.go index 69ad16915..3befe74b8 100644 --- a/examples/flavor/swarm/flavor.go +++ b/examples/flavor/swarm/flavor.go @@ -10,8 +10,10 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "github.com/docker/go-connections/tlsconfig" + "github.com/docker/infrakit/pkg/discovery" group_types "github.com/docker/infrakit/pkg/plugin/group/types" metadata_plugin "github.com/docker/infrakit/pkg/plugin/metadata" + metadata_template "github.com/docker/infrakit/pkg/plugin/metadata/template" "github.com/docker/infrakit/pkg/spi/flavor" "github.com/docker/infrakit/pkg/spi/instance" "github.com/docker/infrakit/pkg/spi/metadata" @@ -65,6 +67,7 @@ type baseFlavor struct { getDockerClient func(Spec) (client.APIClient, error) initScript *template.Template metadataPlugin metadata.Plugin + plugins func() discovery.Plugins } // Runs a poller that periodically samples the swarm status and node info. @@ -183,6 +186,11 @@ func (s *baseFlavor) prepare(role string, flavorProperties *types.Any, instanceS allocation group_types.AllocationMethod) (instance.Spec, error) { spec := Spec{} + + if s.plugins == nil { + return instanceSpec, fmt.Errorf("no plugin discovery") + } + err := flavorProperties.Decode(&spec) if err != nil { return instanceSpec, err @@ -217,7 +225,7 @@ func (s *baseFlavor) prepare(role string, flavorProperties *types.Any, instanceS swarmStatus, node, err = swarmState(dockerClient) if err != nil { - log.Warningln("Worker prepare:", err) + log.Warningln("Cannot prepare:", err) } swarmID = "?" @@ -235,6 +243,19 @@ func (s *baseFlavor) prepare(role string, flavorProperties *types.Any, instanceS link: *link, } + initTemplate.WithFunctions(func() []template.Function { + return []template.Function{ + { + Name: "metadata", + Description: []string{ + "Metadata function takes a path of the form \"plugin_name/path/to/data\"", + "and calls GET on the plugin with the path \"path/to/data\".", + "It's identical to the CLI command infrakit metadata cat ...", + }, + Func: metadata_template.MetadataFunc(s.plugins), + }, + } + }) initScript, err = initTemplate.Render(context) log.Debugln(role, ">>> context.retries =", context.retries, "err=", err, "i=", i) diff --git a/examples/flavor/swarm/flavor_test.go b/examples/flavor/swarm/flavor_test.go index aa60e5b5e..4c0101af4 100644 --- a/examples/flavor/swarm/flavor_test.go +++ b/examples/flavor/swarm/flavor_test.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" docker_client "github.com/docker/docker/client" + "github.com/docker/infrakit/pkg/discovery" mock_client "github.com/docker/infrakit/pkg/mock/docker/docker/client" group_types "github.com/docker/infrakit/pkg/plugin/group/types" "github.com/docker/infrakit/pkg/spi/flavor" @@ -26,16 +27,24 @@ func templ(tpl string) *template.Template { return t } +func plugins() discovery.Plugins { + d, err := discovery.NewPluginDiscovery() + if err != nil { + panic(err) + } + return d +} + func TestValidate(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() managerStop := make(chan struct{}) workerStop := make(chan struct{}) - managerFlavor := NewManagerFlavor(func(Spec) (docker_client.APIClient, error) { + managerFlavor := NewManagerFlavor(plugins, func(Spec) (docker_client.APIClient, error) { return mock_client.NewMockAPIClient(ctrl), nil }, templ(DefaultManagerInitScriptTemplate), managerStop) - workerFlavor := NewWorkerFlavor(func(Spec) (docker_client.APIClient, error) { + workerFlavor := NewWorkerFlavor(plugins, func(Spec) (docker_client.APIClient, error) { return mock_client.NewMockAPIClient(ctrl), nil }, templ(DefaultWorkerInitScriptTemplate), workerStop) @@ -90,7 +99,7 @@ func TestWorker(t *testing.T) { client := mock_client.NewMockAPIClient(ctrl) - flavorImpl := NewWorkerFlavor(func(Spec) (docker_client.APIClient, error) { + flavorImpl := NewWorkerFlavor(plugins, func(Spec) (docker_client.APIClient, error) { return client, nil }, templ(DefaultWorkerInitScriptTemplate), workerStop) @@ -160,7 +169,7 @@ func TestManager(t *testing.T) { client := mock_client.NewMockAPIClient(ctrl) - flavorImpl := NewManagerFlavor(func(Spec) (docker_client.APIClient, error) { + flavorImpl := NewManagerFlavor(plugins, func(Spec) (docker_client.APIClient, error) { return client, nil }, templ(DefaultManagerInitScriptTemplate), managerStop) diff --git a/examples/flavor/swarm/main.go b/examples/flavor/swarm/main.go index 03a9132f6..444aca59e 100644 --- a/examples/flavor/swarm/main.go +++ b/examples/flavor/swarm/main.go @@ -29,10 +29,20 @@ var defaultTemplateOptions = template.Options{ func main() { + plugins := func() discovery.Plugins { + d, err := discovery.NewPluginDiscovery() + if err != nil { + log.Fatalf("Failed to initialize plugin discovery: %s", err) + os.Exit(1) + } + return d + } + cmd := &cobra.Command{ Use: os.Args[0], Short: "Docker Swarm flavor plugin", } + name := cmd.Flags().String("name", "flavor-swarm", "Plugin name to advertise for discovery") logLevel := cmd.Flags().Int("log", cli.DefaultLogLevel, "Logging level. 0 is least verbose. Max is 5") managerInitScriptTemplURL := cmd.Flags().String("manager-init-template", "", "URL, init script template for managers") @@ -53,8 +63,9 @@ func main() { managerStop := make(chan struct{}) workerStop := make(chan struct{}) - managerFlavor := NewManagerFlavor(DockerClient, mt, managerStop) - workerFlavor := NewWorkerFlavor(DockerClient, wt, workerStop) + + managerFlavor := NewManagerFlavor(plugins, DockerClient, mt, managerStop) + workerFlavor := NewWorkerFlavor(plugins, DockerClient, wt, workerStop) cli.RunPlugin(*name, diff --git a/examples/flavor/swarm/manager.go b/examples/flavor/swarm/manager.go index e356e5110..27c7521b6 100644 --- a/examples/flavor/swarm/manager.go +++ b/examples/flavor/swarm/manager.go @@ -5,6 +5,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/client" + "github.com/docker/infrakit/pkg/discovery" group_types "github.com/docker/infrakit/pkg/plugin/group/types" "github.com/docker/infrakit/pkg/plugin/metadata" "github.com/docker/infrakit/pkg/spi/instance" @@ -13,10 +14,11 @@ import ( ) // NewManagerFlavor creates a flavor.Plugin that creates manager and worker nodes connected in a swarm. -func NewManagerFlavor(connect func(Spec) (client.APIClient, error), templ *template.Template, +func NewManagerFlavor(plugins func() discovery.Plugins, connect func(Spec) (client.APIClient, error), + templ *template.Template, stop <-chan struct{}) *ManagerFlavor { - base := &baseFlavor{initScript: templ, getDockerClient: connect} + base := &baseFlavor{initScript: templ, getDockerClient: connect, plugins: plugins} base.metadataPlugin = metadata.NewPluginFromChannel(base.runMetadataSnapshot(stop)) return &ManagerFlavor{baseFlavor: base} } diff --git a/examples/flavor/swarm/worker.go b/examples/flavor/swarm/worker.go index 8708c472d..dc2d02226 100644 --- a/examples/flavor/swarm/worker.go +++ b/examples/flavor/swarm/worker.go @@ -7,6 +7,7 @@ import ( docker_types "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" + "github.com/docker/infrakit/pkg/discovery" group_types "github.com/docker/infrakit/pkg/plugin/group/types" "github.com/docker/infrakit/pkg/plugin/metadata" "github.com/docker/infrakit/pkg/spi/instance" @@ -16,10 +17,11 @@ import ( ) // NewWorkerFlavor creates a flavor.Plugin that creates manager and worker nodes connected in a swarm. -func NewWorkerFlavor(connect func(Spec) (client.APIClient, error), templ *template.Template, +func NewWorkerFlavor(plugins func() discovery.Plugins, connect func(Spec) (client.APIClient, error), + templ *template.Template, stop <-chan struct{}) *WorkerFlavor { - base := &baseFlavor{initScript: templ, getDockerClient: connect} + base := &baseFlavor{initScript: templ, getDockerClient: connect, plugins: plugins} base.metadataPlugin = metadata.NewPluginFromChannel(base.runMetadataSnapshot(stop)) return &WorkerFlavor{baseFlavor: base} } diff --git a/pkg/plugin/metadata/reflect.go b/pkg/plugin/metadata/reflect.go index e8e3ff67b..e3133a9fb 100644 --- a/pkg/plugin/metadata/reflect.go +++ b/pkg/plugin/metadata/reflect.go @@ -42,29 +42,27 @@ func List(path []string, object interface{}) []string { if v == nil { return list } + + val := reflect.Indirect(reflect.ValueOf(v)) if any, is := v.(*types.Any); is { - temp := map[string]interface{}{} + var temp interface{} if err := any.Decode(&temp); err == nil { - if len(temp) > 0 { - return List([]string{"."}, temp) - } - return []string{} + val = reflect.ValueOf(temp) } - return []string{} } - val := reflect.Indirect(reflect.ValueOf(v)) switch val.Kind() { case reflect.Slice: // this is a slice, so return the name as '[%d]' for i := 0; i < val.Len(); i++ { - list = append(list, fmt.Sprintf("[%d]", i)) //val.Index(i).String()) + list = append(list, fmt.Sprintf("[%d]", i)) } case reflect.Map: for _, k := range val.MapKeys() { list = append(list, k.String()) } + case reflect.Struct: vt := val.Type() for i := 0; i < vt.NumField(); i++ { @@ -122,6 +120,14 @@ func get(path []string, object interface{}) interface{} { return object } + if any, is := object.(*types.Any); is { + var temp interface{} + if err := any.Decode(&temp); err == nil { + return get(path, temp) + } + return nil + } + key := path[0] switch key { @@ -139,14 +145,6 @@ func get(path []string, object interface{}) interface{} { return get(path, object) } - if any, is := object.(*types.Any); is { - temp := map[string]interface{}{} - if err := any.Decode(&temp); err == nil { - return get(path[1:], temp) - } - return nil - } - v := reflect.Indirect(reflect.ValueOf(object)) switch v.Kind() { case reflect.Slice: diff --git a/pkg/plugin/metadata/reflect_test.go b/pkg/plugin/metadata/reflect_test.go index 854fc685b..e5b69d2fc 100644 --- a/pkg/plugin/metadata/reflect_test.go +++ b/pkg/plugin/metadata/reflect_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "testing" + "github.com/docker/infrakit/pkg/types" "github.com/stretchr/testify/require" ) @@ -38,9 +39,10 @@ func TestMap(t *testing.T) { require.True(t, put(Path("region/us-west-2/vpc/vpc21/network/network210/id"), "id-network210", m)) require.True(t, put(Path("region/us-west-2/vpc/vpc21/network/network211/id"), "id-network211", m)) require.True(t, put(Path("region/us-west-2/metrics/instances/count"), 100, m)) + require.True(t, put(Path("region/us-west-2/instances"), types.AnyValueMust([]string{"a", "b"}), m)) - require.Equal(t, "id-network1", get(Path("region/us-west-1/vpc/vpc1/network/network1/id"), m)) - require.Equal(t, "id-network1", get(Path("region/us-west-1/vpc/vpc1/network/network1/id/"), m)) + require.Equal(t, "id-network1", Get(Path("region/us-west-1/vpc/vpc1/network/network1/id"), m)) + require.Equal(t, "id-network1", Get(Path("region/us-west-1/vpc/vpc1/network/network1/id/"), m)) require.Equal(t, map[string]interface{}{"id": "id-network1"}, get(Path("region/us-west-1/vpc/vpc1/network/network1"), m)) require.Equal(t, map[string]interface{}{ @@ -65,6 +67,12 @@ func TestMap(t *testing.T) { require.Equal(t, []string{"us-west-1", "us-west-2"}, List(Path("region"), m)) require.Equal(t, []string{"network1", "network2", "network3"}, List(Path("region/us-west-1/vpc/vpc1/network/"), m)) require.Equal(t, []string{}, List(Path("region/us-west-2/metrics/instances/count"), m)) + require.Equal(t, []string{"[0]", "[1]"}, List(Path("region/us-west-2/instances"), m)) + require.Equal(t, []string{}, List(Path("region/us-west-2/instances/[0]"), m)) + require.Equal(t, "a", Get(Path("region/us-west-2/instances/[0]"), m)) + require.Equal(t, "b", Get(Path("region/us-west-2/instances/[1]"), m)) + require.Equal(t, "a", Get(Path("region/us-west-2/instances[0]"), m)) + require.Equal(t, "b", Get(Path("region/us-west-2/instances[1]"), m)) } diff --git a/pkg/plugin/metadata/template/template.go b/pkg/plugin/metadata/template/template.go index 7d395bcf3..e84053441 100644 --- a/pkg/plugin/metadata/template/template.go +++ b/pkg/plugin/metadata/template/template.go @@ -11,9 +11,16 @@ import ( ) // MetadataFunc returns a template function to support metadata retrieval in templates. -func MetadataFunc(plugins func() discovery.Plugins) func(string) (interface{}, error) { +func MetadataFunc(discovery func() discovery.Plugins) func(string) (interface{}, error) { + + plugins := discovery + return func(path string) (interface{}, error) { + if plugins == nil { + return nil, fmt.Errorf("no plugin discovery:%s", path) + } + mpath := metadata_plugin.Path(path) first := mpath.Index(0) if first == nil { @@ -27,7 +34,9 @@ func MetadataFunc(plugins func() discovery.Plugins) func(string) (interface{}, e endpoint, has := lookup[*first] if !has { - return nil, fmt.Errorf("plugin: %s not found", *first) + return false, nil // Don't return error. Just return false for non-existence + } else if mpath.Len() == 1 { + return true, nil // This is a test for availability of the plugin } rpcClient, err := client.New(endpoint.Address, metadata.InterfaceSpec) @@ -35,6 +44,15 @@ func MetadataFunc(plugins func() discovery.Plugins) func(string) (interface{}, e return nil, fmt.Errorf("cannot connect to plugin: %s", *first) } - return metadata_rpc.Adapt(rpcClient).Get(mpath.Shift(1)) + any, err := metadata_rpc.Adapt(rpcClient).Get(mpath.Shift(1)) + if err != nil { + return nil, err + } + var value interface{} + err = any.Decode(&value) + if err != nil { + return any.String(), err + } + return value, nil } } diff --git a/pkg/template/template.go b/pkg/template/template.go index 58e5f7e72..8a2b9b6f0 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -180,6 +180,10 @@ func (t *Template) forkFrom(parent *Template) (dotCopy interface{}, err error) { for k, v := range parent.funcs { t.AddFunc(k, v) } + // inherit other functions + for _, ff := range parent.functions { + t.functions = append(t.functions, ff) + } if parent.context != nil { return DeepCopyObject(parent.context) }