diff --git a/cmd/catalog.go b/cmd/catalog.go index 3cc10b3b..debe6e21 100644 --- a/cmd/catalog.go +++ b/cmd/catalog.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/rancher/go-rancher/catalog" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -67,15 +67,6 @@ func CatalogCommand() cli.Command { }, }, }, - /* - cli.Command{ - Name: "upgrade", - Usage: "Upgrade catalog template", - Action: errorWrapper(envUpdate), - ArgsUsage: "[ID or NAME]" - Flags: []cli.Flag{}, - }, - */ }, } } @@ -86,50 +77,6 @@ type CatalogData struct { Category string } -func getEnvFilter(proj *client.Project, ctx *cli.Context) string { - envFilter := proj.Orchestration - if envFilter == "cattle" { - envFilter = "" - } - if ctx.Bool("system") { - envFilter = "infra" - } - return envFilter -} - -func isInCSV(value, csv string) bool { - for _, part := range strings.Split(csv, ",") { - if value == part { - return true - } - } - return false -} - -func isOrchestrationSupported(ctx *cli.Context, proj *client.Project, labels map[string]interface{}) bool { - // Only check for system templates - if !ctx.Bool("system") { - return true - } - - if supported, ok := labels[orchestrationSupported]; ok { - supportedString := fmt.Sprint(supported) - if supportedString != "" && !isInCSV(proj.Orchestration, supportedString) { - return false - } - } - - return true -} - -func isSupported(ctx *cli.Context, proj *client.Project, item catalog.Template) bool { - envFilter := getEnvFilter(proj, ctx) - if item.TemplateBase != envFilter { - return false - } - return isOrchestrationSupported(ctx, proj, item.Labels) -} - func catalogLs(ctx *cli.Context) error { writer := NewTableWriter([][]string{ {"NAME", "Template.Name"}, @@ -153,7 +100,7 @@ func catalogLs(ctx *cli.Context) error { } func forEachTemplate(ctx *cli.Context, f func(item catalog.Template) error) error { - _, c, proj, cc, err := setupCatalogContext(ctx) + _, c, _, cc, err := setupCatalogContext(ctx) if err != nil { return err } @@ -169,9 +116,6 @@ func forEachTemplate(ctx *cli.Context, f func(item catalog.Template) error) erro } for _, item := range collection.Data { - if !isSupported(ctx, proj, item) { - continue - } if err := f(item); err != nil { return err } @@ -234,7 +178,7 @@ func catalogInstall(ctx *cli.Context) error { return errors.New("Exactly one argument is required") } - _, c, proj, cc, err := setupCatalogContext(ctx) + _, c, _, cc, err := setupCatalogContext(ctx) if err != nil { return err } @@ -269,34 +213,15 @@ func catalogInstall(ctx *cli.Context) error { externalID := fmt.Sprintf("catalog://%s", templateVersion.Id) id := "" - switch proj.Orchestration { - case "cattle": - stack, err := c.Stack.Create(&client.Stack{ - Name: stackName, - DockerCompose: toString(templateVersion.Files["docker-compose.yml"]), - RancherCompose: toString(templateVersion.Files["rancher-compose.yml"]), - Environment: answers, - ExternalId: externalID, - System: ctx.Bool("system"), - StartOnCreate: true, - }) - if err != nil { - return err - } - id = stack.Id - case "kubernetes": - stack, err := c.KubernetesStack.Create(&client.KubernetesStack{ - Name: stackName, - Templates: templateVersion.Files, - ExternalId: externalID, - Environment: answers, - System: ctx.Bool("system"), - }) - if err != nil { - return err - } - id = stack.Id + stack, err := c.Stack.Create(&client.Stack{ + Name: stackName, + Templates: templateVersion.Files, + ExternalId: externalID, + }) + if err != nil { + return err } + id = stack.Id return WaitFor(ctx, id) } @@ -392,11 +317,11 @@ func GetCatalogClient(ctx *cli.Context) (*catalog.RancherClient, error) { return nil, err } - idx := strings.LastIndex(config.URL, "/v2-beta") + idx := strings.LastIndex(config.URL, "/v3") if idx == -1 { idx = strings.LastIndex(config.URL, "/v1") if idx == -1 { - return nil, fmt.Errorf("Invalid URL %s, must contain /v2-beta", config.URL) + return nil, fmt.Errorf("Invalid URL %s, must contain /v3", config.URL) } } diff --git a/cmd/common.go b/cmd/common.go index cd739d9e..d70484c3 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -11,7 +11,7 @@ import ( "syscall" "text/template" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/docker/docker/pkg/namesgenerator" "github.com/urfave/cli" @@ -32,7 +32,7 @@ func GetRawClient(ctx *cli.Context) (*client.RancherClient, error) { return nil, err } return client.NewRancherClient(&client.ClientOpts{ - Url: url + "/v2-beta", + Url: url + "/v3", AccessKey: config.AccessKey, SecretKey: config.SecretKey, }) @@ -93,7 +93,11 @@ func GetClient(ctx *cli.Context) (*client.RancherClient, error) { } func GetEnvironment(def string, c *client.RancherClient) (*client.Project, error) { - resp, err := c.Project.List(nil) + resp, err := c.Project.List(&client.ListOpts{ + Filters: map[string]interface{}{ + "all": true, + }, + }) if err != nil { return nil, err } @@ -109,7 +113,7 @@ func GetEnvironment(def string, c *client.RancherClient) (*client.Project, error if def == "" { names := []string{} for _, p := range resp.Data { - names = append(names, fmt.Sprintf("%s(%s)", p.Name, p.Id)) + names = append(names, fmt.Sprintf("%s(%s), cluster ID: (%s)", p.Name, p.Id, p.ClusterId)) } idx := selectFromList("Environments:", names) @@ -174,6 +178,24 @@ func RandomName() string { return strings.Replace(namesgenerator.GetRandomName(0), "_", "-", -1) } +func getContainerByName(c *client.RancherClient, name string) (client.ResourceCollection, error) { + var result client.ResourceCollection + stack, containerName, err := ParseName(c, name) + containers, err := c.Container.List(&client.ListOpts{ + Filters: map[string]interface{}{ + "stackId": stack.Id, + "name": containerName, + }, + }) + if err != nil { + return result, err + } + for _, container := range containers.Data { + result.Data = append(result.Data, container.Resource) + } + return result, nil +} + func getServiceByName(c *client.RancherClient, name string) (client.ResourceCollection, error) { var result client.ResourceCollection stack, serviceName, err := ParseName(c, name) @@ -219,9 +241,10 @@ func Lookup(c *client.RancherClient, name string, types ...string) (*client.Reso if len(collection.Data) > 1 { ids := []string{} for _, data := range collection.Data { - ids = append(ids, data.Id) + ids = append(ids, fmt.Sprintf("%s (%s)", data.Id, name)) } - return nil, fmt.Errorf("Multiple resources of type %s found for name %s: %v", schemaType, name, ids) + index := selectFromList("Resources: ", ids) + return &collection.Data[index], nil } if len(collection.Data) == 0 { @@ -232,6 +255,8 @@ func Lookup(c *client.RancherClient, name string, types ...string) (*client.Reso collection, err = getHostByHostname(c, name) case "service": collection, err = getServiceByName(c, name) + case "container": + collection, err = getContainerByName(c, name) } if err != nil { return nil, err diff --git a/cmd/config.go b/cmd/config.go index 50ad5faa..5e4551f3 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -10,7 +10,7 @@ import ( "path" "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/Sirupsen/logrus" "github.com/urfave/cli" diff --git a/cmd/docker.go b/cmd/docker.go index 167641d0..3d5527c0 100644 --- a/cmd/docker.go +++ b/cmd/docker.go @@ -11,7 +11,7 @@ import ( "strings" "github.com/Sirupsen/logrus" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/rancher/rancher-docker-api-proxy" "github.com/urfave/cli" ) diff --git a/cmd/env.go b/cmd/env.go index b71951ac..30a97e27 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -2,12 +2,10 @@ package cmd import ( "fmt" - "io/ioutil" - "os" - "github.com/rancher/go-rancher/v2" + "github.com/pkg/errors" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" - "gopkg.in/yaml.v2" ) func EnvCommand() cli.Command { @@ -30,7 +28,7 @@ func EnvCommand() cli.Command { Action: defaultAction(envLs), Flags: envLsFlags, Subcommands: []cli.Command{ - cli.Command{ + { Name: "ls", Usage: "List environments", Description: "\nWith an account API key, all environments in Rancher will be listed. If you are using an environment API key, it will only list the environment of the API key. \n\nExample:\n\t$ rancher env ls\n", @@ -38,7 +36,7 @@ func EnvCommand() cli.Command { Action: envLs, Flags: envLsFlags, }, - cli.Command{ + { Name: "create", Usage: "Create an environment", Description: ` @@ -57,41 +55,13 @@ To add an orchestration framework do TODO Action: envCreate, Flags: []cli.Flag{ cli.StringFlag{ - Name: "template,t", - Usage: "Environment template to create from", - Value: "Cattle", + Name: "cluster,c", + Usage: "Cluster name to create the environment", + Value: "Default", }, }, }, - cli.Command{ - Name: "templates", - ShortName: "template", - Usage: "Interact with environment templates", - Action: defaultAction(envTemplateLs), - Flags: envLsFlags, - Subcommands: []cli.Command{ - cli.Command{ - Name: "export", - Usage: "Export an environment template to STDOUT", - ArgsUsage: "[TEMPLATEID TEMPLATENAME...]", - Action: envTemplateExport, - Flags: []cli.Flag{}, - }, - cli.Command{ - Name: "import", - Usage: "Import an environment template to from file", - ArgsUsage: "[FILE FILE...]", - Action: envTemplateImport, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "public", - Usage: "Make template public", - }, - }, - }, - }, - }, - cli.Command{ + { Name: "rm", Usage: "Remove environment(s)", Description: "\nExample:\n\t$ rancher env rm 1a5\n\t$ rancher env rm newEnv\n", @@ -99,7 +69,7 @@ To add an orchestration framework do TODO Action: envRm, Flags: []cli.Flag{}, }, - cli.Command{ + { Name: "deactivate", Usage: "Deactivate environment(s)", Description: ` @@ -113,7 +83,7 @@ Example: Action: envDeactivate, Flags: []cli.Flag{}, }, - cli.Command{ + { Name: "activate", Usage: "Activate environment(s)", Description: ` @@ -127,6 +97,20 @@ Example: Action: envActivate, Flags: []cli.Flag{}, }, + { + Name: "switch", + Usage: "Switch environment(s)", + Description: ` +Switch current environment to others, + +Example: + $ rancher env switch 1a5 + $ rancher env switch Default +`, + ArgsUsage: "[ID NAME...]", + Action: envSwitch, + Flags: []cli.Flag{}, + }, }, } } @@ -134,17 +118,20 @@ Example: type EnvData struct { ID string Environment *client.Project + Current string + Name string } -type TemplateData struct { - ID string - ProjectTemplate *client.ProjectTemplate -} - -func NewEnvData(project client.Project) *EnvData { +func NewEnvData(project client.Project, current bool, name string) *EnvData { + marked := "" + if current { + marked = " *" + } return &EnvData{ ID: project.Id, Environment: &project, + Current: marked, + Name: name, } } @@ -169,18 +156,22 @@ func envCreate(ctx *cli.Context) error { if ctx.NArg() > 0 { name = ctx.Args()[0] } - - data := map[string]interface{}{ - "name": name, + clusters, err := c.Cluster.List(&client.ListOpts{ + Filters: map[string]interface{}{ + "name": ctx.String("cluster"), + "removed_null": true, + }, + }) + if err != nil { + return err } - template := ctx.String("template") - if template != "" { - template, err := Lookup(c, template, "projectTemplate") - if err != nil { - return err - } - data["projectTemplateId"] = template.Id + if len(clusters.Data) == 0 { + return errors.Errorf("can't find cluster with name %v", ctx.String("cluster")) + } + data := map[string]interface{}{ + "name": name, + "clusterId": clusters.Data[0].Id, } var newEnv client.Project @@ -197,23 +188,46 @@ func envLs(ctx *cli.Context) error { if err != nil { return err } + config, err := lookupConfig(ctx) + if err != nil { + return err + } + currentEnvID := config.Environment writer := NewTableWriter([][]string{ {"ID", "ID"}, - {"NAME", "Environment.Name"}, - {"ORCHESTRATION", "Environment.Orchestration"}, + {"CLUSTER/NAME", "Name"}, {"STATE", "Environment.State"}, {"CREATED", "Environment.Created"}, + {"CURRENT", "Current"}, }, ctx) defer writer.Close() - collection, err := c.Project.List(defaultListOpts(ctx)) + listOpts := defaultListOpts(ctx) + listOpts.Filters["all"] = true + collection, err := c.Project.List(listOpts) if err != nil { return err } for _, item := range collection.Data { - writer.Write(NewEnvData(item)) + current := false + if item.Id == currentEnvID { + current = true + } + clusterName := "" + if item.ClusterId != "" { + cluster, err := c.Cluster.ById(item.ClusterId) + if err != nil { + return err + } + clusterName = cluster.Name + } + name := item.Name + if clusterName != "" { + name = fmt.Sprintf("%s/%s", clusterName, name) + } + writer.Write(NewEnvData(item, current, name)) } return writer.Err() @@ -249,119 +263,48 @@ func envActivate(ctx *cli.Context) error { }) } -func envTemplateLs(ctx *cli.Context) error { +func envSwitch(ctx *cli.Context) error { c, err := GetRawClient(ctx) if err != nil { return err } - writer := NewTableWriter([][]string{ - {"ID", "ID"}, - {"NAME", "ProjectTemplate.Name"}, - {"DESC", "ProjectTemplate.Description"}, - }, ctx) - defer writer.Close() - - collection, err := c.ProjectTemplate.List(defaultListOpts(ctx)) + if ctx.NArg() == 0 { + return cli.ShowCommandHelp(ctx, "env") + } + envID := "" + name := ctx.Args()[0] + if env, err := c.Project.ById(name); err == nil && env != nil && env.Id == name { + envID = name + } else { + if envs, err := c.Project.List(&client.ListOpts{ + Filters: map[string]interface{}{ + "name": name, + }, + }); err == nil { + if len(envs.Data) == 1 { + envID = envs.Data[0].Id + } else if len(envs.Data) > 1 { + names := []string{} + for _, item := range envs.Data { + names = append(names, fmt.Sprintf("%s(%s/%s)", item.Name, item.ClusterId, item.Id)) + } + idx := selectFromList("Found multiple environments in different clusters:", names) + envID = envs.Data[idx].Id + } + } + } + if envID == "" { + return cli.NewExitError("Error: can't find associated environment", 1) + } + config, err := lookupConfig(ctx) if err != nil { return err } - - for _, item := range collection.Data { - writer.Write(TemplateData{ - ID: item.Id, - ProjectTemplate: &item, - }) + config.Environment = envID + err = config.Write() + if err != nil { + return err } - - return writer.Err() -} - -func envTemplateImport(ctx *cli.Context) error { - c, err := GetRawClient(ctx) - if err != nil { - return err - } - - w, err := NewWaiter(ctx) - if err != nil { - return err - } - - for _, file := range ctx.Args() { - template := client.ProjectTemplate{ - IsPublic: ctx.Bool("public"), - } - content, err := ioutil.ReadFile(file) - if err != nil { - return err - } - - if err := yaml.Unmarshal(content, &template); err != nil { - return err - } - - created, err := c.ProjectTemplate.Create(&template) - if err != nil { - return err - } - - w.Add(created.Id) - } - - return w.Wait() -} - -func envTemplateExport(ctx *cli.Context) error { - c, err := GetRawClient(ctx) - if err != nil { - return err - } - - for _, name := range ctx.Args() { - r, err := Lookup(c, name, "projectTemplate") - if err != nil { - return err - } - - template, err := c.ProjectTemplate.ById(r.Id) - if err != nil { - return err - } - - stacks := []map[string]interface{}{} - for _, s := range template.Stacks { - data := map[string]interface{}{ - "name": s.Name, - } - if s.TemplateId != "" { - data["template_id"] = s.TemplateId - } - if s.TemplateVersionId != "" { - data["template_version_id"] = s.TemplateVersionId - } - if len(s.Answers) > 0 { - data["answers"] = s.Answers - } - stacks = append(stacks, data) - } - - data := map[string]interface{}{ - "name": template.Name, - "description": template.Description, - "stacks": stacks, - } - - content, err := yaml.Marshal(&data) - if err != nil { - return err - } - - _, err = os.Stdout.Write(content) - if err != nil { - return err - } - } - - return nil + return envLs(ctx) } diff --git a/cmd/events.go b/cmd/events.go index 5c27da57..13213913 100644 --- a/cmd/events.go +++ b/cmd/events.go @@ -6,7 +6,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/rancher/cli/monitor" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) diff --git a/cmd/exec.go b/cmd/exec.go index b82ae13c..2f7ee73e 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -90,7 +90,7 @@ func selectContainer(c *client.RancherClient, args []string) ([]string, string, return nil, "", "", err } - if _, ok := resource.Links["hosts"]; ok { + if _, ok := resource.Links["host"]; ok { hostID, containerID, err := getHostnameAndContainerID(c, resource.Id) if err != nil { return nil, "", "", err @@ -170,16 +170,15 @@ func getHostnameAndContainerID(c *client.RancherClient, containerID string) (str return "", "", err } - var hosts client.HostCollection - if err := c.GetLink(container.Resource, "hosts", &hosts); err != nil { + var host client.Host + if err := c.GetLink(container.Resource, "host", &host); err != nil { return "", "", err } - - if len(hosts.Data) != 1 { + if host.Id == "" { return "", "", fmt.Errorf("Failed to find host for container %s", container.Name) } - return hosts.Data[0].Id, container.ExternalId, nil + return host.Id, container.ExternalId, nil } func runDockerHelp(subcommand string) error { diff --git a/cmd/export.go b/cmd/export.go index 52f7ec58..39a6930e 100644 --- a/cmd/export.go +++ b/cmd/export.go @@ -9,7 +9,7 @@ import ( "path/filepath" "github.com/Sirupsen/logrus" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -111,10 +111,7 @@ func exportService(ctx *cli.Context) error { return err } - if err := addToTar(archive, stack.Name, "docker-compose.yml", config.DockerComposeConfig); err != nil { - return err - } - if err := addToTar(archive, stack.Name, "rancher-compose.yml", config.RancherComposeConfig); err != nil { + if err := addToTar(archive, stack.Name, "compose.yml", config.DockerComposeConfig); err != nil { return err } if len(config.Actions) > 0 { diff --git a/cmd/format.go b/cmd/format.go index 3853b375..1770b590 100644 --- a/cmd/format.go +++ b/cmd/format.go @@ -4,9 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "strings" - - "github.com/rancher/go-rancher/v2" ) func FormatEndpoint(data interface{}) string { @@ -35,19 +32,21 @@ func FormatEndpoint(data interface{}) string { } func FormatIPAddresses(data interface{}) string { - ips, ok := data.([]client.IpAddress) - if !ok { - return "" - } - - ipStrings := []string{} - for _, ip := range ips { - if ip.Address != "" { - ipStrings = append(ipStrings, ip.Address) - } - } - - return strings.Join(ipStrings, ", ") + //todo: revisit + return "" + //ips, ok := data.([]client.IpAddress) + //if !ok { + // return "" + //} + // + //ipStrings := []string{} + //for _, ip := range ips { + // if ip.Address != "" { + // ipStrings = append(ipStrings, ip.Address) + // } + //} + // + //return strings.Join(ipStrings, ", ") } func FormatJSON(data interface{}) (string, error) { diff --git a/cmd/host.go b/cmd/host.go index 30da9f61..51db1b9f 100644 --- a/cmd/host.go +++ b/cmd/host.go @@ -4,7 +4,7 @@ import ( "bytes" "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -76,7 +76,7 @@ func getLabels(host *client.Host) string { buffer.WriteString(key) buffer.WriteString("=") - buffer.WriteString(value.(string)) + buffer.WriteString(value) it++ } return buffer.String() diff --git a/cmd/host_create.go b/cmd/host_create.go index c8df98e6..63bbc026 100644 --- a/cmd/host_create.go +++ b/cmd/host_create.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/Sirupsen/logrus" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -282,12 +282,12 @@ func hostCreateRun(ctx *cli.Context, c *client.RancherClient, machineSchema clie var lastErr error for _, name := range names { args["hostname"] = name - var machine client.Machine - if err := c.Create("host", args, &machine); err != nil { + var host client.Host + if err := c.Create("host", args, &host); err != nil { lastErr = err logrus.Error(err) } else { - w.Add(machine.Id) + w.Add(host.Id) } } diff --git a/cmd/inspect.go b/cmd/inspect.go index d242f410..8f5f7382 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -3,7 +3,7 @@ package cmd import ( "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) diff --git a/cmd/logs.go b/cmd/logs.go index 8744971f..244c2b00 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -18,7 +18,7 @@ import ( "github.com/docker/libcompose/cli/logger" "github.com/mitchellh/mapstructure" "github.com/rancher/cli/monitor" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/rancher/rancher-docker-api-proxy" "github.com/urfave/cli" ) diff --git a/cmd/prompt.go b/cmd/prompt.go new file mode 100644 index 00000000..a8234be8 --- /dev/null +++ b/cmd/prompt.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "fmt" + + "github.com/c-bata/go-prompt" + "github.com/rancher/cli/rancher_prompt" + "github.com/urfave/cli" +) + +func PromptCommand() cli.Command { + return cli.Command{ + Name: "prompt", + Usage: "Enter rancher cli auto-prompt mode", + ArgsUsage: "None", + Action: promptAction, + Flags: []cli.Flag{}, + } +} + +func promptAction(ctx *cli.Context) error { + fmt.Print("rancher cli auto-completion mode") + defer fmt.Println("Goodbye!") + p := prompt.New( + rancherPrompt.Executor, + rancherPrompt.Completer, + prompt.OptionTitle("rancher-prompt: interactive rancher client"), + prompt.OptionPrefix("rancher$ "), + prompt.OptionInputTextColor(prompt.Yellow), + prompt.OptionMaxSuggestion(20), + ) + p.Run() + return nil +} diff --git a/cmd/ps.go b/cmd/ps.go index 19d0673b..146244d3 100644 --- a/cmd/ps.go +++ b/cmd/ps.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/pkg/errors" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -50,7 +50,7 @@ func GetStackMap(c *client.RancherClient) map[string]client.Stack { } type PsData struct { - Service client.Service + Service interface{} Name string LaunchConfig interface{} Stack client.Stack @@ -86,6 +86,11 @@ func servicePs(ctx *cli.Context) error { return errors.Wrap(err, "service list failed") } + collectionContainers, err := c.Container.List(defaultListOpts(ctx)) + if err != nil { + return errors.Wrap(err, "container list failed") + } + writer := NewTableWriter([][]string{ {"ID", "Service.Id"}, {"TYPE", "Service.Type"}, @@ -93,8 +98,7 @@ func servicePs(ctx *cli.Context) error { {"IMAGE", "LaunchConfig.ImageUuid"}, {"STATE", "CombinedState"}, {"SCALE", "{{len .Service.InstanceIds}}/{{.Service.Scale}}"}, - {"SYSTEM", "Service.System"}, - {"ENDPOINTS", "{{endpoint .Service.PublicEndpoints}}"}, + {"ENDPOINTS", "{{range .Service.PublicEndpoints}}{{.AgentIpAddress}}:{{.PublicPort}}:{{.PrivatePort}}/{{.Protocol}} {{end}}"}, {"DETAIL", "Service.TransitioningMessage"}, }, ctx) @@ -104,6 +108,7 @@ func servicePs(ctx *cli.Context) error { if item.LaunchConfig != nil { item.LaunchConfig.ImageUuid = strings.TrimPrefix(item.LaunchConfig.ImageUuid, "docker:") } + item.Type = "service" combined := item.HealthState if item.State != "active" || combined == "" { @@ -135,6 +140,34 @@ func servicePs(ctx *cli.Context) error { } } + for _, item := range collectionContainers.Data { + if len(item.ServiceIds) == 0 && item.StackId != "" { + launchConfig := client.LaunchConfig{} + launchConfig.ImageUuid = item.Image + + service := client.Service{} + service.Id = item.Id + service.Type = "standalone" + service.InstanceIds = []string{item.Id} + service.Scale = 1 + service.PublicEndpoints = item.PublicEndpoints + service.TransitioningMessage = item.TransitioningMessage + + combined := item.HealthState + if item.State != "active" || combined == "" { + combined = item.State + } + writer.Write(PsData{ + ID: item.Id, + Service: service, + Name: fmt.Sprintf("%s/%s", stackMap[item.StackId].Name, item.Name), + LaunchConfig: launchConfig, + Stack: stackMap[item.StackId], + CombinedState: combined, + }) + } + } + return writer.Err() } @@ -193,7 +226,7 @@ func containerPs(ctx *cli.Context, containers []client.Container) error { {"STATE", "CombinedState"}, {"HOST", "Container.HostId"}, {"IP", "Container.PrimaryIpAddress"}, - {"DOCKER", "DockerID"}, + {"DOCKER_ID", "DockerID"}, {"DETAIL", "Container.TransitioningMessage"}, //TODO: {"PORTS", "{{ports .Container.Ports}}"}, }, ctx) diff --git a/cmd/pull.go b/cmd/pull.go new file mode 100644 index 00000000..ca1c7e89 --- /dev/null +++ b/cmd/pull.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "fmt" + "github.com/fatih/color" + "github.com/rancher/go-rancher/v3" + "github.com/urfave/cli" + "time" +) + +func PullCommand() cli.Command { + return cli.Command{ + Name: "pull", + Usage: "Pull images on hosts that are in the current environment. Examples: rancher pull ubuntu", + Action: pullImages, + Subcommands: []cli.Command{}, + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "hosts", + Usage: "Specify which host should pull images. By default it will pull images on all the hosts in the current environment. Examples: rancher pull --hosts 1h1 --hosts 1h2 ubuntu", + }, + }, + } +} + +func pullImages(ctx *cli.Context) error { + c, err := GetClient(ctx) + if err != nil { + return err + } + if ctx.NArg() == 0 { + return cli.ShowCommandHelp(ctx, "") + } + image := ctx.Args()[0] + + hosts := ctx.StringSlice("hosts") + if len(hosts) == 0 { + hts, err := c.Host.List(defaultListOpts(ctx)) + if err != nil { + return err + } + for _, ht := range hts.Data { + hosts = append(hosts, ht.Id) + } + } + pullTask := client.PullTask{ + Mode: "all", + Image: image, + HostIds: hosts, + } + task, err := c.PullTask.Create(&pullTask) + if err != nil { + return err + } + cl := getRandomColor() + lastMsg := "" + for { + if task.Transitioning != "yes" { + fmt.Printf("Finished pulling image %s\n", image) + return nil + } + time.Sleep(150 * time.Millisecond) + if task.TransitioningMessage != lastMsg { + color.New(cl).Printf("Pulling image. Status: %s\n", task.TransitioningMessage) + lastMsg = task.TransitioningMessage + } + task, err = c.PullTask.ById(task.Id) + if err != nil { + return err + } + } +} diff --git a/cmd/restart.go b/cmd/restart.go index 0f5f776c..0ffdc6a3 100644 --- a/cmd/restart.go +++ b/cmd/restart.go @@ -3,7 +3,7 @@ package cmd import ( "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -40,12 +40,8 @@ func restartResources(ctx *cli.Context) error { if err != nil { return "", err } - err = c.Action(resource.Type, action, resource, &client.ServiceRestart{ - RollingRestartStrategy: client.RollingRestartStrategy{ - BatchSize: int64(ctx.Int("batch-size")), - IntervalMillis: int64(ctx.Int("interval")), - }, - }, resource) + //todo: revisit restart policy + err = c.Action(resource.Type, action, resource, nil, resource) return resource.Id, err }) } diff --git a/cmd/rm.go b/cmd/rm.go index 1d031fa5..ffdfb211 100644 --- a/cmd/rm.go +++ b/cmd/rm.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) diff --git a/cmd/run.go b/cmd/run.go index 37ea1c86..566c1921 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -3,7 +3,7 @@ package cmd import ( "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "fmt" "os" @@ -302,29 +302,173 @@ func serviceRun(ctx *cli.Context) error { if err != nil { return err } - launchConfig := &client.LaunchConfig{ + + if ctx.IsSet("scale") { + launchConfig := &client.LaunchConfig{ + //BlkioDeviceOptions: + BlkioWeight: ctx.Int64("blkio-weight"), + CapAdd: ctx.StringSlice("cap-add"), + CapDrop: ctx.StringSlice("cap-drop"), + //CpuSet: ctx.String(""), + CgroupParent: ctx.String("cgroup-parent"), + CpuSetMems: ctx.String("cpuset-mems"), + CpuPeriod: ctx.Int64("cpu-period"), + CpuQuota: ctx.Int64("cpu-quota"), + CpuShares: ctx.Int64("cpu-shares"), + Devices: ctx.StringSlice("device"), + Dns: ctx.StringSlice("dns"), + DnsOpt: ctx.StringSlice("dns-opt"), + DnsSearch: ctx.StringSlice("dns-search"), + EntryPoint: ctx.StringSlice("entrypoint"), + Expose: ctx.StringSlice("expose"), + GroupAdd: ctx.StringSlice("group-add"), + HealthCmd: ctx.StringSlice("health-cmd"), + HealthTimeout: ctx.Int64("health-timeout"), + HealthInterval: ctx.Int64("health-interval"), + HealthRetries: ctx.Int64("health-retries"), + Hostname: ctx.String("hostname"), + ImageUuid: "docker:" + ctx.Args()[0], + Ip: ctx.String("ip"), + Ip6: ctx.String("ip6"), + IpcMode: ctx.String("ipc"), + Isolation: ctx.String("isolation"), + KernelMemory: ctx.Int64("kernel-memory"), + Labels: map[string]string{}, + Environment: map[string]string{}, + //LogConfig: + Memory: ctx.Int64("memory"), + MemoryReservation: ctx.Int64("memory-reservation"), + MemorySwap: ctx.Int64("memory-swap"), + MemorySwappiness: ctx.Int64("memory-swappiness"), + //NetworkIds: ctx.StringSlice("networkids"), + NetAlias: ctx.StringSlice("net-alias"), + NetworkMode: ctx.String("net"), + OomKillDisable: ctx.Bool("oom-kill-disable"), + OomScoreAdj: ctx.Int64("oom-score-adj"), + PidMode: ctx.String("pid"), + PidsLimit: ctx.Int64("pids-limit"), + Ports: ctx.StringSlice("publish"), + Privileged: ctx.Bool("privileged"), + PublishAllPorts: ctx.Bool("publish-all"), + ReadOnly: ctx.Bool("read-only"), + //todo: add RunInit + //RunInit: ctx.Bool("init"), + SecurityOpt: ctx.StringSlice("security-opt"), + ShmSize: ctx.Int64("shm-size"), + StdinOpen: ctx.Bool("interactive"), + StopSignal: ctx.String("stop-signal"), + Tty: ctx.Bool("tty"), + User: ctx.String("user"), + Uts: ctx.String("uts"), + VolumeDriver: ctx.String("volume-driver"), + WorkingDir: ctx.String("workdir"), + DataVolumes: ctx.StringSlice("volume"), + } + + if ctx.String("log-driver") != "" || len(ctx.StringSlice("log-opt")) > 0 { + launchConfig.LogConfig = &client.LogConfig{ + Driver: ctx.String("log-driver"), + Config: map[string]string{}, + } + for _, opt := range ctx.StringSlice("log-opt") { + parts := strings.SplitN(opt, "=", 2) + if len(parts) > 1 { + launchConfig.LogConfig.Config[parts[0]] = parts[1] + } else { + launchConfig.LogConfig.Config[parts[0]] = "" + } + } + } + + for _, label := range ctx.StringSlice("label") { + parts := strings.SplitN(label, "=", 2) + value := "" + if len(parts) > 1 { + value = parts[1] + } + launchConfig.Labels[parts[0]] = value + } + + for _, env := range ctx.StringSlice("env") { + parts := strings.SplitN(env, "=", 2) + value := "" + + if len(parts) > 1 { + value = parts[1] + + if parts[0] == "" { + errMsg := fmt.Sprintf("invalid argument \"%s\" for e: invalid environment variable: %s\nSee 'rancher run --help'.", env, env) + return cli.NewExitError(errMsg, 1) + } + } else if len(parts) == 1 { + value = os.Getenv(parts[0]) + } + launchConfig.Environment[parts[0]] = value + } + + if ctx.Bool("schedule-global") { + launchConfig.Labels["io.rancher.scheduler.global"] = "true" + } + + if ctx.Bool("pull") { + launchConfig.Labels["io.rancher.container.pull_image"] = "always" + } + + args := ctx.Args()[1:] + + if len(args) > 0 { + launchConfig.Command = args + } + + stack, name, err := ParseName(c, ctx.String("name")) + if err != nil { + return err + } + service := &client.Service{ + Name: name, + StackId: stack.Id, + LaunchConfig: launchConfig, + Scale: int64(ctx.Int("scale")), + } + + service, err = c.Service.Create(service) + if err != nil { + return err + } + + return WaitFor(ctx, service.Id) + } + container := &client.Container{ //BlkioDeviceOptions: BlkioWeight: ctx.Int64("blkio-weight"), CapAdd: ctx.StringSlice("cap-add"), CapDrop: ctx.StringSlice("cap-drop"), //CpuSet: ctx.String(""), - CgroupParent: ctx.String("cgroup-parent"), - CpuSetMems: ctx.String("cpuset-mems"), - CpuPeriod: ctx.Int64("cpu-period"), - CpuQuota: ctx.Int64("cpu-quota"), - CpuShares: ctx.Int64("cpu-shares"), - Devices: ctx.StringSlice("device"), - Dns: ctx.StringSlice("dns"), - DnsOpt: ctx.StringSlice("dns-opt"), - DnsSearch: ctx.StringSlice("dns-search"), - EntryPoint: ctx.StringSlice("entrypoint"), - Expose: ctx.StringSlice("expose"), - GroupAdd: ctx.StringSlice("group-add"), - Hostname: ctx.String("hostname"), - ImageUuid: "docker:" + ctx.Args()[0], - KernelMemory: ctx.Int64("kernel-memory"), - Labels: map[string]interface{}{}, - Environment: map[string]interface{}{}, + CgroupParent: ctx.String("cgroup-parent"), + CpuSetMems: ctx.String("cpuset-mems"), + CpuPeriod: ctx.Int64("cpu-period"), + CpuQuota: ctx.Int64("cpu-quota"), + CpuShares: ctx.Int64("cpu-shares"), + Devices: ctx.StringSlice("device"), + Dns: ctx.StringSlice("dns"), + DnsOpt: ctx.StringSlice("dns-opt"), + DnsSearch: ctx.StringSlice("dns-search"), + EntryPoint: ctx.StringSlice("entrypoint"), + Expose: ctx.StringSlice("expose"), + GroupAdd: ctx.StringSlice("group-add"), + HealthCmd: ctx.StringSlice("health-cmd"), + HealthTimeout: ctx.Int64("health-timeout"), + HealthInterval: ctx.Int64("health-interval"), + HealthRetries: ctx.Int64("health-retries"), + Hostname: ctx.String("hostname"), + ImageUuid: "docker:" + ctx.Args()[0], + Ip: ctx.String("ip"), + Ip6: ctx.String("ip6"), + IpcMode: ctx.String("ipc"), + Isolation: ctx.String("isolation"), + KernelMemory: ctx.Int64("kernel-memory"), + Labels: map[string]string{}, + Environment: map[string]string{}, //LogConfig: Memory: ctx.Int64("memory"), MemoryReservation: ctx.Int64("memory-reservation"), @@ -340,30 +484,35 @@ func serviceRun(ctx *cli.Context) error { Privileged: ctx.Bool("privileged"), PublishAllPorts: ctx.Bool("publish-all"), ReadOnly: ctx.Bool("read-only"), - RunInit: ctx.Bool("init"), - SecurityOpt: ctx.StringSlice("security-opt"), - ShmSize: ctx.Int64("shm-size"), - StdinOpen: ctx.Bool("interactive"), - StopSignal: ctx.String("stop-signal"), - Tty: ctx.Bool("tty"), - User: ctx.String("user"), - Uts: ctx.String("uts"), - VolumeDriver: ctx.String("volume-driver"), - WorkingDir: ctx.String("workdir"), - DataVolumes: ctx.StringSlice("volume"), + //todo: add RunInit + //RunInit: ctx.Bool("init"), + SecurityOpt: ctx.StringSlice("security-opt"), + ShmSize: ctx.Int64("shm-size"), + StdinOpen: ctx.Bool("interactive"), + StopSignal: ctx.String("stop-signal"), + Tty: ctx.Bool("tty"), + User: ctx.String("user"), + Uts: ctx.String("uts"), + VolumeDriver: ctx.String("volume-driver"), + WorkingDir: ctx.String("workdir"), + DataVolumes: ctx.StringSlice("volume"), + } + if ctx.IsSet("it") { + container.StdinOpen = true + container.Tty = true } if ctx.String("log-driver") != "" || len(ctx.StringSlice("log-opt")) > 0 { - launchConfig.LogConfig = &client.LogConfig{ + container.LogConfig = &client.LogConfig{ Driver: ctx.String("log-driver"), - Config: map[string]interface{}{}, + Config: map[string]string{}, } for _, opt := range ctx.StringSlice("log-opt") { parts := strings.SplitN(opt, "=", 2) if len(parts) > 1 { - launchConfig.LogConfig.Config[parts[0]] = parts[1] + container.LogConfig.Config[parts[0]] = parts[1] } else { - launchConfig.LogConfig.Config[parts[0]] = "" + container.LogConfig.Config[parts[0]] = "" } } } @@ -374,7 +523,7 @@ func serviceRun(ctx *cli.Context) error { if len(parts) > 1 { value = parts[1] } - launchConfig.Labels[parts[0]] = value + container.Labels[parts[0]] = value } for _, env := range ctx.StringSlice("env") { @@ -391,40 +540,27 @@ func serviceRun(ctx *cli.Context) error { } else if len(parts) == 1 { value = os.Getenv(parts[0]) } - launchConfig.Environment[parts[0]] = value - } - - if ctx.Bool("schedule-global") { - launchConfig.Labels["io.rancher.scheduler.global"] = "true" + container.Environment[parts[0]] = value } if ctx.Bool("pull") { - launchConfig.Labels["io.rancher.container.pull_image"] = "always" + container.Labels["io.rancher.container.pull_image"] = "always" } args := ctx.Args()[1:] if len(args) > 0 { - launchConfig.Command = args + container.Command = args } - stack, name, err := ParseName(c, ctx.String("name")) + _, name, err := ParseName(c, ctx.String("name")) if err != nil { return err } - - service := &client.Service{ - Name: name, - StackId: stack.Id, - LaunchConfig: launchConfig, - StartOnCreate: true, - Scale: int64(ctx.Int("scale")), - } - - service, err = c.Service.Create(service) + container.Name = name + cont, err := c.Container.Create(container) if err != nil { return err } - - return WaitFor(ctx, service.Id) + return WaitFor(ctx, cont.Id) } diff --git a/cmd/scale.go b/cmd/scale.go index 4e6d0fcf..4c11f456 100644 --- a/cmd/scale.go +++ b/cmd/scale.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) diff --git a/cmd/secret.go b/cmd/secret.go index fd20d6b1..71a89ba0 100644 --- a/cmd/secret.go +++ b/cmd/secret.go @@ -8,7 +8,7 @@ import ( "os" "github.com/Sirupsen/logrus" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) diff --git a/cmd/ssh.go b/cmd/ssh.go index b48b2b20..9da17ef0 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -1,9 +1,7 @@ package cmd import ( - "archive/tar" "bytes" - "compress/gzip" "fmt" "io/ioutil" "net/http" @@ -12,7 +10,10 @@ import ( "path" "strings" - "github.com/rancher/go-rancher/v2" + "archive/zip" + + "github.com/pkg/errors" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -20,7 +21,7 @@ func SSHCommand() cli.Command { return cli.Command{ Name: "ssh", Usage: "SSH into host", - Description: "\nFor any hosts created through Rancher using docker-machine, you can SSH into the host. This is not supported for any custom hosts. If the host is not in the current $RANCHER_ENVIRONMENT, use `--env ` or `--env ` to select a different environment.\n\nExample:\n\t$ rancher ssh 1h1\n\t$ rancher --env 1a5 ssh 1h5\n", + Description: "\nFor any hosts created through Rancher using docker-machine, you can SSH into the host. This is not supported for any custom hosts. If the host is not in the current $RANCHER_ENVIRONMENT, use `--env ` or `--env ` to select a different environment.\n\nExample:\n\t$ rancher ssh root@1h1\n\t$ rancher --env 1a5 ssh ubuntu@1h5\n", ArgsUsage: "[HOSTID HOSTNAME...]", Action: hostSSH, Flags: []cli.Flag{}, @@ -68,6 +69,8 @@ func hostSSH(ctx *cli.Context) error { return err } + user := getDefaultSSHKey(*host) + key, err := getSSHKey(hostname, *host, config.AccessKey, config.SecretKey) if err != nil { return err @@ -77,15 +80,22 @@ func hostSSH(ctx *cli.Context) error { return fmt.Errorf("Failed to find IP for %s", hostname) } - return processExitCode(callSSH(key, host.AgentIpAddress, ctx.Args())) + return processExitCode(callSSH(key, host.AgentIpAddress, ctx.Args(), user)) } -func callSSH(content []byte, ip string, args []string) error { +func callSSH(content []byte, ip string, args []string, user string) error { for i, val := range args { if !strings.HasPrefix(val, "-") && len(val) > 0 { parts := strings.SplitN(val, "@", 2) - parts[len(parts)-1] = ip - args[i] = strings.Join(parts, "@") + if len(parts) == 2 { + parts[len(parts)-1] = ip + args[i] = strings.Join(parts, "@") + } else if len(parts) == 1 { + if user == "" { + return errors.New("Need to provide a ssh username") + } + args[i] = fmt.Sprintf("%s@%s", user, ip) + } break } } @@ -127,6 +137,7 @@ func getSSHKey(hostname string, host client.Host, accessKey, secretKey string) ( return nil, err } req.SetBasicAuth(accessKey, secretKey) + req.Header.Add("Accept-Encoding", "zip") resp, err := http.DefaultClient.Do(req) if err != nil { @@ -143,20 +154,33 @@ func getSSHKey(hostname string, host client.Host, accessKey, secretKey string) ( return nil, fmt.Errorf("%s", tarGz) } - gzipIn, err := gzip.NewReader(bytes.NewBuffer(tarGz)) + zipReader, err := zip.NewReader(bytes.NewReader(tarGz), resp.ContentLength) if err != nil { return nil, err } - tar := tar.NewReader(gzipIn) - for { - header, err := tar.Next() - if err != nil { - return nil, err - } - - if path.Base(header.Name) == "id_rsa" { - return ioutil.ReadAll(tar) + for _, file := range zipReader.File { + if path.Base(file.Name) == "id_rsa" { + r, err := file.Open() + if err != nil { + return nil, err + } + defer r.Close() + return ioutil.ReadAll(r) } } + return nil, errors.New("can't find private key file") +} + +func getDefaultSSHKey(host client.Host) string { + if host.Amazonec2Config != nil { + return host.Amazonec2Config.SshUser + } + if host.DigitaloceanConfig != nil { + return host.DigitaloceanConfig.SshUser + } + if host.Amazonec2Config != nil { + return host.Amazonec2Config.SshUser + } + return "" } diff --git a/cmd/stack.go b/cmd/stack.go index 2a3f9633..710dc641 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -1,13 +1,11 @@ package cmd import ( - "io/ioutil" - "os" - - "github.com/pkg/errors" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/rancher/rancher-compose-executor/lookup" "github.com/urfave/cli" + "io/ioutil" + "os" ) func StackCommand() cli.Command { @@ -107,7 +105,6 @@ func stackLs(ctx *cli.Context) error { {"STATE", "State"}, {"CATALOG", "Catalog"}, {"SERVICES", "ServiceCount"}, - {"SYSTEM", "Stack.System"}, {"DETAIL", "Stack.TransitioningMessage"}, }, ctx) @@ -164,26 +161,12 @@ func stackCreate(ctx *cli.Context) error { var lastErr error for _, name := range names { stack := &client.Stack{ - Name: name, - System: ctx.Bool("system"), - StartOnCreate: ctx.Bool("start"), + Name: name, } if !ctx.Bool("empty") { - var err error - stack.DockerCompose, err = getFile(ctx.String("docker-compose")) - if err != nil { - return err - } - if stack.DockerCompose == "" { - return errors.New("docker-compose.yml files is required") - } - - stack.RancherCompose, err = getFile(ctx.String("rancher-compose")) - if err != nil { - return errors.Wrap(err, "reading "+ctx.String("rancher-compose")) - } - + //var err error + // todo: revisit //stack.Answers, err = parseAnswers(ctx) //if err != nil { //return errors.Wrap(err, "reading answers") diff --git a/cmd/start.go b/cmd/start.go index 56c4c3ff..cc856cf3 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -3,7 +3,7 @@ package cmd import ( "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) diff --git a/cmd/stop.go b/cmd/stop.go index 6c29f303..fa3e0067 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -3,7 +3,7 @@ package cmd import ( "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) diff --git a/cmd/up.go b/cmd/up.go index 7af17b30..32bbb4cd 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -1,76 +1,379 @@ package cmd import ( - "github.com/rancher/rancher-compose-executor/app" - "github.com/rancher/rancher-compose-executor/project" + "bufio" + "fmt" + "io/ioutil" + "math/rand" + "os" + "path" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + dclient "github.com/docker/docker/client" + "github.com/fatih/color" + "github.com/pkg/errors" + "github.com/rancher/cli/monitor" + "github.com/rancher/go-rancher/v3" + "github.com/rancher/rancher-docker-api-proxy" "github.com/urfave/cli" + "golang.org/x/net/context" ) +var colors = []color.Attribute{color.FgGreen, color.FgBlack, color.FgBlue, color.FgCyan, color.FgMagenta, color.FgRed, color.FgWhite, color.FgYellow} + func UpCommand() cli.Command { - factory := &projectFactory{} - cmd := app.UpCommand(factory) - cmd.Flags = append(cmd.Flags, []cli.Flag{ - cli.StringFlag{ - Name: "rancher-file", - Usage: "Specify an alternate Rancher compose file (default: rancher-compose.yml)", + return cli.Command{ + Name: "up", + Usage: "Bring all services up", + Action: rancherUp, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "pull, p", + Usage: "Before doing the upgrade do an image pull on all hosts that have the image already", + }, + cli.BoolFlag{ + Name: "d", + Usage: "Do not block and log", + }, + cli.BoolFlag{ + Name: "render", + Usage: "Display processed Compose files and exit", + }, + cli.BoolFlag{ + Name: "upgrade, u, recreate", + Usage: "Upgrade if service has changed", + }, + cli.BoolFlag{ + Name: "force-upgrade, force-recreate", + Usage: "Upgrade regardless if service has changed", + }, + cli.BoolFlag{ + Name: "confirm-upgrade, c", + Usage: "Confirm that the upgrade was success and delete old containers", + }, + cli.BoolFlag{ + Name: "rollback, r", + Usage: "Rollback to the previous deployed version", + }, + cli.IntFlag{ + Name: "batch-size", + Usage: "Number of containers to upgrade at once", + Value: 2, + }, + cli.IntFlag{ + Name: "interval", + Usage: "Update interval in milliseconds", + Value: 1000, + }, + cli.StringFlag{ + Name: "rancher-file", + Usage: "Specify an alternate Rancher compose file (default: rancher-compose.yml)", + }, + cli.StringFlag{ + Name: "env-file,e", + Usage: "Specify a file from which to read environment variables", + }, + cli.StringSliceFlag{ + Name: "file,f", + Usage: "Specify one or more alternate compose files (default: docker-compose.yml)", + Value: &cli.StringSlice{}, + EnvVar: "COMPOSE_FILE", + }, + cli.StringFlag{ + Name: "stack,s", + Usage: "Specify an alternate project name (default: directory name)", + }, + cli.BoolFlag{ + Name: "prune", + Usage: "Prune services that doesn't exist on the current compose files", + }, }, - cli.StringFlag{ - Name: "env-file,e", - Usage: "Specify a file from which to read environment variables", - }, - cli.StringSliceFlag{ - Name: "file,f", - Usage: "Specify one or more alternate compose files (default: docker-compose.yml)", - Value: &cli.StringSlice{}, - EnvVar: "COMPOSE_FILE", - }, - cli.StringFlag{ - Name: "stack,s", - Usage: "Specify an alternate project name (default: directory name)", - }, - }...) - - cmd.Action = app.WithProject(factory, ProjectUp) - - return cmd + } } -func ProjectUp(p *project.Project, c *cli.Context) error { - w, err := NewWaiter(c) +func rancherUp(ctx *cli.Context) error { + rancherClient, err := GetClient(ctx) + if err != nil { + return err + } + // only look for --file or ./compose.yml + compose := "" + + composeFile := ctx.String("file") + if composeFile != "" { + composeFile = "compose.yml" + } + fp, err := filepath.Abs(composeFile) + if err != nil { + return errors.Wrapf(err, "failed to lookup current directory name") + } + file, err := os.Open(fp) + if err != nil { + return errors.Wrapf(err, "Can not find compose.yml") + } + defer file.Close() + buf, err := ioutil.ReadAll(file) + if err != nil { + return errors.Wrapf(err, "failed to read file") + } + compose = string(buf) + + //get stack name + stackName := "" + + if ctx.String("stack") != "" { + stackName = ctx.String("stack") + } else { + parent := path.Base(path.Dir(fp)) + if parent != "" && parent != "." { + stackName = parent + } else if wd, err := os.Getwd(); err != nil { + return err + } else { + stackName = path.Base(toUnixPath(wd)) + } + } + + stacks, err := rancherClient.Stack.List(&client.ListOpts{ + Filters: map[string]interface{}{ + "name": stackName, + "removed_null": nil, + }, + }) + if err != nil { + return errors.Wrap(err, "failed to list stacks") + } + + if ctx.Bool("rollback") { + if len(stacks.Data) == 0 { + return errors.Errorf("Can't find stack %v", stackName) + } + _, err := rancherClient.Stack.ActionRollback(&stacks.Data[0]) + if err != nil { + return errors.Errorf("failed to rollback stack %v", stackName) + } + return nil + } + if !ctx.Bool("d") { + watcher := monitor.NewUpWatcher(rancherClient) + watcher.Subscribe() + go func() { watcher.Start(stackName) }() + } + + if len(stacks.Data) > 0 { + // update stacks + stacks.Data[0].Templates = map[string]string{ + "compose.yml": compose, + } + prune := ctx.Bool("prune") + logrus.Info("Updating stack") + _, err := rancherClient.Stack.Update(&stacks.Data[0], client.Stack{ + Templates: map[string]string{ + "compose.yml": compose, + }, + Prune: prune, + }) + if err != nil { + return errors.Wrapf(err, "failed to update stack %v", stackName) + } + } else { + // create new stack + prune := ctx.Bool("prune") + _, err := rancherClient.Stack.Create(&client.Stack{ + Name: stackName, + Templates: map[string]string{ + "compose.yml": compose, + }, + Prune: prune, + }) + if err != nil { + return errors.Wrapf(err, "failed to create stack %v", stackName) + } + } + + if !ctx.Bool("d") { + for { + stack, err := getStack(rancherClient, stackName) + if err != nil { + return err + } + if len(stack.ServiceIds) != 0 { + instanceIds := map[string]struct{}{} + services, err := getServices(rancherClient, stack.ServiceIds) + if err != nil { + return err + } + for _, service := range services { + if service.Transitioning != "no" { + logrus.Debugf("Service [%v] is not fully up", service.Name) + time.Sleep(time.Second) + continue + } + for _, instanceID := range service.InstanceIds { + instanceIds[instanceID] = struct{}{} + } + } + if err := getLogs(rancherClient, instanceIds); err != nil { + return errors.Wrapf(err, "failed to get container logs") + } + } + time.Sleep(time.Second) + } + } + + return nil +} + +func toUnixPath(p string) string { + return strings.Replace(p, "\\", "/", -1) +} + +func getStack(c *client.RancherClient, stackName string) (client.Stack, error) { + stacks, err := c.Stack.List(&client.ListOpts{ + Filters: map[string]interface{}{ + "name": stackName, + "removed_null": nil, + }, + }) + if err != nil { + return client.Stack{}, errors.Wrap(err, "failed to list stacks") + } + if len(stacks.Data) > 0 { + return stacks.Data[0], nil + } + return client.Stack{}, errors.Errorf("Failed to find stacks with name %v", stackName) +} + +func getServices(c *client.RancherClient, serviceIds []string) ([]client.Service, error) { + services := []client.Service{} + for _, serviceID := range serviceIds { + service, err := c.Service.ById(serviceID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get service (id: [%v])", serviceID) + } + services = append(services, *service) + } + return services, nil +} + +func getLogs(c *client.RancherClient, instanceIds map[string]struct{}) error { + wg := sync.WaitGroup{} + instances := []client.Instance{} + for instanceID := range instanceIds { + instance, err := c.Instance.ById(instanceID) + if err != nil { + return errors.Wrapf(err, "failed to get instance id [%v]", instanceID) + } + instances = append(instances, *instance) + } + listenSocks := map[string]*dclient.Client{} + for _, i := range instances { + if i.ExternalId == "" || i.HostId == "" { + continue + } + + if dockerClient, ok := listenSocks[i.HostId]; ok { + wg.Add(1) + go func(dockerClient *dclient.Client, i client.Instance) { + if err := log(i, dockerClient); err != nil { + logrus.Error(err) + } + wg.Done() + }(dockerClient, i) + continue + } + + resource, err := Lookup(c, i.HostId, "host") + if err != nil { + return err + } + + host, err := c.Host.ById(resource.Id) + if err != nil { + return err + } + + state := getHostState(host) + if state != "active" && state != "inactive" { + logrus.Errorf("Can not contact host %s in state %s", i.HostId, state) + continue + } + + tempfile, err := ioutil.TempFile("", "docker-sock") + if err != nil { + return err + } + defer os.Remove(tempfile.Name()) + + if err := tempfile.Close(); err != nil { + return err + } + + dockerHost := "unix://" + tempfile.Name() + proxy := dockerapiproxy.NewProxy(c, host.Id, dockerHost) + if err := proxy.Listen(); err != nil { + return err + } + + go func() { + logrus.Fatal(proxy.Serve()) + }() + + dockerClient, err := dclient.NewClient(dockerHost, "", nil, nil) + if err != nil { + logrus.Errorf("Failed to connect to host %s: %v", i.HostId, err) + continue + } + + listenSocks[i.HostId] = dockerClient + + wg.Add(1) + go func(dockerClient *dclient.Client, i client.Instance) { + if err := log(i, dockerClient); err != nil { + logrus.Error(err) + } + wg.Done() + }(dockerClient, i) + } + wg.Wait() + return nil +} + +func log(instance client.Instance, dockerClient *dclient.Client) error { + c, err := dockerClient.ContainerInspect(context.Background(), instance.ExternalId) if err != nil { return err } - return app.ProjectUpAndWait(p, w, c) -} - -type projectFactory struct { -} - -func (p *projectFactory) Create(c *cli.Context) (*project.Project, error) { - config, err := lookupConfig(c) + options := types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + Tail: "10", + } + responseBody, err := dockerClient.ContainerLogs(context.Background(), c.ID, options) if err != nil { - return nil, err + return err } + defer responseBody.Close() - url, err := config.EnvironmentURL() - if err != nil { - return nil, err + scanner := bufio.NewScanner(responseBody) + cl := getRandomColor() + for scanner.Scan() { + text := fmt.Sprintf("[%v]: %v\n", instance.Name, scanner.Text()) + color.New(cl).Fprint(os.Stdout, text) } - - // from config - c.GlobalSet("url", url) - c.GlobalSet("access-key", config.AccessKey) - c.GlobalSet("secret-key", config.SecretKey) - - // copy from flags - c.GlobalSet("rancher-file", c.String("rancher-file")) - c.GlobalSet("env-file", c.String("env-file")) - c.GlobalSet("project-name", c.String("stack")) - for _, f := range c.StringSlice("file") { - c.GlobalSet("file", f) - } - - factory := &app.RancherProjectFactory{} - return factory.Create(c) + return nil +} + +func getRandomColor() color.Attribute { + s1 := rand.NewSource(time.Now().UnixNano()) + r1 := rand.New(s1) + index := r1.Intn(8) + return colors[index] } diff --git a/cmd/util_actions.go b/cmd/util_actions.go index 0ce26dd9..e9da45ed 100644 --- a/cmd/util_actions.go +++ b/cmd/util_actions.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" ) func pickAction(resource *client.Resource, actions ...string) (string, error) { diff --git a/cmd/util_foreach.go b/cmd/util_foreach.go index 7595a1f2..53693a27 100644 --- a/cmd/util_foreach.go +++ b/cmd/util_foreach.go @@ -3,7 +3,7 @@ package cmd import ( "fmt" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -23,6 +23,7 @@ func forEachResourceWithClient(c *client.RancherClient, ctx *cli.Context, types } var lastErr error + fmt.Println(ctx.Args()) for _, id := range ctx.Args() { resource, err := Lookup(c, id, types...) if err != nil { diff --git a/cmd/util_ls.go b/cmd/util_ls.go index 06a9d793..cb1ac468 100644 --- a/cmd/util_ls.go +++ b/cmd/util_ls.go @@ -1,7 +1,7 @@ package cmd import ( - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) diff --git a/cmd/volume.go b/cmd/volume.go index 3e916ef8..0b1abe1b 100644 --- a/cmd/volume.go +++ b/cmd/volume.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/Sirupsen/logrus" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/urfave/cli" ) @@ -135,7 +135,7 @@ func volumeCreate(ctx *cli.Context) error { newVol := &client.Volume{ Name: ctx.Args()[0], Driver: ctx.String("driver"), - DriverOpts: map[string]interface{}{}, + DriverOpts: map[string]string{}, } for _, arg := range ctx.StringSlice("opt") { diff --git a/cmd/wait.go b/cmd/wait.go index e884da01..6fe8d26a 100644 --- a/cmd/wait.go +++ b/cmd/wait.go @@ -7,7 +7,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/rancher/cli/monitor" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" "github.com/rancher/rancher-compose-executor/project/options" "github.com/urfave/cli" ) diff --git a/main.go b/main.go index 2e245321..0cfdcfc4 100644 --- a/main.go +++ b/main.go @@ -3,8 +3,13 @@ package main import ( "os" + "regexp" + "strings" + "github.com/Sirupsen/logrus" + "github.com/pkg/errors" "github.com/rancher/cli/cmd" + "github.com/rancher/cli/rancher_prompt" "github.com/urfave/cli" ) @@ -132,6 +137,8 @@ func mainErr() error { cmd.HostCommand(), cmd.LogsCommand(), cmd.PsCommand(), + cmd.PullCommand(), + cmd.PromptCommand(), cmd.RestartCommand(), cmd.RmCommand(), cmd.RunCommand(), @@ -146,6 +153,42 @@ func mainErr() error { cmd.InspectCommand(), cmd.WaitCommand(), } + for _, com := range app.Commands { + rancherPrompt.Commands[com.Name] = com + rancherPrompt.Commands[com.ShortName] = com + } + rancherPrompt.Flags = app.Flags + parsed, err := parseArgs(os.Args) + if err != nil { + logrus.Error(err) + os.Exit(1) + } - return app.Run(os.Args) + return app.Run(parsed) +} + +var singleAlphaLetterRegxp = regexp.MustCompile("[a-zA-Z]") + +func parseArgs(args []string) ([]string, error) { + result := []string{} + for _, arg := range args { + if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") && len(arg) > 1 { + for i, c := range arg[1:] { + if string(c) == "=" { + if i < 1 { + return nil, errors.New("invalid input with '-' and '=' flag") + } + result[len(result)-1] = result[len(result)-1] + arg[i+1:] + break + } else if singleAlphaLetterRegxp.MatchString(string(c)) { + result = append(result, "-"+string(c)) + } else { + return nil, errors.Errorf("invalid input %v in flag", string(c)) + } + } + } else { + result = append(result, arg) + } + } + return result, nil } diff --git a/main_test.go b/main_test.go new file mode 100644 index 00000000..633385e1 --- /dev/null +++ b/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "gopkg.in/check.v1" + "testing" +) + +// Hook up gocheck into the "go test" runner. +func Test(t *testing.T) { + check.TestingT(t) +} + +type MainTestSuite struct { +} + +var _ = check.Suite(&MainTestSuite{}) + +func (m *MainTestSuite) SetUpSuite(c *check.C) { +} + +func (m *MainTestSuite) TestParseArgs(c *check.C) { + input := [][]string{ + {"rancher", "run", "--debug", "-itd"}, + {"rancher", "run", "--debug", "-itf=b"}, + {"rancher", "run", "--debug", "-itd#"}, + {"rancher", "run", "--debug", "-f=b"}, + {"rancher", "run", "--debug", "-=b"}, + {"rancher", "run", "--debug", "-"}, + } + r0, err := parseArgs(input[0]) + if err != nil { + c.Fatal(err) + } + c.Assert(r0, check.DeepEquals, []string{"rancher", "run", "--debug", "-i", "-t", "-d"}) + + r1, err := parseArgs(input[1]) + if err != nil { + c.Fatal(err) + } + c.Assert(r1, check.DeepEquals, []string{"rancher", "run", "--debug", "-i", "-t", "-f=b"}) + + _, err = parseArgs(input[2]) + if err == nil { + c.Fatal("should raise error") + } + + r3, err := parseArgs(input[3]) + if err != nil { + c.Fatal(err) + } + c.Assert(r3, check.DeepEquals, []string{"rancher", "run", "--debug", "-f=b"}) + + _, err = parseArgs(input[4]) + if err == nil { + c.Fatal("should raise error") + } + + r5, err := parseArgs(input[5]) + if err != nil { + c.Fatal(err) + } + c.Assert(r5, check.DeepEquals, []string{"rancher", "run", "--debug", "-"}) +} diff --git a/monitor/monitor.go b/monitor/monitor.go index 9f6656e7..e2f4f11b 100644 --- a/monitor/monitor.go +++ b/monitor/monitor.go @@ -10,7 +10,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/gorilla/websocket" "github.com/patrickmn/go-cache" - "github.com/rancher/go-rancher/v2" + "github.com/rancher/go-rancher/v3" ) type Event struct { @@ -151,7 +151,7 @@ func (m *Monitor) watch(conn *websocket.Conn) error { continue } - logrus.Debugf("Event: %s %s %s", v.Name, v.ResourceType, v.ResourceID) + logrus.Debugf("Event: %s %s %s %v", v.Name, v.ResourceType, v.ResourceID, v.Data) m.put(v.ResourceType, v.ResourceID, &v) } } diff --git a/monitor/up_watcher.go b/monitor/up_watcher.go new file mode 100644 index 00000000..915132a2 --- /dev/null +++ b/monitor/up_watcher.go @@ -0,0 +1,170 @@ +package monitor + +import ( + "encoding/json" + "fmt" + "net/url" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/gorilla/websocket" + "github.com/pkg/errors" + "github.com/rancher/go-rancher/v3" +) + +type UpWatcher struct { + sync.Mutex + c *client.RancherClient + subCounter int + subscriptions map[int]*Subscription +} + +func (m *UpWatcher) Subscribe() *Subscription { + m.Lock() + defer m.Unlock() + + m.subCounter++ + sub := &Subscription{ + id: m.subCounter, + C: make(chan *Event, 1024), + } + m.subscriptions[sub.id] = sub + + return sub +} + +func (m *UpWatcher) Unsubscribe(sub *Subscription) { + m.Lock() + defer m.Unlock() + + close(sub.C) + delete(m.subscriptions, sub.id) +} + +func NewUpWatcher(c *client.RancherClient) *UpWatcher { + return &UpWatcher{ + c: c, + subscriptions: map[int]*Subscription{}, + } +} + +func (m *UpWatcher) Start(stackName string) error { + schema, ok := m.c.GetSchemas().CheckSchema("subscribe") + if !ok { + return fmt.Errorf("Not authorized to subscribe") + } + + urlString := schema.Links["collection"] + u, err := url.Parse(urlString) + if err != nil { + return err + } + + switch u.Scheme { + case "http": + u.Scheme = "ws" + case "https": + u.Scheme = "wss" + } + + q := u.Query() + q.Add("eventNames", "resource.change") + q.Add("eventNames", "service.kubernetes.change") + + u.RawQuery = q.Encode() + + conn, resp, err := m.c.Websocket(u.String(), nil) + if err != nil { + return err + } + + if resp.StatusCode != 101 { + return fmt.Errorf("Bad status code: %d %s", resp.StatusCode, resp.Status) + } + + logrus.Debugf("Connected to: %s", u.String()) + + return m.watch(conn, stackName) +} + +func (m *UpWatcher) watch(conn *websocket.Conn, stackName string) error { + stackID := "" + serviceIds := map[string]struct{}{} + lastStackMsg := "" + lastServiceMsg := "" + lastContainerMsg := "" + for { + v := Event{} + _, r, err := conn.NextReader() + if err != nil { + return err + } + if err := json.NewDecoder(r).Decode(&v); err != nil { + logrus.Errorf("Failed to parse json in message") + continue + } + + logrus.Debugf("Event: %s %s %s", v.Name, v.ResourceType, v.ResourceID) + if v.ResourceType == "stack" { + stackData := &client.Stack{} + if err := unmarshalling(v.Data["resource"], stackData); err != nil { + logrus.Errorf("failed to unmarshalling err: %v", err) + } + if stackData.Name == stackName { + stackID = stackData.Id + for _, serviceID := range stackData.ServiceIds { + serviceIds[serviceID] = struct{}{} + } + switch stackData.Transitioning { + case "yes": + msg := fmt.Sprintf("Stack [%v]: %s", stackData.Name, stackData.TransitioningMessage) + if msg != lastStackMsg { + logrus.Info(msg) + } + lastStackMsg = msg + } + } + } else if v.ResourceType == "scalingGroup" { + serviceData := &client.Service{} + if err := unmarshalling(v.Data["resource"], serviceData); err != nil { + logrus.Errorf("failed to unmarshalling err: %v", err) + } + if serviceData.StackId == stackID { + switch serviceData.Transitioning { + case "yes": + msg := fmt.Sprintf("Service [%v]: %s", serviceData.Name, serviceData.TransitioningMessage) + if msg != lastServiceMsg { + logrus.Info(msg) + } + lastServiceMsg = msg + } + } + } else if v.ResourceType == "container" { + containerData := &client.Container{} + if err := unmarshalling(v.Data["resource"], containerData); err != nil { + logrus.Errorf("failed to unmarshalling err: %v", err) + } + if containerData.StackId == stackID { + switch containerData.Transitioning { + case "yes": + msg := fmt.Sprintf("Container [%v]: %s", containerData.Name, containerData.TransitioningMessage) + if msg != lastContainerMsg { + logrus.Info(msg) + } + lastContainerMsg = msg + } + } + } + } +} + +func unmarshalling(data interface{}, v interface{}) error { + raw, err := json.Marshal(data) + if err != nil { + return errors.Wrapf(err, "failed to marshall object. Body: %v", data) + } + if err := json.Unmarshal(raw, &v); err != nil { + return errors.Wrapf(err, "failed to unmarshall object. Body: %v", string(raw)) + } + return nil +} diff --git a/rancher_prompt/client.go b/rancher_prompt/client.go new file mode 100644 index 00000000..7bf5afab --- /dev/null +++ b/rancher_prompt/client.go @@ -0,0 +1 @@ +package rancherPrompt diff --git a/rancher_prompt/completer.go b/rancher_prompt/completer.go new file mode 100644 index 00000000..3346d40e --- /dev/null +++ b/rancher_prompt/completer.go @@ -0,0 +1,120 @@ +package rancherPrompt + +import ( + "strings" + + "github.com/c-bata/go-prompt" + "github.com/urfave/cli" +) + +// thanks for the idea from github.com/c-bata/kube-prompt + +var ( + Commands = map[string]cli.Command{} + Flags = []cli.Flag{} +) + +func Completer(d prompt.Document) []prompt.Suggest { + if d.TextBeforeCursor() == "" { + return []prompt.Suggest{} + } + + args := strings.Split(d.TextBeforeCursor(), " ") + w := d.GetWordBeforeCursor() + + // If PIPE is in text before the cursor, returns empty suggestions. + for i := range args { + if args[i] == "|" { + return []prompt.Suggest{} + } + } + + // If word before the cursor starts with "-", returns CLI flag options. + if strings.HasPrefix(w, "-") { + return optionCompleter(args, strings.HasPrefix(w, "--")) + } + + return argumentsCompleter(excludeOptions(args)) +} + +func argumentsCompleter(args []string) []prompt.Suggest { + suggests := []prompt.Suggest{} + for name, command := range Commands { + if command.Name != "prompt" { + suggests = append(suggests, prompt.Suggest{ + Text: name, + Description: command.Usage, + }) + } + } + if len(args) <= 1 { + return prompt.FilterHasPrefix(suggests, args[0], true) + } + + switch args[0] { + case "docker": + if len(args) == 3 { + subcommands := []prompt.Suggest{ + {Text: "attach", Description: "Attach local standard input, output, and error streams to a running container"}, + {Text: "build", Description: "Build an image from a Dockerfile"}, + {Text: "commit", Description: "Create a new image from a container’s changes"}, + {Text: "cp", Description: "Copy files/folders between a container and the local filesystem"}, + {Text: "create", Description: "Create a new container"}, + {Text: "events", Description: "Get real time events from the server"}, + {Text: "exec", Description: "Run a command in a running container"}, + {Text: "export", Description: "Export a container’s filesystem as a tar archive"}, + {Text: "image", Description: "Manage images"}, + {Text: "images", Description: "List images"}, + {Text: "import", Description: "Import the contents from a tarball to create a filesystem image"}, + {Text: "info", Description: "Display system-wide information"}, + {Text: "inspect", Description: "Return low-level information on Docker objects"}, + {Text: "kill", Description: "Kill one or more running containers"}, + {Text: "load", Description: "Load an image from a tar archive or STDIN"}, + {Text: "login", Description: "Log in to a Docker registry"}, + {Text: "logout", Description: "Log out from a Docker registry"}, + {Text: "logs", Description: "Fetch the logs of a container"}, + {Text: "network", Description: "Manage networks"}, + {Text: "pause", Description: "Pause all processes within one or more containers"}, + {Text: "plugin", Description: "Manage plugins"}, + {Text: "port", Description: "List port mappings or a specific mapping for the container"}, + {Text: "ps", Description: "List containers"}, + {Text: "pull", Description: "Pull an image or a repository from a registry"}, + {Text: "push", Description: "Push an image or a repository to a registry"}, + {Text: "rename", Description: "Rename a container"}, + {Text: "restart", Description: "Restart one or more containers"}, + {Text: "rm", Description: "Remove one or more containers"}, + {Text: "rmi", Description: "Remove one or more images"}, + {Text: "run", Description: "Run a command in a new container"}, + {Text: "save", Description: "Save one or more images to a tar archive (streamed to STDOUT by default)"}, + {Text: "search", Description: "Search the Docker Hub for images"}, + {Text: "start", Description: "Start one or more stopped containers"}, + {Text: "stats", Description: "Display a live stream of container(s) resource usage statistics"}, + {Text: "stop", Description: "Stop one or more running containers"}, + {Text: "tag", Description: "Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE"}, + {Text: "top", Description: "Display the running processes of a container"}, + {Text: "unpause", Description: "Unpause all processes within one or more containers"}, + {Text: "update", Description: "Update configuration of one or more containers"}, + {Text: "version", Description: "Show the Docker version information"}, + {Text: "volume", Description: "Manage volumes"}, + {Text: "wait", Description: "Block until one or more containers stop, then print their exit codes"}, + } + return prompt.FilterHasPrefix(subcommands, args[2], true) + } + default: + if len(args) == 2 { + return prompt.FilterHasPrefix(getSubcommandSuggest(args[0]), args[1], true) + } + } + return []prompt.Suggest{} +} + +func getSubcommandSuggest(name string) []prompt.Suggest { + subcommands := []prompt.Suggest{} + for _, com := range Commands[name].Subcommands { + subcommands = append(subcommands, prompt.Suggest{ + Text: com.Name, + Description: com.Usage, + }) + } + return subcommands +} diff --git a/rancher_prompt/executor.go b/rancher_prompt/executor.go new file mode 100644 index 00000000..57b68220 --- /dev/null +++ b/rancher_prompt/executor.go @@ -0,0 +1,40 @@ +package rancherPrompt + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func Executor(s string) { + s = strings.TrimSpace(s) + if s == "" { + return + } + if s == "exit" { + os.Exit(0) + return + } + //hack for rancher docker + // docker --host 1h1 ps -> --host 1h1 docker ps + if strings.HasPrefix(s, "docker ") { + parts := strings.Split(s, " ") + if len(parts) > 2 && (parts[1] == "--host" || parts[1] == "-host") { + t := parts[0] + parts[0] = parts[1] + parts[1] = parts[2] + parts[2] = t + s = strings.Join(parts, " ") + } + } + + cmd := exec.Command("/bin/sh", "-c", "rancher "+s) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Printf("Got error: %s\n", err.Error()) + } + return +} diff --git a/rancher_prompt/options.go b/rancher_prompt/options.go new file mode 100644 index 00000000..8c81c1a2 --- /dev/null +++ b/rancher_prompt/options.go @@ -0,0 +1,117 @@ +package rancherPrompt + +import ( + "strings" + + "github.com/c-bata/go-prompt" + "github.com/urfave/cli" +) + +func optionCompleter(args []string, long bool) []prompt.Suggest { + l := len(args) + if l <= 1 { + if long { + return prompt.FilterHasPrefix(optionHelp, "--", false) + } + return optionHelp + } + flagGlobal := getGlobalFlag() + + var suggests []prompt.Suggest + commandArgs := excludeOptions(args) + + if command, ok := Commands[commandArgs[0]]; ok { + if len(commandArgs) > 1 && len(command.Subcommands) > 0 { + for _, sub := range command.Subcommands { + if sub.Name == commandArgs[1] { + suggests = append(getFlagsSuggests(sub), flagGlobal...) + break + } + } + } else { + suggests = append(getFlagsSuggests(command), flagGlobal...) + } + } + + if long { + return prompt.FilterContains( + prompt.FilterHasPrefix(suggests, "--", false), + strings.TrimLeft(args[l-1], "--"), + true, + ) + } + return prompt.FilterHasPrefix(suggests, strings.TrimLeft(args[l-1], "-"), true) +} + +var optionHelp = []prompt.Suggest{ + {Text: "-h", Description: "Help Commmand"}, + {Text: "--help", Description: "Help Commmand"}, +} + +func excludeOptions(args []string) []string { + ret := make([]string, 0, len(args)) + for i := range args { + if !strings.HasPrefix(args[i], "-") { + ret = append(ret, args[i]) + } + } + return ret +} + +func getGlobalFlag() []prompt.Suggest { + suggests := []prompt.Suggest{} + for _, flag := range Flags { + name := flag.GetName() + parts := strings.Split(name, ",") + for _, part := range parts { + prefix := "--" + if len(parts) == 1 { + prefix = "-" + } + suggests = append(suggests, prompt.Suggest{ + Text: prefix + strings.TrimSpace(part), + Description: getUsageForFlag(flag), + }) + } + } + suggests = append(suggests, optionHelp...) + return suggests +} + +func getFlagsSuggests(command cli.Command) []prompt.Suggest { + suggests := []prompt.Suggest{} + for _, f := range command.Flags { + name := f.GetName() + parts := strings.Split(name, ",") + for _, part := range parts { + prefix := "--" + if len(parts) == 1 { + prefix = "-" + } + suggests = append(suggests, prompt.Suggest{ + Text: prefix + strings.TrimSpace(part), + Description: getUsageForFlag(f), + }) + } + } + return suggests +} + +func getUsageForFlag(flag cli.Flag) string { + if v, ok := flag.(cli.StringFlag); ok { + return v.Usage + } + if v, ok := flag.(cli.StringSliceFlag); ok { + return v.Usage + } + if v, ok := flag.(cli.IntFlag); ok { + return v.Usage + } + if v, ok := flag.(cli.IntSliceFlag); ok { + return v.Usage + } + if v, ok := flag.(cli.BoolFlag); ok { + return v.Usage + } + return "" +}