1
0
mirror of https://github.com/rancher/cli.git synced 2026-02-05 09:48:36 +01:00
This commit is contained in:
Daishan Peng
2017-09-15 14:29:06 -07:00
parent e0e6102b28
commit 03ebe1a203
39 changed files with 1502 additions and 478 deletions

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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"
)

View File

@@ -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)
}

View File

@@ -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"
)

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -3,7 +3,7 @@ package cmd
import (
"strings"
"github.com/rancher/go-rancher/v2"
"github.com/rancher/go-rancher/v3"
"github.com/urfave/cli"
)

View File

@@ -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"
)

34
cmd/prompt.go Normal file
View File

@@ -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
}

View File

@@ -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)

72
cmd/pull.go Normal file
View File

@@ -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
}
}
}

View File

@@ -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
})
}

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/rancher/go-rancher/v2"
"github.com/rancher/go-rancher/v3"
"github.com/urfave/cli"
)

View File

@@ -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)
}

View File

@@ -5,7 +5,7 @@ import (
"strconv"
"strings"
"github.com/rancher/go-rancher/v2"
"github.com/rancher/go-rancher/v3"
"github.com/urfave/cli"
)

View File

@@ -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"
)

View File

@@ -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 <envID>` or `--env <envName>` 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 <envID>` or `--env <envName>` 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 ""
}

View File

@@ -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")

View File

@@ -3,7 +3,7 @@ package cmd
import (
"strings"
"github.com/rancher/go-rancher/v2"
"github.com/rancher/go-rancher/v3"
"github.com/urfave/cli"
)

View File

@@ -3,7 +3,7 @@ package cmd
import (
"strings"
"github.com/rancher/go-rancher/v2"
"github.com/rancher/go-rancher/v3"
"github.com/urfave/cli"
)

417
cmd/up.go
View File

@@ -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]
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -1,7 +1,7 @@
package cmd
import (
"github.com/rancher/go-rancher/v2"
"github.com/rancher/go-rancher/v3"
"github.com/urfave/cli"
)

View File

@@ -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") {

View File

@@ -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"
)

45
main.go
View File

@@ -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
}

63
main_test.go Normal file
View File

@@ -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", "-"})
}

View File

@@ -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)
}
}

170
monitor/up_watcher.go Normal file
View File

@@ -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
}

1
rancher_prompt/client.go Normal file
View File

@@ -0,0 +1 @@
package rancherPrompt

120
rancher_prompt/completer.go Normal file
View File

@@ -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 containers 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 containers 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
}

View File

@@ -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
}

117
rancher_prompt/options.go Normal file
View File

@@ -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 ""
}