Files
virtual-kubelet/vendor/github.com/hyperhq/hypercli/api/client/compose.go
2017-12-05 17:53:58 -06:00

520 lines
16 KiB
Go

package client
import (
"fmt"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/Sirupsen/logrus"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
Cli "github.com/hyperhq/hypercli/cli"
"github.com/hyperhq/hypercli/pkg/jsonmessage"
flag "github.com/hyperhq/hypercli/pkg/mflag"
"github.com/hyperhq/libcompose/docker"
"github.com/hyperhq/libcompose/logger"
"github.com/hyperhq/libcompose/project"
"github.com/hyperhq/libcompose/project/options"
"golang.org/x/net/context"
)
const ComposeFipAuto = "auto"
// CmdCompose is the parent subcommand for all compose commands
//
// Usage: hyper compose <COMMAND> [OPTIONS]
func (cli *DockerCli) CmdCompose(args ...string) error {
cmd := Cli.Subcmd("compose", []string{"<COMMAND>"}, composeUsage(), false)
cmd.Require(flag.Min, 1)
err := cmd.ParseFlags(args, true)
cmd.Usage()
return err
}
// CmdComposeRun
//
// Usage: hyper compose run [OPTIONS] SERVICE [COMMAND] [ARGS...]
func (cli *DockerCli) CmdComposeRun(args ...string) error {
cmd := Cli.Subcmd("compose run", []string{"SERVICE [COMMAND] [ARGS...]"}, "Run a one-off command on a service", false)
composeFile := cmd.String([]string{"f", "-file"}, "docker-compose.yml", "Specify an alternate compose file")
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
rm := cmd.Bool([]string{"-rm"}, false, "Remove container after run, ignored in detached mode")
cmd.Require(flag.Min, 1)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
if *projectName == "" {
*projectName = getBaseDir()
}
project, err := docker.NewProject(&docker.Context{
Context: project.Context{
ComposeFiles: []string{*composeFile},
ProjectName: *projectName,
Autoremove: *rm,
},
ClientFactory: cli,
})
if err != nil {
return err
}
service := cmd.Args()[0]
status, err := project.Run(context.Background(), service, cmd.Args()[1:])
if err != nil {
return err
}
if *rm {
opts := options.Delete{RemoveVolume: true}
if err = project.Delete(opts, service); err != nil {
return err
}
}
if status != 0 {
return Cli.StatusError{StatusCode: status}
}
return nil
}
// CmdComposeDown
//
// Usage: hyper compose down [OPTIONS]
func (cli *DockerCli) CmdComposeDown(args ...string) error {
cmd := Cli.Subcmd("compose down", []string{}, "Stop and remove containers, images, and volumes\ncreated by `up`. Only containers and networks are removed by default.", false)
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
rmi := cmd.String([]string{"-rmi"}, "", "Remove images, type may be one of: 'all' to remove\nall images, or 'local' to remove only images that\ndon't have an custom name set by the `image` field")
vol := cmd.Bool([]string{"v", "-volumes"}, false, "Remove data volumes")
rmorphans := cmd.Bool([]string{"-remove-orphans"}, false, "Remove containers for services not defined in the Compose file")
cmd.Require(flag.Exact, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
imageType := options.ImageType(*rmi)
if !imageType.Valid() {
return fmt.Errorf("rmi with %s is not valid", *rmi)
}
if *projectName == "" {
*projectName = getBaseDir()
}
body, err := cli.client.ComposeDown(*projectName, cmd.Args(), *rmi, *vol, *rmorphans)
if err != nil {
return err
}
defer body.Close()
return jsonmessage.DisplayJSONMessagesStream(body, cli.out, cli.outFd, cli.isTerminalOut, nil)
}
// CmdComposeUp
//
// Usage: hyper compose up [OPTIONS]
func (cli *DockerCli) CmdComposeUp(args ...string) error {
cmd := Cli.Subcmd("compose up", []string{"[SERVICE...]"}, "Builds, (re)creates, starts, and attaches to containers for a service.\n\nUnless they are already running, this command also starts any linked services.\n\n"+
"The `hyper compose up` command aggregates the output of each container. When\n"+
"the command exits, all containers are stopped. Running `hyper compose up -d`\n"+
"starts the containers in the background and leaves them running.\n\n"+
"If there are existing containers for a service, and the service's configuration\n"+
"or image was changed after the container's creation, `hyper compose up` picks\n"+
"up the changes by stopping and recreating the containers (preserving mounted\n"+
"volumes). To prevent Compose from picking up changes, use the `--no-recreate`\n"+
"flag.\n\n"+
"If you want to force Compose to stop and recreate all containers, use the\n"+
"`--force-recreate` flag.", false)
composeFile := cmd.String([]string{"f", "-file"}, "docker-compose.yml", "Specify an alternate compose file")
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
detach := cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: Run containers in the background,\nprint new container names.\nIncompatible with --abort-on-container-exit.")
forcerecreate := cmd.Bool([]string{"-force-recreate"}, false, "Recreate containers even if their configuration\nand image haven't changed.\nIncompatible with --no-recreate.")
norecreate := cmd.Bool([]string{"-no-recreate"}, false, "If containers already exist, don't recreate them.\nIncompatible with --force-recreate.")
cmd.Require(flag.Min, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
project, err := docker.NewProject(&docker.Context{
Context: project.Context{
ComposeFiles: []string{*composeFile},
ProjectName: *projectName,
LoggerFactory: logger.NewColorLoggerFactory(),
},
ClientFactory: cli,
})
if err != nil {
return err
}
services := cmd.Args()
c, vc, nc := project.GetConfig()
if *projectName == "" {
*projectName = getBaseDir()
}
var fips = []string{}
var newFipNum = 0
for _, svconfig := range c.M {
if svconfig.Fip == ComposeFipAuto {
newFipNum++
}
}
fipFilterArgs, _ := filters.FromParam("dangling=true")
options := types.NetworkListOptions{
Filters: fipFilterArgs,
}
fipList, err := cli.client.FipList(context.Background(), options)
if err == nil {
for _, fip := range fipList {
if fip["container"] == "" && fip["service"] == "" {
fips = append(fips, fip["fip"])
}
}
}
if newFipNum > len(fips) {
if askForConfirmation(warnMessage) == true {
newFips, err := cli.client.FipAllocate(context.Background(), fmt.Sprintf("%d", newFipNum-len(fips)))
if err != nil {
return err
}
fips = append(fips, newFips...)
}
}
i := 0
for _, svconfig := range c.M {
if svconfig.Fip == ComposeFipAuto {
if i >= newFipNum {
svconfig.Fip = ""
} else {
svconfig.Fip = fips[i]
i++
}
}
}
body, err := cli.client.ComposeUp(*projectName, services, c, vc, nc, cli.configFile.AuthConfigs, *forcerecreate, *norecreate)
if err != nil {
return err
}
defer body.Close()
err = jsonmessage.DisplayJSONMessagesStream(body, cli.out, cli.outFd, cli.isTerminalOut, nil)
if err != nil {
return err
}
if !*detach {
signalChan := make(chan os.Signal, 1)
cleanupDone := make(chan bool)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
errChan := make(chan error)
go func() {
errChan <- project.Log(true, services...)
}()
go func() {
select {
case <-signalChan:
fmt.Printf("\nGracefully stopping...\n")
project.Stop(0, services...)
cleanupDone <- true
case err := <-errChan:
if err != nil {
logrus.Fatal(err)
}
cleanupDone <- true
}
}()
<-cleanupDone
return nil
}
return nil
}
// CmdComposeStart
//
// Usage: hyper compose start [OPTIONS] [SERVICE]
func (cli *DockerCli) CmdComposeStart(args ...string) error {
cmd := Cli.Subcmd("compose start", []string{"[SERVICE...]"}, "Start existing containers.", false)
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
cmd.Require(flag.Min, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
services := cmd.Args()
if *projectName == "" {
*projectName = getBaseDir()
}
body, err := cli.client.ComposeStart(*projectName, services)
if err != nil {
return err
}
defer body.Close()
return jsonmessage.DisplayJSONMessagesStream(body, cli.out, cli.outFd, cli.isTerminalOut, nil)
}
// CmdComposeStop
//
// Usage: hyper compose stop [OPTIONS]
func (cli *DockerCli) CmdComposeStop(args ...string) error {
cmd := Cli.Subcmd("compose stop", []string{"[SERVICE...]"}, "Stop running containers without removing them.\n\nThey can be started again with `hyper compose start`.", false)
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
nSeconds := cmd.Int([]string{"t", "-timeout"}, 10, "Specify a shutdown timeout in seconds.")
cmd.Require(flag.Min, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
services := cmd.Args()
if *projectName == "" {
*projectName = getBaseDir()
}
body, err := cli.client.ComposeStop(*projectName, services, *nSeconds)
if err != nil {
return err
}
defer body.Close()
return jsonmessage.DisplayJSONMessagesStream(body, cli.out, cli.outFd, cli.isTerminalOut, nil)
}
// CmdComposeCreate
//
// Usage: hyper compose create [OPTIONS]
func (cli *DockerCli) CmdComposeCreate(args ...string) error {
cmd := Cli.Subcmd("compose create", []string{"[SERVICE...]"}, "Creates containers for a service.", false)
composeFile := cmd.String([]string{"f", "-file"}, "docker-compose.yml", "Specify an alternate compose file")
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
forcerecreate := cmd.Bool([]string{"-force-recreate"}, false, "Recreate containers even if their configuration\nand image haven't changed.\nIncompatible with --no-recreate.")
norecreate := cmd.Bool([]string{"-no-recreate"}, false, "If containers already exist, don't recreate them.\nIncompatible with --force-recreate.")
cmd.Require(flag.Min, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
project, err := docker.NewProject(&docker.Context{
Context: project.Context{
ComposeFiles: []string{*composeFile},
ProjectName: *projectName,
LoggerFactory: logger.NewColorLoggerFactory(),
},
ClientFactory: cli,
})
if err != nil {
return err
}
services := cmd.Args()
c, vc, nc := project.GetConfig()
if *projectName == "" {
*projectName = getBaseDir()
}
body, err := cli.client.ComposeCreate(*projectName, services, c, vc, nc, cli.configFile.AuthConfigs, *forcerecreate, *norecreate)
if err != nil {
return err
}
defer body.Close()
return jsonmessage.DisplayJSONMessagesStream(body, cli.out, cli.outFd, cli.isTerminalOut, nil)
}
// CmdComposePs
//
// Usage: hyper compose ps [OPTIONS]
func (cli *DockerCli) CmdComposePs(args ...string) error {
cmd := Cli.Subcmd("compose ps", []string{"[SERVICE...]"}, "List containers.", false)
composeFile := cmd.String([]string{"f", "-file"}, "docker-compose.yml", "Specify an alternate compose file")
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display IDs")
cmd.Require(flag.Min, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
if *projectName == "" {
*projectName = getBaseDir()
}
project, err := docker.NewProject(&docker.Context{
Context: project.Context{
ComposeFiles: []string{*composeFile},
ProjectName: *projectName,
},
ClientFactory: cli,
})
if err != nil {
return err
}
ps, err := project.Ps(*quiet, cmd.Args()...)
if err != nil {
return err
}
fmt.Printf(ps.String(true))
return nil
}
// CmdComposeKill
//
// Usage: hyper compose kill [OPTIONS]
func (cli *DockerCli) CmdComposeKill(args ...string) error {
cmd := Cli.Subcmd("compose kill", []string{"[SERVICE...]"}, "Force stop service containers.", false)
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
signal := cmd.String([]string{}, "KILL", "Signal to send to the container")
cmd.Require(flag.Min, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
services := cmd.Args()
if *projectName == "" {
*projectName = getBaseDir()
}
body, err := cli.client.ComposeKill(*projectName, services, *signal)
if err != nil {
return err
}
defer body.Close()
return jsonmessage.DisplayJSONMessagesStream(body, cli.out, cli.outFd, cli.isTerminalOut, nil)
}
// CmdComposeRm
//
// Usage: hyper compose rm [OPTIONS] [SERVICE]
func (cli *DockerCli) CmdComposeRm(args ...string) error {
cmd := Cli.Subcmd("compose rm", []string{"[SERVICE...]"}, "Remove stopped service containers.", false)
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
removeVol := cmd.Bool([]string{"v"}, false, "Remove volumes associated with containers")
cmd.Require(flag.Min, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
services := cmd.Args()
if *projectName == "" {
*projectName = getBaseDir()
}
body, err := cli.client.ComposeRm(*projectName, services, *removeVol)
if err != nil {
return err
}
defer body.Close()
return jsonmessage.DisplayJSONMessagesStream(body, cli.out, cli.outFd, cli.isTerminalOut, nil)
}
// CmdComposeScale
//
// Usage: hyper compose scale [OPTIONS] [SERVICE=NUM...]
func (cli *DockerCli) CmdComposeScale(args ...string) error {
cmd := Cli.Subcmd("compose scale", []string{"[SERVICE=NUM...]"}, "Set number of containers to run for a service.", false)
composeFile := cmd.String([]string{"f", "-file"}, "docker-compose.yml", "Specify an alternate compose file")
projectName := cmd.String([]string{"p", "-project-name"}, "", "Specify an alternate project name")
timeout := cmd.Int([]string{"t", "-timeout"}, 10, "Specify a shutdown timeout in seconds")
cmd.Require(flag.Min, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
if *projectName == "" {
*projectName = getBaseDir()
}
project, err := docker.NewProject(&docker.Context{
Context: project.Context{
ComposeFiles: []string{*composeFile},
ProjectName: *projectName,
},
ClientFactory: cli,
})
if err != nil {
return err
}
servicesScale := map[string]int{}
for _, ss := range cmd.Args() {
fields := strings.SplitN(ss, "=", 2)
if len(fields) != 2 {
continue
}
num, err := strconv.Atoi(fields[1])
if err != nil {
return err
}
servicesScale[fields[0]] = num
}
err = project.Scale(*timeout, servicesScale)
if err != nil {
return err
}
return nil
}
// CmdComposePull
//
// Usage: hyper compose pull [OPTIONS]
func (cli *DockerCli) CmdComposePull(args ...string) error {
cmd := Cli.Subcmd("compose pull", []string{"[SERVICE...]"}, "Pull images of services.", false)
composeFile := cmd.String([]string{"f", "-file"}, "docker-compose.yml", "Specify an alternate compose file")
cmd.Require(flag.Min, 0)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
project, err := docker.NewProject(&docker.Context{
Context: project.Context{
ComposeFiles: []string{*composeFile},
},
ClientFactory: cli,
})
if err != nil {
return err
}
err = project.Pull(cmd.Args()...)
if err != nil {
return err
}
return nil
}
func composeUsage() string {
composeCommands := [][]string{
{"create", "Creates containers for a service"},
{"down", "Stop and remove containers, images, and volumes"},
{"kill", "Force stop service containers"},
{"ps", "List containers"},
{"pull", "Pull images of services"},
{"rm", "Remove stopped service containers"},
{"run", "Run a one-off command"},
{"scale", "Set number of containers for a service"},
{"start", "Start services"},
{"stop", "Stop services"},
{"up", "Create and start containers"},
}
help := "Commands:\n"
for _, cmd := range composeCommands {
help += fmt.Sprintf(" %-25.25s%s\n", cmd[0], cmd[1])
}
help += fmt.Sprintf("\nRun 'hyper compose COMMAND --help' for more information on a command.")
return help
}
func (cli *DockerCli) Create(s project.Service) client.APIClient {
return cli.client
}
func getBaseDir() string {
file, err := os.Getwd()
if err != nil {
return ""
}
return filepath.Base(file)
}