424 lines
13 KiB
Go
424 lines
13 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/hyperhq/hyper-api/types"
|
|
"github.com/hyperhq/hyper-api/types/filters"
|
|
"github.com/hyperhq/hyper-api/types/strslice"
|
|
Cli "github.com/hyperhq/hypercli/cli"
|
|
ropts "github.com/hyperhq/hypercli/opts"
|
|
flag "github.com/hyperhq/hypercli/pkg/mflag"
|
|
"github.com/hyperhq/hypercli/pkg/signal"
|
|
"github.com/hyperhq/hypercli/runconfig/opts"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// CmdService is the parent subcommand for all service commands
|
|
//
|
|
// Usage: docker service <COMMAND> [OPTIONS]
|
|
func (cli *DockerCli) CmdService(args ...string) error {
|
|
cmd := Cli.Subcmd("service", []string{"COMMAND [OPTIONS]"}, serviceUsage(), false)
|
|
cmd.Require(flag.Min, 1)
|
|
err := cmd.ParseFlags(args, true)
|
|
cmd.Usage()
|
|
return err
|
|
}
|
|
|
|
// CmdServiceCreate creates a new service with a given name
|
|
//
|
|
// Usage: hyper service create [OPTIONS] COUNT
|
|
func (cli *DockerCli) CmdServiceCreate(args ...string) error {
|
|
cmd := Cli.Subcmd("service create", []string{"IMAGE"}, "Create a new service", false)
|
|
var (
|
|
flSecurityGroups = ropts.NewListOpts(nil)
|
|
flEnv = ropts.NewListOpts(opts.ValidateEnv)
|
|
flLabels = ropts.NewListOpts(opts.ValidateEnv)
|
|
flEnvFile = ropts.NewListOpts(nil)
|
|
flVolumes = ropts.NewListOpts(nil)
|
|
flLabelsFile = ropts.NewListOpts(nil)
|
|
|
|
flName = cmd.String([]string{"-name"}, "", "Service name")
|
|
flStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached")
|
|
flTty = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY")
|
|
flEntrypoint = cmd.String([]string{"-entrypoint"}, "", "Overwrite the default ENTRYPOINT of the image")
|
|
flNetMode = cmd.String([]string{}, "bridge", "Connect containers to a network, only bridge is supported now")
|
|
flStopSignal = cmd.String([]string{"-stop-signal"}, signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal))
|
|
flContainerSize = cmd.String([]string{"-size"}, "s4", "The size of service containers (e.g. s1, s2, s3, s4, m1, m2, m3, l1, l2, l3)")
|
|
flWorkingDir = cmd.String([]string{"w", "-workdir"}, "", "Working directory inside the container")
|
|
flSSLCert = cmd.String([]string{"-ssl-cert"}, "", "SSL cert file for httpsTerm service")
|
|
flServicePort = cmd.Int([]string{"-service-port"}, 0, "Publish port of the service")
|
|
flContainerPort = cmd.Int([]string{"-container-port"}, 0, "Container port of the service, default same with service port")
|
|
flReplicas = cmd.Int([]string{"-replicas"}, -1, "Number of containers belonging to this service")
|
|
flHealthCheckInterval = cmd.Int([]string{"-health-check-interval"}, 3, "Interval in seconds for health checking the containers")
|
|
flHealthCheckFall = cmd.Int([]string{"-health-check-fall"}, 3, "Number of consecutive valid health checks before considering the server as DOWN")
|
|
flHealthCheckRise = cmd.Int([]string{"-health-check-rise"}, 2, "Number of consecutive valid health checks before considering the server as UP")
|
|
flSessionAffinity = cmd.Bool([]string{"-session-affinity"}, false, "Whether the service uses sticky sessions")
|
|
flAlgorithm = cmd.String([]string{"-algorithm"}, types.LBAlgorithmRoundRobin, "Algorithm of the service (e.g. roundrobin, leastconn, source)")
|
|
flProtocol = cmd.String([]string{"-protocol"}, types.LBProtocolTCP, "Protocol of the service (e.g. http, https, tcp, httpsTerm).")
|
|
)
|
|
cmd.Var(&flLabels, []string{"l", "-label"}, "Set meta data on a container")
|
|
cmd.Var(&flLabelsFile, []string{"-label-file"}, "Read in a line delimited file of labels")
|
|
cmd.Var(&flEnv, []string{"e", "-env"}, "Set environment variables")
|
|
cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a file of environment variables")
|
|
cmd.Var(&flSecurityGroups, []string{"-sg"}, "Security group for each container")
|
|
cmd.Var(&flVolumes, []string{"v", "--volume"}, "Volume for each container")
|
|
|
|
cmd.Require(flag.Exact, 1)
|
|
err := cmd.ParseFlags(args, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if *flReplicas <= 0 {
|
|
return fmt.Errorf("replicas must be bigger than 0")
|
|
}
|
|
|
|
var binds = map[string]struct{}{}
|
|
// add any bind targets to the list of container services
|
|
for bind := range flVolumes.GetMap() {
|
|
binds[bind] = struct{}{}
|
|
}
|
|
var (
|
|
parsedArgs = cmd.Args()
|
|
runCmd strslice.StrSlice
|
|
entrypoint strslice.StrSlice
|
|
image = cmd.Arg(0)
|
|
)
|
|
|
|
if _, _, err = cli.client.ImageInspectWithRaw(context.Background(), image, false); err != nil && strings.Contains(err.Error(), "No such image") {
|
|
if err := cli.pullImage(context.Background(), image); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(parsedArgs) > 1 {
|
|
runCmd = strslice.StrSlice(parsedArgs[1:])
|
|
}
|
|
if *flEntrypoint != "" {
|
|
entrypoint = strslice.StrSlice{*flEntrypoint}
|
|
}
|
|
// collect all the environment variables for the container
|
|
envVariables, err := opts.ReadKVStrings(flEnvFile.GetAll(), flEnv.GetAll())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// collect all the labels for the container
|
|
labels, err := opts.ReadKVStrings(flLabelsFile.GetAll(), flLabels.GetAll())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var sgs = map[string]struct{}{}
|
|
for sg := range flSecurityGroups.GetMap() {
|
|
sgs[sg] = struct{}{}
|
|
}
|
|
|
|
sslData := []byte{}
|
|
if *flSSLCert != "" {
|
|
sslData, err = ioutil.ReadFile(*flSSLCert)
|
|
}
|
|
|
|
sv := types.Service{
|
|
Name: *flName,
|
|
Image: image,
|
|
WorkingDir: *flWorkingDir,
|
|
ContainerSize: *flContainerSize,
|
|
ServicePort: *flServicePort,
|
|
ContainerPort: *flContainerPort,
|
|
Replicas: *flReplicas,
|
|
Entrypoint: entrypoint,
|
|
Cmd: runCmd,
|
|
Env: envVariables,
|
|
Volumes: binds,
|
|
Labels: opts.ConvertKVStringsToMap(labels),
|
|
SecurityGroups: sgs,
|
|
Tty: *flTty,
|
|
Stdin: *flStdin,
|
|
NetMode: *flNetMode,
|
|
StopSignal: *flStopSignal,
|
|
HealthCheckInterval: *flHealthCheckInterval,
|
|
HealthCheckFall: *flHealthCheckFall,
|
|
HealthCheckRise: *flHealthCheckRise,
|
|
Algorithm: *flAlgorithm,
|
|
Protocol: *flProtocol,
|
|
SessionAffinity: *flSessionAffinity,
|
|
SSLCert: string(sslData),
|
|
}
|
|
|
|
service, err := cli.client.ServiceCreate(context.Background(), sv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cli.out, "Service %s is created.\n", service.Name)
|
|
return nil
|
|
}
|
|
|
|
// CmdServiceDelete deletes one or more services
|
|
//
|
|
// Usage: hyper service rm service [service...]
|
|
func (cli *DockerCli) CmdServiceRm(args ...string) error {
|
|
cmd := Cli.Subcmd("service rm", []string{"service [service...]"}, "Remove one or more services", false)
|
|
flKeep := cmd.Bool([]string{"-keep"}, false, "Keep the service container")
|
|
cmd.Require(flag.Min, 1)
|
|
if err := cmd.ParseFlags(args, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
status := 0
|
|
for _, sn := range cmd.Args() {
|
|
if err := cli.client.ServiceDelete(context.Background(), sn, *flKeep); err != nil {
|
|
fmt.Fprintf(cli.err, "%s\n", err)
|
|
status = 1
|
|
continue
|
|
}
|
|
fmt.Fprintf(cli.out, "%s\n", sn)
|
|
}
|
|
if status != 0 {
|
|
return Cli.StatusError{StatusCode: status}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CmdServiceLs lists all the services
|
|
//
|
|
// Usage: hyper service ls [OPTIONS]
|
|
func (cli *DockerCli) CmdServiceLs(args ...string) error {
|
|
cmd := Cli.Subcmd("service ls", nil, "Lists services", true)
|
|
|
|
flFilter := ropts.NewListOpts(nil)
|
|
cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
|
|
|
|
cmd.Require(flag.Exact, 0)
|
|
err := cmd.ParseFlags(args, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Consolidate all filter flags, and sanity check them early.
|
|
// They'll get process after get response from server.
|
|
serviceFilterArgs := filters.NewArgs()
|
|
for _, f := range flFilter.GetAll() {
|
|
if serviceFilterArgs, err = filters.ParseFlag(f, serviceFilterArgs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
options := types.ServiceListOptions{
|
|
Filters: serviceFilterArgs,
|
|
}
|
|
|
|
services, err := cli.client.ServiceList(context.Background(), options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
|
fmt.Fprintf(w, "Name\tFIP\tContainers\tStatus\tMessage\n")
|
|
for _, service := range services {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", service.Name, service.FIP, showContainersInList(service.Containers), service.Status, service.Message)
|
|
}
|
|
|
|
w.Flush()
|
|
return nil
|
|
}
|
|
|
|
func showContainersInList(containers []string) string {
|
|
var result = []string{}
|
|
for _, c := range containers {
|
|
result = append(result, c[:12])
|
|
}
|
|
if len(result) > 2 {
|
|
return strings.Join([]string{result[0], result[1], "..."}, ", ")
|
|
}
|
|
return strings.Join(result, ", ")
|
|
}
|
|
|
|
// CmdServiceInspect
|
|
//
|
|
// Usage: docker service inspect [OPTIONS] service [service...]
|
|
func (cli *DockerCli) CmdServiceInspect(args ...string) error {
|
|
cmd := Cli.Subcmd("service inspect", []string{"service [service...]"}, "Display detailed information on the given service", true)
|
|
tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template")
|
|
|
|
cmd.Require(flag.Min, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
if err := cmd.Parse(args); err != nil {
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
inspectSearcher := func(name string) (interface{}, []byte, error) {
|
|
i, err := cli.client.ServiceInspect(ctx, name)
|
|
return i, nil, err
|
|
}
|
|
|
|
return cli.inspectElements(*tmplStr, cmd.Args(), inspectSearcher)
|
|
}
|
|
|
|
// CmdServiceScale
|
|
//
|
|
// Usage: hyper service scale [OPTIONS] SERVICE=REPLICAS [SERVICE=REPLICAS...]
|
|
func (cli *DockerCli) CmdServiceScale(args ...string) error {
|
|
cmd := Cli.Subcmd("service scale", []string{"SERVICE=REPLICAS [SERVICE=REPLICAS...]"}, "", true)
|
|
|
|
cmd.Require(flag.Min, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
if err := cmd.Parse(args); err != nil {
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
for _, sr := range cmd.Args() {
|
|
fields := strings.SplitN(sr, "=", 2)
|
|
if len(fields) != 2 {
|
|
fmt.Fprintf(cli.err, "invalid argument")
|
|
continue
|
|
}
|
|
replicas, err := strconv.Atoi(fields[1])
|
|
if err != nil {
|
|
fmt.Fprintf(cli.err, "%v\n", err)
|
|
continue
|
|
}
|
|
sv := types.ServiceUpdate{
|
|
Replicas: &replicas,
|
|
}
|
|
|
|
service, err := cli.client.ServiceUpdate(ctx, fields[0], sv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cli.out, "%s\n", service.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CmdServiceRolling_update
|
|
//
|
|
// Usage: hyper service rolling-update [OPTIONS] SERVICE [SERVICE...]
|
|
func (cli *DockerCli) CmdServiceRolling_update(args ...string) error {
|
|
cmd := Cli.Subcmd("service rolling-update", []string{"SERVICE [SERVICE...]"}, "Perform a rolling update of the given service", true)
|
|
flImage := cmd.String([]string{"-image"}, "", "New container image")
|
|
|
|
cmd.Require(flag.Min, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
if err := cmd.Parse(args); err != nil {
|
|
return nil
|
|
}
|
|
|
|
if len(*flImage) == 0 {
|
|
return fmt.Errorf("image is required for rolling-update")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if _, _, err := cli.client.ImageInspectWithRaw(ctx, *flImage, false); err != nil && strings.Contains(err.Error(), "No such image") {
|
|
if err := cli.pullImage(ctx, *flImage); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, sr := range cmd.Args() {
|
|
sv := types.ServiceUpdate{
|
|
Image: flImage,
|
|
}
|
|
|
|
service, err := cli.client.ServiceUpdate(ctx, sr, sv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cli.out, "Rolling-update is requested for service %s.\n", service.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CmdServiceAttach_fip
|
|
//
|
|
// Usage: hyper service attach_fip [OPTIONS] SERVICE [SERVICE...]
|
|
func (cli *DockerCli) CmdServiceAttach_fip(args ...string) error {
|
|
cmd := Cli.Subcmd("service attach-fip", []string{"SERVICE"}, "Attach a fip to the service", true)
|
|
flFip := cmd.String([]string{"-fip"}, "", "Attach a fip to the service")
|
|
|
|
cmd.Require(flag.Exact, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
if err := cmd.Parse(args); err != nil {
|
|
return nil
|
|
}
|
|
if *flFip == "" {
|
|
return fmt.Errorf("Error: please provide the attached FIP via --fip")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
sv := types.ServiceUpdate{
|
|
FIP: flFip,
|
|
}
|
|
|
|
service, err := cli.client.ServiceUpdate(ctx, cmd.Arg(0), sv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cli.out, "%s\n", service.Name)
|
|
return nil
|
|
}
|
|
|
|
// CmdServiceDetach_fip
|
|
//
|
|
// Usage: hyper service detach_fip [OPTIONS] SERVICE [SERVICE...]
|
|
func (cli *DockerCli) CmdServiceDetach_fip(args ...string) error {
|
|
cmd := Cli.Subcmd("service detach-fip", []string{"SERVICE [SERVICE...]"}, "Detach a fip from the service", true)
|
|
|
|
cmd.Require(flag.Min, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
if err := cmd.Parse(args); err != nil {
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
fip := ""
|
|
for _, sr := range cmd.Args() {
|
|
sv := types.ServiceUpdate{
|
|
FIP: &fip,
|
|
}
|
|
|
|
service, err := cli.client.ServiceUpdate(ctx, sr, sv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cli.out, "%s\n", service.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func serviceUsage() string {
|
|
serviceCommands := [][]string{
|
|
{"create", "Create a service"},
|
|
{"inspect", "Display detailed information on the given service"},
|
|
{"ls", "List all services"},
|
|
{"scale", "Scale the service"},
|
|
{"rolling-update", "Perform a rolling update of the given service"},
|
|
{"attach-fip", "Attach a fip to the service"},
|
|
{"detach-fip", "Detach the fip from the service"},
|
|
{"rm", "Remove one or more services"},
|
|
}
|
|
|
|
help := "Commands:\n"
|
|
|
|
for _, cmd := range serviceCommands {
|
|
help += fmt.Sprintf(" %-25.25s%s\n", cmd[0], cmd[1])
|
|
}
|
|
|
|
help += fmt.Sprintf("\nRun 'hyper service COMMAND --help' for more information on a command.")
|
|
return help
|
|
}
|