From 1d26d67196e1bce928046f72ad5837412e87baf2 Mon Sep 17 00:00:00 2001 From: David Chung Date: Sun, 14 May 2017 13:35:07 -0700 Subject: [PATCH 01/12] Dynamic CLI Signed-off-by: David Chung --- cmd/infrakit/base/output.go | 92 -------------- cmd/infrakit/base/template.go | 121 ++----------------- cmd/infrakit/info/info.go | 15 ++- cmd/infrakit/instance/instance.go | 78 ++++++------ cmd/infrakit/main.go | 172 ++++++++++++++------------ cmd/infrakit/metadata/metadata.go | 26 ++-- cmd/infrakit/playbook/playbook.go | 28 ++--- pkg/cli/context.go | 4 +- pkg/cli/output.go | 66 ++++++++++ pkg/cli/registry.go | 137 +++++++++++++++++++++ pkg/cli/services.go | 193 ++++++++++++++++++++++++++++++ pkg/cli/template.go | 3 +- pkg/cli/v1/flavor/cmd.go | 30 +++++ pkg/cli/v1/flavor/healthy.go | 81 +++++++++++++ pkg/cli/v1/flavor/prepare.go | 105 ++++++++++++++++ pkg/cli/v1/flavor/validate.go | 77 ++++++++++++ pkg/cli/v1/group/cmd.go | 33 +++++ pkg/cli/v1/group/commit.go | 65 ++++++++++ pkg/cli/v1/group/describe.go | 67 +++++++++++ pkg/cli/v1/group/destroy.go | 42 +++++++ pkg/cli/v1/group/free.go | 43 +++++++ pkg/cli/v1/group/inspect.go | 49 ++++++++ pkg/cli/v1/group/ls.go | 41 +++++++ pkg/cli/v1/info.go | 175 +++++++++++++++++++++++++++ pkg/cli/v1/instance/cmd.go | 31 +++++ pkg/cli/v1/instance/describe.go | 131 ++++++++++++++++++++ pkg/cli/v1/instance/destroy.go | 45 +++++++ pkg/cli/v1/instance/provision.go | 55 +++++++++ pkg/cli/v1/instance/validate.go | 49 ++++++++ pkg/rpc/client/info.go | 17 +-- pkg/rpc/handshake.go | 3 +- pkg/spi/plugin.go | 17 +++ pkg/template/template.go | 10 ++ 33 files changed, 1735 insertions(+), 366 deletions(-) delete mode 100644 cmd/infrakit/base/output.go create mode 100644 pkg/cli/output.go create mode 100644 pkg/cli/registry.go create mode 100644 pkg/cli/services.go create mode 100644 pkg/cli/v1/flavor/cmd.go create mode 100644 pkg/cli/v1/flavor/healthy.go create mode 100644 pkg/cli/v1/flavor/prepare.go create mode 100644 pkg/cli/v1/flavor/validate.go create mode 100644 pkg/cli/v1/group/cmd.go create mode 100644 pkg/cli/v1/group/commit.go create mode 100644 pkg/cli/v1/group/describe.go create mode 100644 pkg/cli/v1/group/destroy.go create mode 100644 pkg/cli/v1/group/free.go create mode 100644 pkg/cli/v1/group/inspect.go create mode 100644 pkg/cli/v1/group/ls.go create mode 100644 pkg/cli/v1/info.go create mode 100644 pkg/cli/v1/instance/cmd.go create mode 100644 pkg/cli/v1/instance/describe.go create mode 100644 pkg/cli/v1/instance/destroy.go create mode 100644 pkg/cli/v1/instance/provision.go create mode 100644 pkg/cli/v1/instance/validate.go diff --git a/cmd/infrakit/base/output.go b/cmd/infrakit/base/output.go deleted file mode 100644 index d1bf10823..000000000 --- a/cmd/infrakit/base/output.go +++ /dev/null @@ -1,92 +0,0 @@ -package base - -import ( - "fmt" - "io" - - "github.com/docker/infrakit/pkg/types" - "github.com/ghodss/yaml" - "github.com/spf13/pflag" -) - -// RawOutputFunc is a function that writes some data to the output writer -type RawOutputFunc func(w io.Writer, v interface{}) (rendered bool, err error) - -// RawOutput returns the flagset and the func for printing output -func RawOutput() (*pflag.FlagSet, RawOutputFunc) { - - fs := pflag.NewFlagSet("output", pflag.ExitOnError) - - raw := fs.BoolP("raw", "r", false, "True to dump to output instead of executing") - yamlDoc := fs.BoolP("yaml", "y", false, "True if input is in yaml format; json is the default") - - return fs, func(w io.Writer, v interface{}) (rendered bool, err error) { - if !*raw { - return false, nil - } - - any, err := types.AnyValue(v) - if err != nil { - return false, err - } - - buff := any.Bytes() - if *yamlDoc { - buff, err = yaml.JSONToYAML(buff) - } - - fmt.Fprintln(w, string(buff)) - return true, nil - } -} - -// OutputFunc is a function that writes some data to the output writer -type OutputFunc func(w io.Writer, v interface{}) (rendered bool, err error) - -// Output returns the flagset and the func for printing output -func Output() (*pflag.FlagSet, OutputFunc) { - - fs := pflag.NewFlagSet("output", pflag.ExitOnError) - - yamlDoc := fs.BoolP("yaml", "y", false, "True if input is in yaml format; json is the default") - return fs, func(w io.Writer, v interface{}) (rendered bool, err error) { - - var out string - - switch v := v.(type) { - case string: - if *yamlDoc { - if y, err := yaml.JSONToYAML([]byte(v)); err == nil { - out = string(y) - } - } else { - out = v - } - case []byte: - if *yamlDoc { - if y, err := yaml.JSONToYAML(v); err == nil { - out = string(y) - } - } else { - out = string(v) - } - default: - any, err := types.AnyValue(v) - if err != nil { - return false, err - } - - buff := any.Bytes() - if *yamlDoc { - if y, err := yaml.JSONToYAML(buff); err == nil { - out = string(y) - } - } else { - out = any.String() - } - } - - fmt.Fprintln(w, out) - return true, nil - } -} diff --git a/cmd/infrakit/base/template.go b/cmd/infrakit/base/template.go index 89e759b03..06616b02a 100644 --- a/cmd/infrakit/base/template.go +++ b/cmd/infrakit/base/template.go @@ -1,33 +1,19 @@ package base import ( - "fmt" "io/ioutil" "os" - "path" - "strings" "github.com/docker/infrakit/pkg/cli" "github.com/docker/infrakit/pkg/discovery" - "github.com/docker/infrakit/pkg/template" - "github.com/ghodss/yaml" "github.com/spf13/pflag" ) -// ProcessTemplateFunc is the function that processes the template at url and returns view or error. -type ProcessTemplateFunc func(url string) (rendered string, err error) - -// ToJSONFunc converts the input buffer to json format -type ToJSONFunc func(in []byte) (json []byte, err error) - -// FromJSONFunc converts json formatted input to output buffer -type FromJSONFunc func(json []byte) (out []byte, err error) - // ReadFromStdinIfElse checks condition and reads from stdin if true; otherwise it executes other. func ReadFromStdinIfElse( condition func() bool, otherwise func() (string, error), - toJSON ToJSONFunc) (rendered string, err error) { + toJSON cli.ToJSONFunc) (rendered string, err error) { if condition() { buff, err := ioutil.ReadAll(os.Stdin) @@ -51,103 +37,10 @@ func ReadFromStdinIfElse( } // TemplateProcessor returns a flagset and a function for processing template input. -func TemplateProcessor(plugins func() discovery.Plugins) (*pflag.FlagSet, ToJSONFunc, FromJSONFunc, ProcessTemplateFunc) { - - fs := pflag.NewFlagSet("template", pflag.ExitOnError) - - globals := fs.StringSliceP("var", "v", []string{}, "key=value pairs of globally scoped variagbles") - yamlDoc := fs.BoolP("yaml", "y", false, "True if input is in yaml format; json is the default") - dump := fs.BoolP("dump", "x", false, "True to dump to output instead of executing") - singlePass := fs.BoolP("final", "f", false, "True to render template as the final pass") - - return fs, - // ToJSONFunc - func(in []byte) (json []byte, err error) { - - defer func() { - - if *dump { - fmt.Println("Raw:") - fmt.Println(string(in)) - fmt.Println("Converted") - fmt.Println(string(json)) - os.Exit(0) // special for debugging - } - }() - - if *yamlDoc { - json, err = yaml.YAMLToJSON(in) - return - } - json = in - return - - }, - // FromJSONFunc - func(json []byte) (out []byte, err error) { - - defer func() { - - if *dump { - fmt.Println("Raw:") - fmt.Println(string(json)) - fmt.Println("Converted") - fmt.Println(string(out)) - os.Exit(0) // special for debugging - } - }() - - if *yamlDoc { - out, err = yaml.JSONToYAML(json) - return - } - out = json - return - - }, - // ProcessTemplateFunc - func(url string) (view string, err error) { - - if !strings.Contains(url, "://") { - p := url - if dir, err := os.Getwd(); err == nil { - p = path.Join(dir, url) - } - url = "file://" + p - } - - log.Debug("reading template", "url", url) - engine, err := template.NewTemplate(url, template.Options{MultiPass: !*singlePass}) - if err != nil { - return - } - - for _, global := range *globals { - kv := strings.SplitN(global, "=", 2) - if len(kv) != 2 { - log.Warn("bad format kv", "input", global) - continue - } - key := strings.TrimSpace(kv[0]) - val := strings.TrimSpace(kv[1]) - if key != "" && val != "" { - engine.Global(key, val) - } - } - - cli.ConfigureTemplate(engine, plugins) - - view, err = engine.Render(nil) - if err != nil { - return - } - - log.Debug("rendered", "view", view) - if *dump { - fmt.Println("Final:") - fmt.Println(string(view)) - os.Exit(0) - } - return - } +func TemplateProcessor(plugins func() discovery.Plugins) (*pflag.FlagSet, cli.ToJSONFunc, cli.FromJSONFunc, cli.ProcessTemplateFunc) { + services := cli.NewServices(plugins) + return services.ProcessTemplateFlags, + services.ToJSON, + services.FromJSON, + services.ProcessTemplate } diff --git a/cmd/infrakit/info/info.go b/cmd/infrakit/info/info.go index 43a041a4f..90e351018 100644 --- a/cmd/infrakit/info/info.go +++ b/cmd/infrakit/info/info.go @@ -46,7 +46,11 @@ func Command(plugins func() discovery.Plugins) *cobra.Command { return err } - infoClient := client.NewPluginInfoClient(endpoint.Address) + infoClient, err := client.NewPluginInfoClient(endpoint.Address) + if err != nil { + return err + } + info, err := infoClient.GetInfo() if err != nil { return err @@ -82,7 +86,10 @@ func Command(plugins func() discovery.Plugins) *cobra.Command { return err } - infoClient := client.NewPluginInfoClient(endpoint.Address) + infoClient, err := client.NewPluginInfoClient(endpoint.Address) + if err != nil { + return err + } info, err := infoClient.GetFunctions() if err != nil { return err @@ -124,10 +131,10 @@ Interfaces: {{range $iface := .Interfaces}} RPC: {{range $method := $iface.Methods}} Method: {{$method.Request | q "method" }} Request: - {{$method.Request | to_json_format " " " "}} + {{$method.Request | jsonEncode | yamlEncode }} Response: - {{$method.Response | to_json_format " " " "}} + {{$method.Response | jsonEncode | yamlEncode }} ------------------------- {{end}} diff --git a/cmd/infrakit/instance/instance.go b/cmd/infrakit/instance/instance.go index 66e579bfd..1cd7e7643 100644 --- a/cmd/infrakit/instance/instance.go +++ b/cmd/infrakit/instance/instance.go @@ -3,6 +3,7 @@ package instance import ( "bytes" "fmt" + "io" "os" "sort" "strings" @@ -160,7 +161,7 @@ func Command(plugins func() discovery.Plugins) *cobra.Command { tagsTemplate := describe.Flags().StringP("tags-view", "t", "*", "Template to render tags") propertiesTemplate := describe.Flags().StringP("properties-view", "v", "{{.}}", "Template to render properties") - rawOutputFlags, rawOutput := base.RawOutput() + rawOutputFlags, rawOutput := cli.Output() describe.Flags().AddFlagSet(rawOutputFlags) describe.RunE = func(cmd *cobra.Command, args []string) error { @@ -185,55 +186,50 @@ func Command(plugins func() discovery.Plugins) *cobra.Command { } desc, err := instancePlugin.DescribeInstances(filter, *properties) - if err == nil { - - rendered, err := rawOutput(os.Stdout, desc) - if err != nil { - return err - } - - if rendered { - return nil - } + if err != nil { + return err + } + return rawOutput(os.Stdout, desc, + func(io.Writer, interface{}) error { - if !*quiet { - if *properties { - fmt.Printf("%-30s\t%-30s\t%-30s\t%-s\n", "ID", "LOGICAL", "TAGS", "PROPERTIES") + if !*quiet { + if *properties { + fmt.Printf("%-30s\t%-30s\t%-30s\t%-s\n", "ID", "LOGICAL", "TAGS", "PROPERTIES") - } else { - fmt.Printf("%-30s\t%-30s\t%-s\n", "ID", "LOGICAL", "TAGS") + } else { + fmt.Printf("%-30s\t%-30s\t%-s\n", "ID", "LOGICAL", "TAGS") + } } - } - for _, d := range desc { + for _, d := range desc { - logical := " - " - if d.LogicalID != nil { - logical = string(*d.LogicalID) - } + logical := " - " + if d.LogicalID != nil { + logical = string(*d.LogicalID) + } - tagViewBuff := "" - if *tagsTemplate == "*" { - // default -- this is a hack - printTags := []string{} - for k, v := range d.Tags { - printTags = append(printTags, fmt.Sprintf("%s=%s", k, v)) + tagViewBuff := "" + if *tagsTemplate == "*" { + // default -- this is a hack + printTags := []string{} + for k, v := range d.Tags { + printTags = append(printTags, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(printTags) + tagViewBuff = strings.Join(printTags, ",") + } else { + tagViewBuff = renderTags(d.Tags, tagsView) } - sort.Strings(printTags) - tagViewBuff = strings.Join(printTags, ",") - } else { - tagViewBuff = renderTags(d.Tags, tagsView) - } - if *properties { - fmt.Printf("%-30s\t%-30s\t%-30s\t%-s\n", d.ID, logical, tagViewBuff, - renderProperties(d.Properties, propertiesView)) - } else { - fmt.Printf("%-30s\t%-30s\t%-s\n", d.ID, logical, tagViewBuff) + if *properties { + fmt.Printf("%-30s\t%-30s\t%-30s\t%-s\n", d.ID, logical, tagViewBuff, + renderProperties(d.Properties, propertiesView)) + } else { + fmt.Printf("%-30s\t%-30s\t%-s\n", d.ID, logical, tagViewBuff) + } } - } - } - return err + return nil + }) } cmd.AddCommand( diff --git a/cmd/infrakit/main.go b/cmd/infrakit/main.go index d7f831f78..754182757 100644 --- a/cmd/infrakit/main.go +++ b/cmd/infrakit/main.go @@ -19,16 +19,21 @@ import ( "github.com/docker/infrakit/pkg/types" "github.com/spf13/cobra" + _ "github.com/docker/infrakit/pkg/cli/v1" + + // TODO - deprecate these in favor of the dynamic commands (see above) + //_ "github.com/docker/infrakit/cmd/infrakit/flavor" + //_ "github.com/docker/infrakit/cmd/infrakit/instance" + //_ "github.com/docker/infrakit/cmd/infrakit/group" + //_ "github.com/docker/infrakit/cmd/infrakit/info" + _ "github.com/docker/infrakit/cmd/infrakit/event" - _ "github.com/docker/infrakit/cmd/infrakit/flavor" - _ "github.com/docker/infrakit/cmd/infrakit/group" - _ "github.com/docker/infrakit/cmd/infrakit/info" - _ "github.com/docker/infrakit/cmd/infrakit/instance" _ "github.com/docker/infrakit/cmd/infrakit/manager" _ "github.com/docker/infrakit/cmd/infrakit/metadata" + _ "github.com/docker/infrakit/cmd/infrakit/resource" + _ "github.com/docker/infrakit/cmd/infrakit/playbook" _ "github.com/docker/infrakit/cmd/infrakit/plugin" - _ "github.com/docker/infrakit/cmd/infrakit/resource" _ "github.com/docker/infrakit/cmd/infrakit/template" _ "github.com/docker/infrakit/cmd/infrakit/util" ) @@ -49,82 +54,19 @@ func main() { log := logutil.New("module", "main") + // Log setup + logOptions := &logutil.ProdDefaults + cmd := &cobra.Command{ Use: os.Args[0], Short: "infrakit cli", + PersistentPreRunE: func(c *cobra.Command, args []string) error { + logutil.Configure(logOptions) + return nil + }, } - - // Log setup - logOptions := &logutil.ProdDefaults - ulist := []*url.URL{} - remotes := []string{} - cmd.PersistentFlags().AddFlagSet(cli.Flags(logOptions)) cmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) - cmd.PersistentFlags().StringSliceVarP(&remotes, "host", "H", remotes, "host list. Default is local sockets") - - // parse the list of hosts - cmd.PersistentPreRunE = func(c *cobra.Command, args []string) error { - logutil.Configure(logOptions) - - hosts := []string{} - - if len(remotes) > 0 { - - // The command line flag wins. - hosts = remotes - - } else { - - // If not -- see if INFRAKIT_HOST is set to point to a host list in the $INFRAKIT_HOME/hosts file. - host := os.Getenv("INFRAKIT_HOST") - if host == "" { - return nil // do nothing -- local mode - } - - // If the env is set but we don't have any hosts file locally, don't exit. Print a warning and proceed. - // Now look up the host lists in the file - hostsFile := filepath.Join(os.Getenv("INFRAKIT_HOME"), "hosts") - buff, err := ioutil.ReadFile(hostsFile) - if err != nil { - return nil // do nothing -- local mode - } - m := map[string]string{} - yaml, err := types.AnyYAML(buff) - if err != nil { - return fmt.Errorf("bad format for hosts file at %s for INFRAKIT_HOST=%s, err=%v", hostsFile, host, err) - - } - err = yaml.Decode(&m) - if err != nil { - return fmt.Errorf("cannot decode hosts file at %s for INFRAKIT_HOST=%s, err=%v", hostsFile, host, err) - } - - if list, has := m[host]; has { - hosts = strings.Split(list, ",") - } else { - return fmt.Errorf("no entry in hosts file at %s for INFRAKIT_HOST=%s", hostsFile, host) - } - } - - for _, h := range hosts { - addProtocol := false - if !strings.Contains(h, "://") { - h = "http://" + h - addProtocol = true - } - u, err := url.Parse(h) - if err != nil { - return err - } - if addProtocol { - u.Scheme = "http" - } - - ulist = append(ulist, u) - } - return nil - } // Don't print usage text for any error returned from a RunE function. // Only print it when explicitly requested. @@ -135,6 +77,12 @@ func main() { cmd.SilenceErrors = true f := func() discovery.Plugins { + ulist, err := remotes() + if err != nil { + log.Crit("Cannot lookup plugins", "err", err) + os.Exit(1) + } + if len(ulist) == 0 { d, err := discovery_local.NewPluginDiscovery() if err != nil { @@ -158,6 +106,20 @@ func main() { cmd.AddCommand(c) }) + // Set environment variable to disable this feature. + if os.Getenv("INFRAKIT_DYNAMIC_CLI") != "false" { + // Load dynamic plugin commands based on discovery + pluginCommands, err := cli.LoadAll(cli.NewServices(f)) + if err != nil { + log.Crit("error loading", "cmd", cmd.Use, "err", err) + fmt.Println(err.Error()) + os.Exit(1) + } + for _, c := range pluginCommands { + cmd.AddCommand(c) + } + } + // Help template includes the usage string, which is configure below cmd.SetHelpTemplate(helpTemplate) cmd.SetUsageTemplate(usageTemplate) @@ -168,6 +130,66 @@ func main() { fmt.Println(err.Error()) os.Exit(1) } + + // write the file for bash completion if environment variable is set + bashCompletionScript := os.Getenv("INFRAKIT_BASH_COMPLETION") + if bashCompletionScript != "" { + cmd.GenBashCompletionFile(bashCompletionScript) + } +} + +func remotes() ([]*url.URL, error) { + ulist := []*url.URL{} + + hosts := []string{} + // See if INFRAKIT_HOST is set to point to a host list in the $INFRAKIT_HOME/hosts file. + host := os.Getenv("INFRAKIT_HOST") + if host == "" { + return ulist, nil // do nothing -- local mode + } + + // If the env is set but we don't have any hosts file locally, don't exit. + // Print a warning and proceed. + // Now look up the host lists in the file + hostsFile := filepath.Join(os.Getenv("INFRAKIT_HOME"), "hosts") + buff, err := ioutil.ReadFile(hostsFile) + if err != nil { + return ulist, nil // do nothing -- local mode + } + + m := map[string]string{} + yaml, err := types.AnyYAML(buff) + if err != nil { + return nil, fmt.Errorf("bad format for hosts file at %s for INFRAKIT_HOST=%s, err=%v", hostsFile, host, err) + } + err = yaml.Decode(&m) + if err != nil { + return nil, fmt.Errorf("cannot decode hosts file at %s for INFRAKIT_HOST=%s, err=%v", hostsFile, host, err) + } + + if list, has := m[host]; has { + hosts = strings.Split(list, ",") + } else { + return nil, fmt.Errorf("no entry in hosts file at %s for INFRAKIT_HOST=%s", hostsFile, host) + } + + for _, h := range hosts { + addProtocol := false + if !strings.Contains(h, "://") { + h = "http://" + h + addProtocol = true + } + u, err := url.Parse(h) + if err != nil { + panic(err) + } + if addProtocol { + u.Scheme = "http" + } + ulist = append(ulist, u) + } + + return ulist, nil } const ( diff --git a/cmd/infrakit/metadata/metadata.go b/cmd/infrakit/metadata/metadata.go index 9bc5e3947..3fc6b6508 100644 --- a/cmd/infrakit/metadata/metadata.go +++ b/cmd/infrakit/metadata/metadata.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/docker/infrakit/cmd/infrakit/base" + "github.com/docker/infrakit/pkg/cli" "github.com/docker/infrakit/pkg/discovery" logutil "github.com/docker/infrakit/pkg/log" "github.com/docker/infrakit/pkg/rpc/client" @@ -207,7 +208,7 @@ func Command(plugins func() discovery.Plugins) *cobra.Command { return nil } - catFlags, catOutput := base.Output() + catFlags, catOutput := cli.Output() cat := &cobra.Command{ Use: "cat", Short: "Get metadata entry by path", @@ -231,18 +232,21 @@ func Command(plugins func() discovery.Plugins) *cobra.Command { 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 - } - - catOutput(os.Stdout, str) - } - } else { + if err != nil { log.Warn("Cannot metadata cat on plugin", "target", *first, "err", err) + continue + } + if value == nil { + log.Warn("value is nil") + continue } + + str := value.String() + if s, err := strconv.Unquote(value.String()); err == nil { + str = s + } + + catOutput(os.Stdout, str, nil) } } } diff --git a/cmd/infrakit/playbook/playbook.go b/cmd/infrakit/playbook/playbook.go index d39263105..c177695c1 100644 --- a/cmd/infrakit/playbook/playbook.go +++ b/cmd/infrakit/playbook/playbook.go @@ -2,6 +2,7 @@ package playbook import ( "fmt" + "io" "io/ioutil" "os" "os/user" @@ -151,7 +152,7 @@ func Command(plugins func() discovery.Plugins) *cobra.Command { }, } - rawOutputFlags, rawOutput := base.RawOutput() + rawOutputFlags, rawOutput := cli.Output() list := &cobra.Command{ Use: "ls", Short: "List playbooks", @@ -164,25 +165,20 @@ func Command(plugins func() discovery.Plugins) *cobra.Command { modules, err := loadPlaybooks() if err != nil { - fmt.Println("***") return err } - rendered, err := rawOutput(os.Stdout, modules) - if err != nil { - return err - } - if rendered { - return nil - } - if !*quiet { - fmt.Printf("%-30s\t%-30s\n", "PLAYBOOK", "URL") - } + return rawOutput(os.Stdout, modules, + func(io.Writer, interface{}) error { + if !*quiet { + fmt.Printf("%-30s\t%-30s\n", "PLAYBOOK", "URL") + } - for op, url := range modules { - fmt.Printf("%-30v\t%-30v\n", op, url) - } - return nil + for op, url := range modules { + fmt.Printf("%-30v\t%-30v\n", op, url) + } + return nil + }) }, } list.Flags().AddFlagSet(rawOutputFlags) diff --git a/pkg/cli/context.go b/pkg/cli/context.go index 923a2ef2d..22857ec79 100644 --- a/pkg/cli/context.go +++ b/pkg/cli/context.go @@ -578,7 +578,7 @@ func (c *Context) BuildFlags() error { return err } - _, err = ConfigureTemplate(t, c.plugins).Render(c) + _, err = configureTemplate(t, c.plugins).Render(c) return err } @@ -598,7 +598,7 @@ func (c *Context) Execute() error { c.exec = true c.template = t - script, err := ConfigureTemplate(t, c.plugins).Render(c) + script, err := configureTemplate(t, c.plugins).Render(c) if err != nil { return err } diff --git a/pkg/cli/output.go b/pkg/cli/output.go new file mode 100644 index 000000000..9a0411e54 --- /dev/null +++ b/pkg/cli/output.go @@ -0,0 +1,66 @@ +package cli + +import ( + "fmt" + "io" + + "github.com/docker/infrakit/pkg/types" + "github.com/ghodss/yaml" + "github.com/spf13/pflag" +) + +// OutputFunc is a function that writes some data to the output writer +type OutputFunc func(w io.Writer, v interface{}, defaultView func(io.Writer, interface{}) error) (err error) + +// Output returns the flagset and the func for printing output +func Output() (*pflag.FlagSet, OutputFunc) { + + fs := pflag.NewFlagSet("output", pflag.ExitOnError) + raw := fs.BoolP("raw", "r", false, "True to dump raw output") + + yamlDoc := fs.BoolP("yaml", "y", false, "True to output yaml; json is the default") + return fs, func(w io.Writer, v interface{}, defaultView func(io.Writer, interface{}) error) (err error) { + + if !*raw && defaultView != nil { + return defaultView(w, v) + } + + var out string + + switch v := v.(type) { + case string: + if *yamlDoc { + if y, err := yaml.JSONToYAML([]byte(v)); err == nil { + out = string(y) + } + } else { + out = v + } + case []byte: + if *yamlDoc { + if y, err := yaml.JSONToYAML(v); err == nil { + out = string(y) + } + } else { + out = string(v) + } + default: + any, err := types.AnyValue(v) + if err != nil { + return err + } + + buff := any.Bytes() + if *yamlDoc { + if y, err := yaml.JSONToYAML(buff); err == nil { + out = string(y) + } + } else { + out = any.String() + } + } + + fmt.Fprintln(w, out) + return nil + } +} diff --git a/pkg/cli/registry.go b/pkg/cli/registry.go new file mode 100644 index 000000000..84cf7b636 --- /dev/null +++ b/pkg/cli/registry.go @@ -0,0 +1,137 @@ +package cli + +import ( + "fmt" + "path" + "sync" + + "github.com/docker/infrakit/pkg/rpc" + "github.com/docker/infrakit/pkg/rpc/client" + "github.com/docker/infrakit/pkg/spi" + "github.com/spf13/cobra" +) + +// CmdBuilder is a factory function that creates a command +type CmdBuilder func(name string, services *Services) *cobra.Command + +var ( + lock sync.Mutex + + cmdBuilders = map[string][]CmdBuilder{} +) + +// Register registers a command from the CmdBuilders +func Register(spi spi.InterfaceSpec, builders []CmdBuilder) { + + lock.Lock() + defer lock.Unlock() + + list, has := cmdBuilders[spi.Encode()] + if !has { + list = []CmdBuilder{} + } + cmdBuilders[spi.Encode()] = append(list, builders...) +} + +// visitCommands iterate through all the CmdBuilders known +func visitCommands(spi spi.InterfaceSpec, visit func(b CmdBuilder)) { + if builders, has := cmdBuilders[spi.Encode()]; has { + for _, builder := range builders { + visit(builder) + } + } +} + +func getPluginObjects(hs rpc.Handshaker, major string) map[string][]spi.InterfaceSpec { + + objects := map[string][]spi.InterfaceSpec{} + + // The spi this object implements (e.g. Instance/0.5.0) + spis, err := hs.Implements() + if err != nil { + log.Warn("error getting interface", "name", major, "err", err) + return objects + } + + // For each spi, eg. Instance/0.5.0 a list of object names + typesBySpi, err := hs.Types() + if err != nil { + log.Warn("error getting typed objects in this plugin", "name", major, "err", err) + + // Here we assume there are no lower level objects + objects[major] = spis + return objects + } + + for encodedSpi, names := range typesBySpi { + + // the key is a string form of InterfaceSpec because yaml/ json don't handle + // objects as keys very well. + + theSpi := spi.DecodeInterfaceSpec(string(encodedSpi)) + + objectName := major + for _, minor := range names { + + if minor != "." { + objectName = path.Join(major, minor) + } + + if list, has := objects[objectName]; !has { + objects[objectName] = []spi.InterfaceSpec{ + theSpi, + } + } else { + objects[objectName] = append(list, theSpi) + } + } + } + + return objects +} + +// LoadAll loads all the dynamic, plugin commands based on what's registered and discovered. +func LoadAll(services *Services) ([]*cobra.Command, error) { + lock.Lock() + defer lock.Unlock() + + // first discovery all the running plugins + running, err := services.Plugins().List() + if err != nil { + return nil, err + } + + commands := []*cobra.Command{} + + // Show the interfaces implemented by each plugin + for major, entry := range running { + hs, err := client.NewHandshaker(entry.Address) + if err != nil { + log.Warn("handshaker error", "err", err, "addr", entry.Address) + continue + } + + objects := getPluginObjects(hs, major) + + // for each object, we have a name and a list of interfaces. + for name, spis := range objects { + + command := &cobra.Command{ + Use: name, + Short: fmt.Sprintf("Access the %s instance plugin", name), + } + + for _, spi := range spis { + visitCommands(spi, func(buildCmd CmdBuilder) { + + subcommand := buildCmd(name, services) + command.AddCommand(subcommand) + }) + } + + commands = append(commands, command) + } + } + + return commands, nil +} diff --git a/pkg/cli/services.go b/pkg/cli/services.go new file mode 100644 index 000000000..4320d7825 --- /dev/null +++ b/pkg/cli/services.go @@ -0,0 +1,193 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "github.com/docker/infrakit/pkg/discovery" + "github.com/docker/infrakit/pkg/template" + "github.com/ghodss/yaml" + "github.com/spf13/pflag" +) + +// Services is a common set of utiities that are available to each command. +// For example, plugin lookup, metadata lookup, template engine +type Services struct { + + // Plugins provide a lookup for plugins + Plugins func() discovery.Plugins + + // ProcessTemplateFlags are common flags associated with the base services. They should be added to subcommands + // if the subcommands make use of the services provided here. + ProcessTemplateFlags *pflag.FlagSet + + // ProcessTemplate is the function that processes the template at url and returns view or error. + ProcessTemplate ProcessTemplateFunc + + // ToJSON converts the input buffer to json format + ToJSON ToJSONFunc + + // FromJSON converts json formatted input to output buffer + FromJSON FromJSONFunc + + // OutputFlags are flags that control output format + OutputFlags *pflag.FlagSet + + // Output is the function that does output + Output OutputFunc +} + +// NewServices creates an instance of common services for all commands +func NewServices(plugins func() discovery.Plugins) *Services { + flags, toJSON, fromJSON, processTemplate := templateProcessor(plugins) + outputFlags, outputFunc := Output() + return &Services{ + Plugins: plugins, + ProcessTemplateFlags: flags, + ProcessTemplate: processTemplate, + ToJSON: toJSON, + FromJSON: fromJSON, + OutputFlags: outputFlags, + Output: outputFunc, + } +} + +// ProcessTemplateFunc is the function that processes the template at url and returns view or error. +type ProcessTemplateFunc func(url string) (rendered string, err error) + +// ToJSONFunc converts the input buffer to json format +type ToJSONFunc func(in []byte) (json []byte, err error) + +// FromJSONFunc converts json formatted input to output buffer +type FromJSONFunc func(json []byte) (out []byte, err error) + +// ReadFromStdinIfElse checks condition and reads from stdin if true; otherwise it executes other. +func (s *Services) ReadFromStdinIfElse(condition func() bool, + otherwise func() (string, error), + toJSON ToJSONFunc) (rendered string, err error) { + + if condition() { + buff, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return "", err + } + json, err := toJSON(buff) + log.Debug("stdin", "buffer", string(json)) + return string(json), err + } + rendered, err = otherwise() + if err != nil { + return + } + var buff []byte + buff, err = toJSON([]byte(rendered)) + if err != nil { + return + } + return string(buff), nil +} + +// templateProcessor returns a flagset and a function for processing template input. +func templateProcessor(plugins func() discovery.Plugins) (*pflag.FlagSet, ToJSONFunc, FromJSONFunc, ProcessTemplateFunc) { + + fs := pflag.NewFlagSet("template", pflag.ExitOnError) + + globals := fs.StringSliceP("var", "v", []string{}, "key=value pairs of globally scoped variagbles") + yamlDoc := fs.BoolP("yaml", "y", false, "True if input is in yaml format; json is the default") + dump := fs.BoolP("dump", "x", false, "True to dump to output instead of executing") + singlePass := fs.BoolP("final", "f", false, "True to render template as the final pass") + + return fs, + // ToJSONFunc + func(in []byte) (json []byte, err error) { + + defer func() { + + if *dump { + fmt.Println("Raw:") + fmt.Println(string(in)) + fmt.Println("Converted") + fmt.Println(string(json)) + os.Exit(0) // special for debugging + } + }() + + if *yamlDoc { + json, err = yaml.YAMLToJSON(in) + return + } + json = in + return + + }, + // FromJSONFunc + func(json []byte) (out []byte, err error) { + + defer func() { + + if *dump { + fmt.Println("Raw:") + fmt.Println(string(json)) + fmt.Println("Converted") + fmt.Println(string(out)) + os.Exit(0) // special for debugging + } + }() + + if *yamlDoc { + out, err = yaml.JSONToYAML(json) + return + } + out = json + return + + }, + // ProcessTemplateFunc + func(url string) (view string, err error) { + + if !strings.Contains(url, "://") { + p := url + if dir, err := os.Getwd(); err == nil { + p = path.Join(dir, url) + } + url = "file://" + p + } + + log.Debug("reading template", "url", url) + engine, err := template.NewTemplate(url, template.Options{MultiPass: !*singlePass}) + if err != nil { + return + } + + for _, global := range *globals { + kv := strings.SplitN(global, "=", 2) + if len(kv) != 2 { + log.Warn("bad format kv", "input", global) + continue + } + key := strings.TrimSpace(kv[0]) + val := strings.TrimSpace(kv[1]) + if key != "" && val != "" { + engine.Global(key, val) + } + } + + configureTemplate(engine, plugins) + + view, err = engine.Render(nil) + if err != nil { + return + } + + log.Debug("rendered", "view", view) + if *dump { + fmt.Println("Final:") + fmt.Println(string(view)) + os.Exit(0) + } + return + } +} diff --git a/pkg/cli/template.go b/pkg/cli/template.go index b039b290b..d40a80829 100644 --- a/pkg/cli/template.go +++ b/pkg/cli/template.go @@ -8,8 +8,7 @@ import ( "github.com/docker/infrakit/pkg/template" ) -// ConfigureTemplate is a utility that helps setup template engines in a standardized way across all uses. -func ConfigureTemplate(engine *template.Template, plugins func() discovery.Plugins) *template.Template { +func configureTemplate(engine *template.Template, plugins func() discovery.Plugins) *template.Template { engine.WithFunctions(func() []template.Function { return []template.Function{ { diff --git a/pkg/cli/v1/flavor/cmd.go b/pkg/cli/v1/flavor/cmd.go new file mode 100644 index 000000000..dc0d235af --- /dev/null +++ b/pkg/cli/v1/flavor/cmd.go @@ -0,0 +1,30 @@ +package flavor + +import ( + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/discovery" + logutil "github.com/docker/infrakit/pkg/log" + "github.com/docker/infrakit/pkg/plugin" + flavor_plugin "github.com/docker/infrakit/pkg/rpc/flavor" + "github.com/docker/infrakit/pkg/spi/flavor" +) + +var log = logutil.New("module", "cli/v1/flavor") + +func init() { + cli.Register(flavor.InterfaceSpec, + []cli.CmdBuilder{ + Validate, + Prepare, + Healthy, + }) +} + +// LoadPlugin loads the typed plugin +func LoadPlugin(plugins discovery.Plugins, name string) (flavor.Plugin, error) { + endpoint, err := plugins.Find(plugin.Name(name)) + if err != nil { + return nil, err + } + return flavor_plugin.NewClient(plugin.Name(name), endpoint.Address) +} diff --git a/pkg/cli/v1/flavor/healthy.go b/pkg/cli/v1/flavor/healthy.go new file mode 100644 index 000000000..4e7080c0f --- /dev/null +++ b/pkg/cli/v1/flavor/healthy.go @@ -0,0 +1,81 @@ +package flavor + +import ( + "fmt" + "os" + "strings" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/types" + "github.com/spf13/cobra" +) + +// Healthy returns the healthy command +func Healthy(name string, services *cli.Services) *cobra.Command { + + flavorPropertiesURL := "" + + healthy := &cobra.Command{ + Use: "healthy id...", + Short: "Healthy checks the healthy of the instances by ids given", + } + healthy.Flags().String("properties", "", "Properties of the flavor plugin, a url") + + tags := healthy.Flags().StringSlice("tags", []string{}, "Tags to filter") + asLogicalID := false + healthy.Flags().BoolVarP(&asLogicalID, "logical-id", "l", asLogicalID, "Args are logical IDs") + healthy.Flags().AddFlagSet(services.ProcessTemplateFlags) + + healthy.RunE = func(cmd *cobra.Command, args []string) error { + + if len(args) == 0 { + cmd.Usage() + os.Exit(1) + } + + flavorPlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(flavorPlugin, "instance plugin not found", "name", name) + + flavorProperties, err := services.ProcessTemplate(flavorPropertiesURL) + if err != nil { + return err + } + + filter := map[string]string{} + for _, t := range *tags { + p := strings.Split(t, "=") + if len(p) == 2 { + filter[p[0]] = p[1] + } else { + filter[p[0]] = "" + } + } + + for _, arg := range args { + + desc := instance.Description{} + if len(filter) > 0 { + desc.Tags = filter + } + + if asLogicalID { + logical := instance.LogicalID(arg) + desc.LogicalID = &logical + } else { + desc.ID = instance.ID(arg) + } + + healthy, err := flavorPlugin.Healthy(types.AnyString(flavorProperties), desc) + if err == nil { + fmt.Printf("%v\n", healthy) + } + } + + return err + } + return healthy +} diff --git a/pkg/cli/v1/flavor/prepare.go b/pkg/cli/v1/flavor/prepare.go new file mode 100644 index 000000000..0c73e5c8f --- /dev/null +++ b/pkg/cli/v1/flavor/prepare.go @@ -0,0 +1,105 @@ +package flavor + +import ( + "os" + + "github.com/docker/infrakit/pkg/cli" + group_types "github.com/docker/infrakit/pkg/plugin/group/types" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/types" + "github.com/spf13/cobra" +) + +// Prepare returns the prepare command +func Prepare(name string, services *cli.Services) *cobra.Command { + logicalIDs := []string{} + groupSize := uint(0) + groupID := "" + groupSequence := uint(0) + flavorPropertiesURL := "" + + prepare := &cobra.Command{ + Use: "prepare ", + Short: "Prepare provisioning inputs for an instance. Read from stdin if url is '-'", + } + + prepare.RunE = func(cmd *cobra.Command, args []string) error { + + if len(args) != 0 { + cmd.Usage() + os.Exit(1) + } + + flavorPlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(flavorPlugin, "flavor plugin not found", "name", name) + + if len(args) != 1 { + cmd.Usage() + os.Exit(1) + } + + flavorProperties, err := services.ProcessTemplate(flavorPropertiesURL) + if err != nil { + return err + } + + view, err := services.ReadFromStdinIfElse( + func() bool { return args[0] == "-" }, + func() (string, error) { return services.ProcessTemplate(args[0]) }, + services.ToJSON, + ) + if err != nil { + return err + } + + spec := instance.Spec{} + if err := types.AnyString(view).Decode(&spec); err != nil { + return err + } + + spec, err = flavorPlugin.Prepare( + types.AnyString(flavorProperties), + spec, + allocationMethodFromFlags(&logicalIDs, &groupSize), + indexFromFlags(&groupID, &groupSequence), + ) + if err != nil { + return err + } + + return services.Output(os.Stdout, spec, nil) + } + + prepare.Flags().String("properties", "", "Properties of the flavor plugin, a url") + prepare.Flags().StringSliceVar( + &logicalIDs, + "logical-ids", + []string{}, + "Logical IDs to use as the Allocation method") + prepare.Flags().UintVar( + &groupSize, + "size", + 0, + "Group Size to use as the Allocation method") + prepare.Flags().AddFlagSet(services.ProcessTemplateFlags) + + prepare.Flags().StringVar( + &groupID, + "index-group", + "", + "ID of the group") + prepare.Flags().UintVar( + &groupSequence, + "index-sequence", + 0, + "Sequence number within the group") + return prepare +} + +func indexFromFlags(groupID *string, groupSequence *uint) group_types.Index { + return group_types.Index{Group: group.ID(*groupID), Sequence: *groupSequence} +} diff --git a/pkg/cli/v1/flavor/validate.go b/pkg/cli/v1/flavor/validate.go new file mode 100644 index 000000000..abc3c38c1 --- /dev/null +++ b/pkg/cli/v1/flavor/validate.go @@ -0,0 +1,77 @@ +package flavor + +import ( + "fmt" + "os" + + "github.com/docker/infrakit/pkg/cli" + group_types "github.com/docker/infrakit/pkg/plugin/group/types" + "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/types" + "github.com/spf13/cobra" +) + +// Validate returns the validate command +func Validate(name string, services *cli.Services) *cobra.Command { + logicalIDs := []string{} + groupSize := uint(0) + + validate := &cobra.Command{ + Use: "validate", + Short: "Validate a flavor configuration", + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) != 0 { + cmd.Usage() + os.Exit(1) + } + + flavorPlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(flavorPlugin, "instance plugin not found", "name", name) + + view, err := services.ReadFromStdinIfElse( + func() bool { return args[0] == "-" }, + func() (string, error) { return services.ProcessTemplate(args[0]) }, + services.ToJSON, + ) + if err != nil { + return err + } + + err = flavorPlugin.Validate(types.AnyString(view), allocationMethodFromFlags(&logicalIDs, &groupSize)) + if err == nil { + fmt.Println("validate:ok") + } + return err + }, + } + + validate.Flags().StringSliceVar( + &logicalIDs, + "logical-ids", + []string{}, + "Logical IDs to use as the Allocation method") + validate.Flags().UintVar( + &groupSize, + "size", + 0, + "Group Size to use as the Allocation method") + validate.Flags().AddFlagSet(services.ProcessTemplateFlags) + + return validate +} + +func allocationMethodFromFlags(logicalIDs *[]string, groupSize *uint) group_types.AllocationMethod { + ids := []instance.LogicalID{} + for _, id := range *logicalIDs { + ids = append(ids, instance.LogicalID(id)) + } + + return group_types.AllocationMethod{ + Size: *groupSize, + LogicalIDs: ids, + } +} diff --git a/pkg/cli/v1/group/cmd.go b/pkg/cli/v1/group/cmd.go new file mode 100644 index 000000000..a30706259 --- /dev/null +++ b/pkg/cli/v1/group/cmd.go @@ -0,0 +1,33 @@ +package group + +import ( + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/discovery" + logutil "github.com/docker/infrakit/pkg/log" + "github.com/docker/infrakit/pkg/plugin" + group_rpc "github.com/docker/infrakit/pkg/rpc/group" + "github.com/docker/infrakit/pkg/spi/group" +) + +var log = logutil.New("module", "cli/v1/group") + +func init() { + cli.Register(group.InterfaceSpec, + []cli.CmdBuilder{ + Ls, + Inspect, + Describe, + Commit, + Free, + Destroy, + }) +} + +// LoadPlugin loads the typed plugin +func LoadPlugin(plugins discovery.Plugins, name string) (group.Plugin, error) { + endpoint, err := plugins.Find(plugin.Name(name)) + if err != nil { + return nil, err + } + return group_rpc.NewClient(endpoint.Address) +} diff --git a/pkg/cli/v1/group/commit.go b/pkg/cli/v1/group/commit.go new file mode 100644 index 000000000..d319de564 --- /dev/null +++ b/pkg/cli/v1/group/commit.go @@ -0,0 +1,65 @@ +package group + +import ( + "fmt" + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/docker/infrakit/pkg/types" + "github.com/spf13/cobra" +) + +// Commit returns the Commit command +func Commit(name string, services *cli.Services) *cobra.Command { + + commit := &cobra.Command{ + Use: "commit ", + Short: "Commit a group configuration. Read from stdin if url is '-'", + } + + pretend := false + commit.Flags().BoolVarP(&pretend, "pretend", "p", pretend, "Don't actually commit, only explain where appropriate") + + commit.RunE = func(cmd *cobra.Command, args []string) error { + + if len(args) != 1 { + cmd.Usage() + os.Exit(1) + } + + groupPlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(groupPlugin, "group plugin not found", "name", name) + + view, err := services.ReadFromStdinIfElse( + func() bool { return args[0] == "-" }, + func() (string, error) { return services.ProcessTemplate(args[0]) }, + services.ToJSON, + ) + if err != nil { + return err + } + + spec := group.Spec{} + if err := types.AnyString(view).Decode(&spec); err != nil { + return err + } + + details, err := groupPlugin.CommitGroup(spec, pretend) + if err != nil { + return err + } + + if pretend { + fmt.Printf("Committing %s would involve: %s\n", spec.ID, details) + } else { + fmt.Printf("Committed %s: %s\n", spec.ID, details) + } + return nil + } + commit.Flags().AddFlagSet(services.ProcessTemplateFlags) + return commit +} diff --git a/pkg/cli/v1/group/describe.go b/pkg/cli/v1/group/describe.go new file mode 100644 index 000000000..4c618787b --- /dev/null +++ b/pkg/cli/v1/group/describe.go @@ -0,0 +1,67 @@ +package group + +import ( + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/spf13/cobra" +) + +// Describe returns the Describe command +func Describe(name string, services *cli.Services) *cobra.Command { + + describe := &cobra.Command{ + Use: "describe ", + Short: "Describe a group. Returns a list of members", + } + + quiet := describe.Flags().BoolP("quiet", "q", false, "Print rows without column headers") + describe.RunE = func(cmd *cobra.Command, args []string) error { + + if len(args) != 1 { + cmd.Usage() + os.Exit(1) + } + + groupPlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(groupPlugin, "group plugin not found", "name", name) + + groupID := group.ID(args[0]) + desc, err := groupPlugin.DescribeGroup(groupID) + if err != nil { + return err + } + + return services.Output(os.Stdout, desc, + func(io.Writer, interface{}) error { + if !*quiet { + fmt.Printf("%-30s\t%-30s\t%-s\n", "ID", "LOGICAL", "TAGS") + } + for _, d := range desc.Instances { + logical := " - " + if d.LogicalID != nil { + logical = string(*d.LogicalID) + } + + printTags := []string{} + for k, v := range d.Tags { + printTags = append(printTags, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(printTags) + + fmt.Printf("%-30s\t%-30s\t%-s\n", d.ID, logical, strings.Join(printTags, ",")) + } + return nil + }) + } + describe.Flags().AddFlagSet(services.OutputFlags) + return describe +} diff --git a/pkg/cli/v1/group/destroy.go b/pkg/cli/v1/group/destroy.go new file mode 100644 index 000000000..93c18aada --- /dev/null +++ b/pkg/cli/v1/group/destroy.go @@ -0,0 +1,42 @@ +package group + +import ( + "fmt" + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/spf13/cobra" +) + +// Destroy returns the Destroy command +func Destroy(name string, services *cli.Services) *cobra.Command { + + destroy := &cobra.Command{ + Use: "destroy ", + Short: "Destroy a group by terminating and removing all members from infrastructure", + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) != 1 { + cmd.Usage() + os.Exit(1) + } + + groupPlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(groupPlugin, "group plugin not found", "name", name) + + groupID := group.ID(args[0]) + err = groupPlugin.DestroyGroup(groupID) + if err != nil { + return err + } + + fmt.Println("Destroy", groupID, "initiated") + return nil + }, + } + return destroy +} diff --git a/pkg/cli/v1/group/free.go b/pkg/cli/v1/group/free.go new file mode 100644 index 000000000..14e814452 --- /dev/null +++ b/pkg/cli/v1/group/free.go @@ -0,0 +1,43 @@ +package group + +import ( + "fmt" + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/spf13/cobra" +) + +// Free returns the Free command +func Free(name string, services *cli.Services) *cobra.Command { + + free := &cobra.Command{ + Use: "free ", + Short: "Free a group nonedestructively from active monitoring", + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) != 1 { + cmd.Usage() + os.Exit(1) + } + + groupPlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(groupPlugin, "group plugin not found", "name", name) + + groupID := group.ID(args[0]) + err = groupPlugin.FreeGroup(groupID) + if err != nil { + return err + } + + fmt.Println("Freed", groupID) + return nil + }, + } + free.Flags().AddFlagSet(services.OutputFlags) + return free +} diff --git a/pkg/cli/v1/group/inspect.go b/pkg/cli/v1/group/inspect.go new file mode 100644 index 000000000..129cbedaf --- /dev/null +++ b/pkg/cli/v1/group/inspect.go @@ -0,0 +1,49 @@ +package group + +import ( + "fmt" + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/spf13/cobra" +) + +// Inspect returns the Inspect command +func Inspect(name string, services *cli.Services) *cobra.Command { + + inspect := &cobra.Command{ + Use: "inspect ", + Short: "Insepct a group. Returns the raw configuration associated with a group", + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) != 1 { + cmd.Usage() + os.Exit(1) + } + + groupPlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(groupPlugin, "group plugin not found", "name", name) + + groupID := group.ID(args[0]) + specs, err := groupPlugin.InspectGroups() + + if err == nil { + + for _, spec := range specs { + if spec.ID == groupID { + return services.Output(os.Stdout, spec, nil) + } + } + + return fmt.Errorf("Group %s is not being watched", groupID) + } + return err + }, + } + inspect.Flags().AddFlagSet(services.OutputFlags) + return inspect +} diff --git a/pkg/cli/v1/group/ls.go b/pkg/cli/v1/group/ls.go new file mode 100644 index 000000000..1f6f9d19c --- /dev/null +++ b/pkg/cli/v1/group/ls.go @@ -0,0 +1,41 @@ +package group + +import ( + "fmt" + + "github.com/docker/infrakit/pkg/cli" + "github.com/spf13/cobra" +) + +// Ls returns the Ls command +func Ls(name string, services *cli.Services) *cobra.Command { + + ls := &cobra.Command{ + Use: "ls", + Short: "List groups", + } + + quiet := ls.Flags().BoolP("quiet", "q", false, "Print rows without column headers") + + ls.RunE = func(cmd *cobra.Command, args []string) error { + + groupPlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(groupPlugin, "group plugin not found", "name", name) + + groups, err := groupPlugin.InspectGroups() + if err == nil { + if !*quiet { + fmt.Printf("%s\n", "ID") + } + for _, g := range groups { + fmt.Printf("%s\n", g.ID) + } + } + + return err + } + return ls +} diff --git a/pkg/cli/v1/info.go b/pkg/cli/v1/info.go new file mode 100644 index 000000000..029e8e01c --- /dev/null +++ b/pkg/cli/v1/info.go @@ -0,0 +1,175 @@ +package v1 + +import ( + "encoding/json" + "fmt" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/plugin" + "github.com/docker/infrakit/pkg/rpc/client" + "github.com/docker/infrakit/pkg/spi/flavor" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/template" + "github.com/spf13/cobra" + + // v1 loads these packages + _ "github.com/docker/infrakit/pkg/cli/v1/flavor" + _ "github.com/docker/infrakit/pkg/cli/v1/group" + _ "github.com/docker/infrakit/pkg/cli/v1/instance" +) + +func init() { + cli.Register(instance.InterfaceSpec, + []cli.CmdBuilder{ + Info, + }) + cli.Register(flavor.InterfaceSpec, + []cli.CmdBuilder{ + Info, + }) + cli.Register(group.InterfaceSpec, + []cli.CmdBuilder{ + Info, + }) +} + +// Info returns the info command +func Info(name string, services *cli.Services) *cobra.Command { + cmd := &cobra.Command{ + Use: "info", + Short: "print plugin info", + PersistentPreRunE: func(c *cobra.Command, args []string) error { + return cli.EnsurePersistentPreRunE(c) + }, + } + + raw := cmd.Flags().Bool("raw", false, "True to show raw data") + + api := &cobra.Command{ + Use: "api", + Short: "Show api / RPC interface supported by the plugin of the given name", + } + + templateFuncs := &cobra.Command{ + Use: "template", + Short: "Show template functions supported by the plugin, if the plugin uses template for configuration.", + } + cmd.AddCommand(api, templateFuncs) + + api.RunE = func(cmd *cobra.Command, args []string) error { + endpoint, err := services.Plugins().Find(plugin.Name(name)) + if err != nil { + return err + } + + infoClient, err := client.NewPluginInfoClient(endpoint.Address) + if err != nil { + return err + } + + info, err := infoClient.GetInfo() + if err != nil { + return err + } + + if *raw { + buff, err := json.MarshalIndent(info, "", " ") + if err != nil { + return err + } + + fmt.Println(string(buff)) + return nil + } + // render a view using template + renderer, err := template.NewTemplate("str://"+apiViewTemplate, template.Options{}) + if err != nil { + return err + } + + view, err := renderer.Global("plugin", name).Render(info) + if err != nil { + return err + } + + fmt.Print(view) + return nil + } + + templateFuncs.RunE = func(cmd *cobra.Command, args []string) error { + endpoint, err := services.Plugins().Find(plugin.Name(name)) + if err != nil { + return err + } + + infoClient, err := client.NewPluginInfoClient(endpoint.Address) + if err != nil { + return err + } + info, err := infoClient.GetFunctions() + if err != nil { + return err + } + + if *raw { + buff, err := json.MarshalIndent(info, "", " ") + if err != nil { + return err + } + + fmt.Println(string(buff)) + return nil + } + // render a view using template + renderer, err := template.NewTemplate("str://"+funcsViewTemplate, template.Options{}) + if err != nil { + return err + } + + view, err := renderer.Global("plugin", name).Render(info) + if err != nil { + return err + } + + fmt.Print(view) + return nil + } + + return cmd +} + +const ( + apiViewTemplate = ` +Plugin: {{var "plugin"}} +Implements: {{range $spi := .Implements}}{{$spi.Name}}/{{$spi.Version}} {{end}} +Interfaces: {{range $iface := .Interfaces}} + SPI: {{$iface.Name}}/{{$iface.Version}} + RPC: {{range $method := $iface.Methods}} + Method: {{$method.Request | q "method" }} + Request: + {{$method.Request | jsonEncode | yamlEncode }} + + Response: + {{$method.Response | jsonEncode | yamlEncode }} + + ------------------------- + {{end}} +{{end}} +` + + funcsViewTemplate = ` +{{range $category, $functions := .}} +{{var "plugin"}}/{{$category}} _________________________________________________________________________________________ + {{range $f := $functions}} + Name: {{$f.Name}} + Description: {{join "\n " $f.Description}} + Function: {{$f.Function}} + Usage: {{$f.Usage}} + + ------------------------- + {{end}} + +{{end}} +` +) diff --git a/pkg/cli/v1/instance/cmd.go b/pkg/cli/v1/instance/cmd.go new file mode 100644 index 000000000..9d13ec36f --- /dev/null +++ b/pkg/cli/v1/instance/cmd.go @@ -0,0 +1,31 @@ +package instance + +import ( + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/discovery" + logutil "github.com/docker/infrakit/pkg/log" + "github.com/docker/infrakit/pkg/plugin" + instance_plugin "github.com/docker/infrakit/pkg/rpc/instance" + "github.com/docker/infrakit/pkg/spi/instance" +) + +var log = logutil.New("module", "cli/v1/instance") + +func init() { + cli.Register(instance.InterfaceSpec, + []cli.CmdBuilder{ + Validate, + Provision, + Describe, + Destroy, + }) +} + +// LoadPlugin loads the typed plugin +func LoadPlugin(plugins discovery.Plugins, name string) (instance.Plugin, error) { + endpoint, err := plugins.Find(plugin.Name(name)) + if err != nil { + return nil, err + } + return instance_plugin.NewClient(plugin.Name(name), endpoint.Address) +} diff --git a/pkg/cli/v1/instance/describe.go b/pkg/cli/v1/instance/describe.go new file mode 100644 index 000000000..07407470e --- /dev/null +++ b/pkg/cli/v1/instance/describe.go @@ -0,0 +1,131 @@ +package instance + +import ( + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/template" + "github.com/docker/infrakit/pkg/types" + "github.com/spf13/cobra" +) + +// Describe returns the describe command +func Describe(name string, services *cli.Services) *cobra.Command { + describe := &cobra.Command{ + Use: "describe", + Short: "Describe all managed instances across all groups, subject to filter", + } + describe.Flags().AddFlagSet(services.OutputFlags) + + tags := describe.Flags().StringSlice("tags", []string{}, "Tags to filter") + properties := describe.Flags().BoolP("properties", "p", false, "Also returns current status/ properties") + tagsTemplate := describe.Flags().StringP("tags-view", "t", "*", "Template to render tags") + propertiesTemplate := describe.Flags().StringP("properties-view", "v", "{{.}}", "Template to render properties") + + quiet := describe.Flags().BoolP("quiet", "q", false, "Print rows without column headers") + + describe.RunE = func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + cmd.Usage() + os.Exit(1) + } + + instancePlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(instancePlugin, "instance plugin not found", "name", name) + + options := template.Options{} + tagsView, err := template.NewTemplate(template.ValidURL(*tagsTemplate), options) + if err != nil { + return err + } + propertiesView, err := template.NewTemplate(template.ValidURL(*propertiesTemplate), options) + if err != nil { + return err + } + + filter := map[string]string{} + for _, t := range *tags { + p := strings.Split(t, "=") + if len(p) == 2 { + filter[p[0]] = p[1] + } else { + filter[p[0]] = "" + } + } + + desc, err := instancePlugin.DescribeInstances(filter, *properties) + if err != nil { + return err + } + + return services.Output(os.Stdout, desc, + func(w io.Writer, v interface{}) error { + if !*quiet { + if *properties { + fmt.Printf("%-30s\t%-30s\t%-30s\t%-s\n", "ID", "LOGICAL", "TAGS", "PROPERTIES") + + } else { + fmt.Printf("%-30s\t%-30s\t%-s\n", "ID", "LOGICAL", "TAGS") + } + } + for _, d := range desc { + + logical := " - " + if d.LogicalID != nil { + logical = string(*d.LogicalID) + } + + tagViewBuff := "" + if *tagsTemplate == "*" { + // default -- this is a hack + printTags := []string{} + for k, v := range d.Tags { + printTags = append(printTags, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(printTags) + tagViewBuff = strings.Join(printTags, ",") + } else { + tagViewBuff = renderTags(d.Tags, tagsView) + } + + if *properties { + fmt.Printf("%-30s\t%-30s\t%-30s\t%-s\n", d.ID, logical, tagViewBuff, + renderProperties(d.Properties, propertiesView)) + } else { + fmt.Printf("%-30s\t%-30s\t%-s\n", d.ID, logical, tagViewBuff) + } + } + return nil + }) + } + return describe +} + +func renderTags(m map[string]string, view *template.Template) string { + buff, err := view.Render(m) + if err != nil { + return err.Error() + } + return buff +} + +func renderProperties(properties *types.Any, view *template.Template) string { + var v interface{} + err := properties.Decode(&v) + if err != nil { + return err.Error() + } + + buff, err := view.Render(v) + if err != nil { + return err.Error() + } + return buff +} diff --git a/pkg/cli/v1/instance/destroy.go b/pkg/cli/v1/instance/destroy.go new file mode 100644 index 000000000..ad4290e1d --- /dev/null +++ b/pkg/cli/v1/instance/destroy.go @@ -0,0 +1,45 @@ +package instance + +import ( + "fmt" + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/instance" + "github.com/spf13/cobra" +) + +// Destroy returns the destroy command +func Destroy(name string, services *cli.Services) *cobra.Command { + + destroy := &cobra.Command{ + Use: "destroy ...", + Short: "Destroy the instance", + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) < 1 { + cmd.Usage() + os.Exit(1) + } + + instancePlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(instancePlugin, "instance plugin not found", "name", name) + + for _, a := range args { + + instanceID := instance.ID(a) + err := instancePlugin.Destroy(instanceID) + + if err != nil { + return err + } + fmt.Println("destroyed", instanceID) + } + return nil + }, + } + return destroy +} diff --git a/pkg/cli/v1/instance/provision.go b/pkg/cli/v1/instance/provision.go new file mode 100644 index 000000000..bf450f2ff --- /dev/null +++ b/pkg/cli/v1/instance/provision.go @@ -0,0 +1,55 @@ +package instance + +import ( + "fmt" + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/types" + "github.com/spf13/cobra" +) + +// Provision returns the provision command +func Provision(name string, services *cli.Services) *cobra.Command { + + cmd := &cobra.Command{ + Use: "provision ", + Short: "Provisions an instance. Read from stdin if url is '-'", + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) != 1 { + cmd.Usage() + os.Exit(1) + } + + instancePlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(instancePlugin, "instance plugin not found", "name", name) + + view, err := services.ReadFromStdinIfElse( + func() bool { return args[0] == "-" }, + func() (string, error) { return services.ProcessTemplate(args[0]) }, + services.ToJSON, + ) + if err != nil { + return err + } + + spec := instance.Spec{} + if err := types.AnyString(view).Decode(&spec); err != nil { + return err + } + + id, err := instancePlugin.Provision(spec) + if err == nil && id != nil { + fmt.Printf("%s\n", *id) + } + return err + }, + } + cmd.Flags().AddFlagSet(services.ProcessTemplateFlags) + return cmd +} diff --git a/pkg/cli/v1/instance/validate.go b/pkg/cli/v1/instance/validate.go new file mode 100644 index 000000000..f3bf28f14 --- /dev/null +++ b/pkg/cli/v1/instance/validate.go @@ -0,0 +1,49 @@ +package instance + +import ( + "fmt" + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/types" + "github.com/spf13/cobra" +) + +// Validate returns the validate command +func Validate(name string, services *cli.Services) *cobra.Command { + + cmd := &cobra.Command{ + Use: "validate ", + Short: "Validates an flavor config. Read from stdin if url is '-'", + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) != 1 { + cmd.Usage() + os.Exit(1) + } + + instancePlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(instancePlugin, "instance plugin not found", "name", name) + + view, err := services.ReadFromStdinIfElse( + func() bool { return args[0] == "-" }, + func() (string, error) { return services.ProcessTemplate(args[0]) }, + services.ToJSON, + ) + if err != nil { + return err + } + + err = instancePlugin.Validate(types.AnyString(view)) + if err == nil { + fmt.Println("validate:ok") + } + return err + }, + } + cmd.Flags().AddFlagSet(services.ProcessTemplateFlags) + return cmd +} diff --git a/pkg/rpc/client/info.go b/pkg/rpc/client/info.go index 24a3ebadb..1e6415fa9 100644 --- a/pkg/rpc/client/info.go +++ b/pkg/rpc/client/info.go @@ -2,8 +2,8 @@ package client import ( "encoding/json" - "net" "net/http" + "net/url" "github.com/docker/infrakit/pkg/plugin" "github.com/docker/infrakit/pkg/rpc" @@ -11,22 +11,25 @@ import ( ) // NewPluginInfoClient returns a plugin informer that can give metadata about a plugin -func NewPluginInfoClient(socketPath string) *InfoClient { - dialUnix := func(proto, addr string) (conn net.Conn, err error) { - return net.Dial("unix", socketPath) +func NewPluginInfoClient(address string) (*InfoClient, error) { + u, httpC, err := parseAddress(address) + if err != nil { + return nil, err } - return &InfoClient{client: &http.Client{Transport: &http.Transport{Dial: dialUnix}}} + return &InfoClient{addr: address, client: httpC, url: u}, nil } // InfoClient is the client for retrieving plugin info type InfoClient struct { client *http.Client + addr string + url *url.URL } // GetInfo implements the Info interface and returns the metadata about the plugin func (i *InfoClient) GetInfo() (plugin.Info, error) { meta := plugin.Info{} - resp, err := i.client.Get("http://d" + rpc.URLAPI) + resp, err := i.client.Get(i.url.Scheme + "://" + i.url.Host + rpc.URLAPI) if err != nil { return meta, err } @@ -38,7 +41,7 @@ func (i *InfoClient) GetInfo() (plugin.Info, error) { // GetFunctions returns metadata about the plugin's template functions, if the plugin supports templating. func (i *InfoClient) GetFunctions() (map[string][]template.Function, error) { meta := map[string][]template.Function{} - resp, err := i.client.Get("http://d" + rpc.URLFunctions) + resp, err := i.client.Get(i.url.Scheme + "://" + i.url.Host + rpc.URLFunctions) if err != nil { return meta, err } diff --git a/pkg/rpc/handshake.go b/pkg/rpc/handshake.go index d5decc6b3..bc69de3ae 100644 --- a/pkg/rpc/handshake.go +++ b/pkg/rpc/handshake.go @@ -1,7 +1,6 @@ package rpc import ( - "fmt" "net/http" "github.com/docker/infrakit/pkg/spi" @@ -45,7 +44,7 @@ func (h Handshake) Implements(_ *http.Request, req *ImplementsRequest, resp *Imp func (h Handshake) Types(_ *http.Request, req *TypesRequest, resp *TypesResponse) error { m := map[InterfaceSpec][]string{} for k, v := range h { - m[InterfaceSpec(fmt.Sprintf("%s/%s", k.Name, k.Version))] = v + m[InterfaceSpec(k.Encode())] = v } resp.Types = m return nil diff --git a/pkg/spi/plugin.go b/pkg/spi/plugin.go index fb7f70350..350cfc759 100644 --- a/pkg/spi/plugin.go +++ b/pkg/spi/plugin.go @@ -1,6 +1,9 @@ package spi import ( + "fmt" + "strings" + "github.com/docker/infrakit/pkg/types" ) @@ -13,6 +16,20 @@ type InterfaceSpec struct { Version string } +// Encode encodes a struct form to string +func (i InterfaceSpec) Encode() string { + return fmt.Sprintf("%s/%s", i.Name, i.Version) +} + +// Decode takes a string and returns the struct +func DecodeInterfaceSpec(s string) InterfaceSpec { + p := strings.SplitN(s, "/", 2) + return InterfaceSpec{ + Name: p[0], + Version: p[1], + } +} + // VendorInfo provides vendor-specific information type VendorInfo struct { InterfaceSpec // vendor-defined name / version diff --git a/pkg/template/template.go b/pkg/template/template.go index 6531681cd..73c430946 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -97,6 +97,16 @@ type Void string const voidValue Void = "" +// ValidURL makes sure the input is of the URL form. If the input does not +// container :// then a str:// is prepended so that the input string is interpreted +// literally as the template itself. +func ValidURL(s string) string { + if strings.Index(s, "://") == -1 { + return "str://" + s + } + return s +} + // NewTemplate fetches the content at the url and returns a template. If the string begins // with str:// as scheme, then the rest of the string is interpreted as the body of the template. func NewTemplate(s string, opt Options) (*Template, error) { From c73afabe9c82fd8f55ce377e89eb551317013bf8 Mon Sep 17 00:00:00 2001 From: David Chung Date: Sun, 14 May 2017 13:38:00 -0700 Subject: [PATCH 02/12] fix broken merge Signed-off-by: David Chung --- cmd/infrakit/base/template.go | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 cmd/infrakit/base/template.go diff --git a/cmd/infrakit/base/template.go b/cmd/infrakit/base/template.go new file mode 100644 index 000000000..06616b02a --- /dev/null +++ b/cmd/infrakit/base/template.go @@ -0,0 +1,46 @@ +package base + +import ( + "io/ioutil" + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/discovery" + "github.com/spf13/pflag" +) + +// ReadFromStdinIfElse checks condition and reads from stdin if true; otherwise it executes other. +func ReadFromStdinIfElse( + condition func() bool, + otherwise func() (string, error), + toJSON cli.ToJSONFunc) (rendered string, err error) { + + if condition() { + buff, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return "", err + } + json, err := toJSON(buff) + log.Debug("stdin", "buffer", string(json)) + return string(json), err + } + rendered, err = otherwise() + if err != nil { + return + } + var buff []byte + buff, err = toJSON([]byte(rendered)) + if err != nil { + return + } + return string(buff), nil +} + +// TemplateProcessor returns a flagset and a function for processing template input. +func TemplateProcessor(plugins func() discovery.Plugins) (*pflag.FlagSet, cli.ToJSONFunc, cli.FromJSONFunc, cli.ProcessTemplateFunc) { + services := cli.NewServices(plugins) + return services.ProcessTemplateFlags, + services.ToJSON, + services.FromJSON, + services.ProcessTemplate +} From 18619108ed68b71a1c0511606190b1191fb95037 Mon Sep 17 00:00:00 2001 From: David Chung Date: Sun, 14 May 2017 13:52:01 -0700 Subject: [PATCH 03/12] fix lint Signed-off-by: David Chung --- pkg/spi/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/spi/plugin.go b/pkg/spi/plugin.go index 350cfc759..8c7647cdd 100644 --- a/pkg/spi/plugin.go +++ b/pkg/spi/plugin.go @@ -21,7 +21,7 @@ func (i InterfaceSpec) Encode() string { return fmt.Sprintf("%s/%s", i.Name, i.Version) } -// Decode takes a string and returns the struct +// DecodeInterfaceSpec takes a string and returns the struct func DecodeInterfaceSpec(s string) InterfaceSpec { p := strings.SplitN(s, "/", 2) return InterfaceSpec{ From 794b561ea589a00671b2b72c81e5b37e6d5b5583 Mon Sep 17 00:00:00 2001 From: David Chung Date: Sun, 14 May 2017 14:37:45 -0700 Subject: [PATCH 04/12] fix broken build Signed-off-by: David Chung --- pkg/rpc/mux/reverse_proxy_test.go | 3 ++- pkg/rpc/mux/server_test.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/rpc/mux/reverse_proxy_test.go b/pkg/rpc/mux/reverse_proxy_test.go index 06d335231..7bdddb998 100644 --- a/pkg/rpc/mux/reverse_proxy_test.go +++ b/pkg/rpc/mux/reverse_proxy_test.go @@ -131,7 +131,8 @@ func TestMuxPlugins(t *testing.T) { require.Equal(t, []string{"region"}, first(must(rpc_metadata.NewClient(socketPath)).List(types.PathFromString("aws")))) - infoClient := client.NewPluginInfoClient(socketPath) + infoClient, err := client.NewPluginInfoClient(socketPath) + require.NoError(t, err) info, err := infoClient.GetInfo() require.NoError(t, err) T(100).Infoln("info=", info) diff --git a/pkg/rpc/mux/server_test.go b/pkg/rpc/mux/server_test.go index 32c856309..828895f87 100644 --- a/pkg/rpc/mux/server_test.go +++ b/pkg/rpc/mux/server_test.go @@ -36,7 +36,8 @@ func TestMuxServer(t *testing.T) { require.Equal(t, []string{"region"}, first(must(rpc_metadata.NewClient(socketPath)).List(types.PathFromString("aws")))) - infoClient := client.NewPluginInfoClient(socketPath) + infoClient, err := client.NewPluginInfoClient(socketPath) + require.NoError(t, err) info, err := infoClient.GetInfo() require.NoError(t, err) T(100).Infoln("info=", info) From a6005e251869b6f5369f9645f380f1ea21eaa5ef Mon Sep 17 00:00:00 2001 From: David Chung Date: Sun, 14 May 2017 14:47:20 -0700 Subject: [PATCH 05/12] move remotes determination to pkg/cli Signed-off-by: David Chung --- cmd/infrakit/main.go | 61 +--------------------------------------- pkg/cli/remotes.go | 67 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 pkg/cli/remotes.go diff --git a/cmd/infrakit/main.go b/cmd/infrakit/main.go index 754182757..c237b524d 100644 --- a/cmd/infrakit/main.go +++ b/cmd/infrakit/main.go @@ -3,11 +3,7 @@ package main import ( "flag" "fmt" - "io/ioutil" - "net/url" "os" - "path/filepath" - "strings" "github.com/docker/infrakit/cmd/infrakit/base" "github.com/docker/infrakit/pkg/cli" @@ -16,7 +12,6 @@ import ( discovery_local "github.com/docker/infrakit/pkg/discovery/local" "github.com/docker/infrakit/pkg/discovery/remote" logutil "github.com/docker/infrakit/pkg/log" - "github.com/docker/infrakit/pkg/types" "github.com/spf13/cobra" _ "github.com/docker/infrakit/pkg/cli/v1" @@ -77,7 +72,7 @@ func main() { cmd.SilenceErrors = true f := func() discovery.Plugins { - ulist, err := remotes() + ulist, err := cli.Remotes() if err != nil { log.Crit("Cannot lookup plugins", "err", err) os.Exit(1) @@ -138,60 +133,6 @@ func main() { } } -func remotes() ([]*url.URL, error) { - ulist := []*url.URL{} - - hosts := []string{} - // See if INFRAKIT_HOST is set to point to a host list in the $INFRAKIT_HOME/hosts file. - host := os.Getenv("INFRAKIT_HOST") - if host == "" { - return ulist, nil // do nothing -- local mode - } - - // If the env is set but we don't have any hosts file locally, don't exit. - // Print a warning and proceed. - // Now look up the host lists in the file - hostsFile := filepath.Join(os.Getenv("INFRAKIT_HOME"), "hosts") - buff, err := ioutil.ReadFile(hostsFile) - if err != nil { - return ulist, nil // do nothing -- local mode - } - - m := map[string]string{} - yaml, err := types.AnyYAML(buff) - if err != nil { - return nil, fmt.Errorf("bad format for hosts file at %s for INFRAKIT_HOST=%s, err=%v", hostsFile, host, err) - } - err = yaml.Decode(&m) - if err != nil { - return nil, fmt.Errorf("cannot decode hosts file at %s for INFRAKIT_HOST=%s, err=%v", hostsFile, host, err) - } - - if list, has := m[host]; has { - hosts = strings.Split(list, ",") - } else { - return nil, fmt.Errorf("no entry in hosts file at %s for INFRAKIT_HOST=%s", hostsFile, host) - } - - for _, h := range hosts { - addProtocol := false - if !strings.Contains(h, "://") { - h = "http://" + h - addProtocol = true - } - u, err := url.Parse(h) - if err != nil { - panic(err) - } - if addProtocol { - u.Scheme = "http" - } - ulist = append(ulist, u) - } - - return ulist, nil -} - const ( helpTemplate = ` diff --git a/pkg/cli/remotes.go b/pkg/cli/remotes.go new file mode 100644 index 000000000..f6ef0390e --- /dev/null +++ b/pkg/cli/remotes.go @@ -0,0 +1,67 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/docker/infrakit/pkg/types" +) + +// Remotes returns a list of remote URLs to connect to +func Remotes() ([]*url.URL, error) { + ulist := []*url.URL{} + + hosts := []string{} + // See if INFRAKIT_HOST is set to point to a host list in the $INFRAKIT_HOME/hosts file. + host := os.Getenv("INFRAKIT_HOST") + if host == "" { + return ulist, nil // do nothing -- local mode + } + + // If the env is set but we don't have any hosts file locally, don't exit. + // Print a warning and proceed. + // Now look up the host lists in the file + hostsFile := filepath.Join(os.Getenv("INFRAKIT_HOME"), "hosts") + buff, err := ioutil.ReadFile(hostsFile) + if err != nil { + return ulist, nil // do nothing -- local mode + } + + m := map[string]string{} + yaml, err := types.AnyYAML(buff) + if err != nil { + return nil, fmt.Errorf("bad format for hosts file at %s for INFRAKIT_HOST=%s, err=%v", hostsFile, host, err) + } + err = yaml.Decode(&m) + if err != nil { + return nil, fmt.Errorf("cannot decode hosts file at %s for INFRAKIT_HOST=%s, err=%v", hostsFile, host, err) + } + + if list, has := m[host]; has { + hosts = strings.Split(list, ",") + } else { + return nil, fmt.Errorf("no entry in hosts file at %s for INFRAKIT_HOST=%s", hostsFile, host) + } + + for _, h := range hosts { + addProtocol := false + if !strings.Contains(h, "://") { + h = "http://" + h + addProtocol = true + } + u, err := url.Parse(h) + if err != nil { + panic(err) + } + if addProtocol { + u.Scheme = "http" + } + ulist = append(ulist, u) + } + + return ulist, nil +} From f47dc332c2e759dba014a1eb74dddde5ad194bcd Mon Sep 17 00:00:00 2001 From: David Chung Date: Sun, 14 May 2017 16:29:48 -0700 Subject: [PATCH 06/12] fix bugs with info / remote proxy Signed-off-by: David Chung --- cmd/infrakit/info/info.go | 158 -------------------------------- cmd/infrakit/main.go | 3 +- pkg/cli/v1/info.go | 6 ++ pkg/cli/v1/resource/cmd.go | 30 ++++++ pkg/cli/v1/resource/commit.go | 65 +++++++++++++ pkg/cli/v1/resource/describe.go | 64 +++++++++++++ pkg/cli/v1/resource/destroy.go | 62 +++++++++++++ pkg/rpc/client/info.go | 13 ++- pkg/rpc/mux/reverse_proxy.go | 1 + 9 files changed, 240 insertions(+), 162 deletions(-) delete mode 100644 cmd/infrakit/info/info.go create mode 100644 pkg/cli/v1/resource/cmd.go create mode 100644 pkg/cli/v1/resource/commit.go create mode 100644 pkg/cli/v1/resource/describe.go create mode 100644 pkg/cli/v1/resource/destroy.go diff --git a/cmd/infrakit/info/info.go b/cmd/infrakit/info/info.go deleted file mode 100644 index 90e351018..000000000 --- a/cmd/infrakit/info/info.go +++ /dev/null @@ -1,158 +0,0 @@ -package info - -import ( - "encoding/json" - "fmt" - - "github.com/docker/infrakit/cmd/infrakit/base" - "github.com/docker/infrakit/pkg/cli" - "github.com/docker/infrakit/pkg/discovery" - "github.com/docker/infrakit/pkg/plugin" - "github.com/docker/infrakit/pkg/rpc/client" - "github.com/docker/infrakit/pkg/template" - "github.com/spf13/cobra" -) - -func init() { - base.Register(Command) -} - -// Command creates a cobra Command that prints build version information. -func Command(plugins func() discovery.Plugins) *cobra.Command { - cmd := &cobra.Command{ - Use: "info", - Short: "print plugin info", - PersistentPreRunE: func(c *cobra.Command, args []string) error { - return cli.EnsurePersistentPreRunE(c) - }, - } - name := cmd.PersistentFlags().String("name", "", "Name of plugin") - raw := cmd.PersistentFlags().Bool("raw", false, "True to show raw data") - - api := &cobra.Command{ - Use: "api", - Short: "Show api / RPC interface supported by the plugin of the given name", - } - - templateFuncs := &cobra.Command{ - Use: "template", - Short: "Show template functions supported by the plugin, if the plugin uses template for configuration.", - } - cmd.AddCommand(api, templateFuncs) - - api.RunE = func(cmd *cobra.Command, args []string) error { - endpoint, err := plugins().Find(plugin.Name(*name)) - if err != nil { - return err - } - - infoClient, err := client.NewPluginInfoClient(endpoint.Address) - if err != nil { - return err - } - - info, err := infoClient.GetInfo() - if err != nil { - return err - } - - if *raw { - buff, err := json.MarshalIndent(info, "", " ") - if err != nil { - return err - } - - fmt.Println(string(buff)) - return nil - } - // render a view using template - renderer, err := template.NewTemplate("str://"+apiViewTemplate, template.Options{}) - if err != nil { - return err - } - - view, err := renderer.Global("plugin", *name).Render(info) - if err != nil { - return err - } - - fmt.Print(view) - return nil - } - - templateFuncs.RunE = func(cmd *cobra.Command, args []string) error { - endpoint, err := plugins().Find(plugin.Name(*name)) - if err != nil { - return err - } - - infoClient, err := client.NewPluginInfoClient(endpoint.Address) - if err != nil { - return err - } - info, err := infoClient.GetFunctions() - if err != nil { - return err - } - - if *raw { - buff, err := json.MarshalIndent(info, "", " ") - if err != nil { - return err - } - - fmt.Println(string(buff)) - return nil - } - // render a view using template - renderer, err := template.NewTemplate("str://"+funcsViewTemplate, template.Options{}) - if err != nil { - return err - } - - view, err := renderer.Global("plugin", *name).Render(info) - if err != nil { - return err - } - - fmt.Print(view) - return nil - } - - return cmd -} - -const ( - apiViewTemplate = ` -Plugin: {{var "plugin"}} -Implements: {{range $spi := .Implements}}{{$spi.Name}}/{{$spi.Version}} {{end}} -Interfaces: {{range $iface := .Interfaces}} - SPI: {{$iface.Name}}/{{$iface.Version}} - RPC: {{range $method := $iface.Methods}} - Method: {{$method.Request | q "method" }} - Request: - {{$method.Request | jsonEncode | yamlEncode }} - - Response: - {{$method.Response | jsonEncode | yamlEncode }} - - ------------------------- - {{end}} -{{end}} -` - - funcsViewTemplate = ` -{{range $category, $functions := .}} -{{var "plugin"}}/{{$category}} _________________________________________________________________________________________ - {{range $f := $functions}} - Name: {{$f.Name}} - Description: {{join "\n " $f.Description}} - Function: {{$f.Function}} - Usage: {{$f.Usage}} - - ------------------------- - {{end}} - -{{end}} -` -) diff --git a/cmd/infrakit/main.go b/cmd/infrakit/main.go index c237b524d..2f57efefc 100644 --- a/cmd/infrakit/main.go +++ b/cmd/infrakit/main.go @@ -20,12 +20,11 @@ import ( //_ "github.com/docker/infrakit/cmd/infrakit/flavor" //_ "github.com/docker/infrakit/cmd/infrakit/instance" //_ "github.com/docker/infrakit/cmd/infrakit/group" - //_ "github.com/docker/infrakit/cmd/infrakit/info" + //_ "github.com/docker/infrakit/cmd/infrakit/resource" _ "github.com/docker/infrakit/cmd/infrakit/event" _ "github.com/docker/infrakit/cmd/infrakit/manager" _ "github.com/docker/infrakit/cmd/infrakit/metadata" - _ "github.com/docker/infrakit/cmd/infrakit/resource" _ "github.com/docker/infrakit/cmd/infrakit/playbook" _ "github.com/docker/infrakit/cmd/infrakit/plugin" diff --git a/pkg/cli/v1/info.go b/pkg/cli/v1/info.go index 029e8e01c..10a03786f 100644 --- a/pkg/cli/v1/info.go +++ b/pkg/cli/v1/info.go @@ -10,6 +10,7 @@ import ( "github.com/docker/infrakit/pkg/spi/flavor" "github.com/docker/infrakit/pkg/spi/group" "github.com/docker/infrakit/pkg/spi/instance" + "github.com/docker/infrakit/pkg/spi/resource" "github.com/docker/infrakit/pkg/template" "github.com/spf13/cobra" @@ -17,6 +18,7 @@ import ( _ "github.com/docker/infrakit/pkg/cli/v1/flavor" _ "github.com/docker/infrakit/pkg/cli/v1/group" _ "github.com/docker/infrakit/pkg/cli/v1/instance" + _ "github.com/docker/infrakit/pkg/cli/v1/resource" ) func init() { @@ -32,6 +34,10 @@ func init() { []cli.CmdBuilder{ Info, }) + cli.Register(resource.InterfaceSpec, + []cli.CmdBuilder{ + Info, + }) } // Info returns the info command diff --git a/pkg/cli/v1/resource/cmd.go b/pkg/cli/v1/resource/cmd.go new file mode 100644 index 000000000..fa65ef0cf --- /dev/null +++ b/pkg/cli/v1/resource/cmd.go @@ -0,0 +1,30 @@ +package resource + +import ( + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/discovery" + logutil "github.com/docker/infrakit/pkg/log" + "github.com/docker/infrakit/pkg/plugin" + resource_rpc "github.com/docker/infrakit/pkg/rpc/resource" + "github.com/docker/infrakit/pkg/spi/resource" +) + +var log = logutil.New("module", "cli/v1/resource") + +func init() { + cli.Register(resource.InterfaceSpec, + []cli.CmdBuilder{ + Commit, + Describe, + Destroy, + }) +} + +// LoadPlugin loads the typed plugin +func LoadPlugin(plugins discovery.Plugins, name string) (resource.Plugin, error) { + endpoint, err := plugins.Find(plugin.Name(name)) + if err != nil { + return nil, err + } + return resource_rpc.NewClient(endpoint.Address) +} diff --git a/pkg/cli/v1/resource/commit.go b/pkg/cli/v1/resource/commit.go new file mode 100644 index 000000000..996df973b --- /dev/null +++ b/pkg/cli/v1/resource/commit.go @@ -0,0 +1,65 @@ +package resource + +import ( + "fmt" + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/resource" + "github.com/docker/infrakit/pkg/types" + "github.com/spf13/cobra" +) + +// Commit returns the Commit command +func Commit(name string, services *cli.Services) *cobra.Command { + + commit := &cobra.Command{ + Use: "commit ", + Short: "Commit a resource configuration. Read from stdin if url is '-'", + } + + pretend := false + commit.Flags().BoolVarP(&pretend, "pretend", "p", pretend, "Don't actually commit, only explain where appropriate") + + commit.RunE = func(cmd *cobra.Command, args []string) error { + + if len(args) != 1 { + cmd.Usage() + os.Exit(1) + } + + resourcePlugin, err := LoadPlugin(services.Plugins(), name) + if err != nil { + return nil + } + cli.MustNotNil(resourcePlugin, "resource plugin not found", "name", name) + + view, err := services.ReadFromStdinIfElse( + func() bool { return args[0] == "-" }, + func() (string, error) { return services.ProcessTemplate(args[0]) }, + services.ToJSON, + ) + if err != nil { + return err + } + + spec := resource.Spec{} + if err := types.AnyString(view).Decode(&spec); err != nil { + return err + } + + details, err := resourcePlugin.Commit(spec, pretend) + if err != nil { + return err + } + + if pretend { + fmt.Printf("Committing %s would involve: %s\n", spec.ID, details) + } else { + fmt.Printf("Committed %s: %s\n", spec.ID, details) + } + return nil + } + commit.Flags().AddFlagSet(services.ProcessTemplateFlags) + return commit +} diff --git a/pkg/cli/v1/resource/describe.go b/pkg/cli/v1/resource/describe.go new file mode 100644 index 000000000..608187a28 --- /dev/null +++ b/pkg/cli/v1/resource/describe.go @@ -0,0 +1,64 @@ +package resource + +import ( + "os" + + "github.com/docker/infrakit/pkg/cli" + "github.com/docker/infrakit/pkg/spi/resource" + "github.com/docker/infrakit/pkg/types" + "github.com/spf13/cobra" +) + +// Describe returns the Describe command +func Describe(name string, services *cli.Services) *cobra.Command { + + describe := &cobra.Command{ + Use: "describe