Import docker/docker/cli

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
Upstream-commit: 1630fc40f8
Component: cli
This commit is contained in:
Daniel Nephin
2017-04-17 17:40:59 -04:00
325 changed files with 38711 additions and 0 deletions
@@ -0,0 +1,153 @@
package stack
import (
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli/compose/convert"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.Client
services []string
networks []string
secrets []string
removedServices []string
removedNetworks []string
removedSecrets []string
serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error)
networkListFunc func(options types.NetworkListOptions) ([]types.NetworkResource, error)
secretListFunc func(options types.SecretListOptions) ([]swarm.Secret, error)
serviceRemoveFunc func(serviceID string) error
networkRemoveFunc func(networkID string) error
secretRemoveFunc func(secretID string) error
}
func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
if cli.serviceListFunc != nil {
return cli.serviceListFunc(options)
}
namespace := namespaceFromFilters(options.Filters)
servicesList := []swarm.Service{}
for _, name := range cli.services {
if belongToNamespace(name, namespace) {
servicesList = append(servicesList, serviceFromName(name))
}
}
return servicesList, nil
}
func (cli *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
if cli.networkListFunc != nil {
return cli.networkListFunc(options)
}
namespace := namespaceFromFilters(options.Filters)
networksList := []types.NetworkResource{}
for _, name := range cli.networks {
if belongToNamespace(name, namespace) {
networksList = append(networksList, networkFromName(name))
}
}
return networksList, nil
}
func (cli *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
if cli.secretListFunc != nil {
return cli.secretListFunc(options)
}
namespace := namespaceFromFilters(options.Filters)
secretsList := []swarm.Secret{}
for _, name := range cli.secrets {
if belongToNamespace(name, namespace) {
secretsList = append(secretsList, secretFromName(name))
}
}
return secretsList, nil
}
func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error {
if cli.serviceRemoveFunc != nil {
return cli.serviceRemoveFunc(serviceID)
}
cli.removedServices = append(cli.removedServices, serviceID)
return nil
}
func (cli *fakeClient) NetworkRemove(ctx context.Context, networkID string) error {
if cli.networkRemoveFunc != nil {
return cli.networkRemoveFunc(networkID)
}
cli.removedNetworks = append(cli.removedNetworks, networkID)
return nil
}
func (cli *fakeClient) SecretRemove(ctx context.Context, secretID string) error {
if cli.secretRemoveFunc != nil {
return cli.secretRemoveFunc(secretID)
}
cli.removedSecrets = append(cli.removedSecrets, secretID)
return nil
}
func serviceFromName(name string) swarm.Service {
return swarm.Service{
ID: "ID-" + name,
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: name},
},
}
}
func networkFromName(name string) types.NetworkResource {
return types.NetworkResource{
ID: "ID-" + name,
Name: name,
}
}
func secretFromName(name string) swarm.Secret {
return swarm.Secret{
ID: "ID-" + name,
Spec: swarm.SecretSpec{
Annotations: swarm.Annotations{Name: name},
},
}
}
func namespaceFromFilters(filters filters.Args) string {
label := filters.Get("label")[0]
return strings.TrimPrefix(label, convert.LabelNamespace+"=")
}
func belongToNamespace(id, namespace string) bool {
return strings.HasPrefix(id, namespace+"_")
}
func objectName(namespace, name string) string {
return namespace + "_" + name
}
func objectID(name string) string {
return "ID-" + name
}
func buildObjectIDs(objectNames []string) []string {
IDs := make([]string, len(objectNames))
for i, name := range objectNames {
IDs[i] = objectID(name)
}
return IDs
}
+35
View File
@@ -0,0 +1,35 @@
package stack
import (
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/spf13/cobra"
)
// NewStackCommand returns a cobra command for `stack` subcommands
func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "stack",
Short: "Manage Docker stacks",
Args: cli.NoArgs,
RunE: dockerCli.ShowHelp,
Tags: map[string]string{"version": "1.25"},
}
cmd.AddCommand(
newDeployCommand(dockerCli),
newListCommand(dockerCli),
newRemoveCommand(dockerCli),
newServicesCommand(dockerCli),
newPsCommand(dockerCli),
)
return cmd
}
// NewTopLevelDeployCommand returns a command for `docker deploy`
func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := newDeployCommand(dockerCli)
// Remove the aliases at the top level
cmd.Aliases = []string{}
cmd.Tags = map[string]string{"experimental": "", "version": "1.25"}
return cmd
}
@@ -0,0 +1,66 @@
package stack
import (
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli/compose/convert"
"github.com/docker/docker/client"
"github.com/docker/docker/opts"
)
func getStackFilter(namespace string) filters.Args {
filter := filters.NewArgs()
filter.Add("label", convert.LabelNamespace+"="+namespace)
return filter
}
func getServiceFilter(namespace string) filters.Args {
filter := getStackFilter(namespace)
filter.Add("runtime", string(swarm.RuntimeContainer))
return filter
}
func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args {
filter := opt.Value()
filter.Add("label", convert.LabelNamespace+"="+namespace)
return filter
}
func getAllStacksFilter() filters.Args {
filter := filters.NewArgs()
filter.Add("label", convert.LabelNamespace)
return filter
}
func getServices(
ctx context.Context,
apiclient client.APIClient,
namespace string,
) ([]swarm.Service, error) {
return apiclient.ServiceList(
ctx,
types.ServiceListOptions{Filters: getServiceFilter(namespace)})
}
func getStackNetworks(
ctx context.Context,
apiclient client.APIClient,
namespace string,
) ([]types.NetworkResource, error) {
return apiclient.NetworkList(
ctx,
types.NetworkListOptions{Filters: getStackFilter(namespace)})
}
func getStackSecrets(
ctx context.Context,
apiclient client.APIClient,
namespace string,
) ([]swarm.Secret, error) {
return apiclient.SecretList(
ctx,
types.SecretListOptions{Filters: getStackFilter(namespace)})
}
@@ -0,0 +1,97 @@
package stack
import (
"fmt"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/compose/convert"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
const (
defaultNetworkDriver = "overlay"
)
type deployOptions struct {
bundlefile string
composefile string
namespace string
sendRegistryAuth bool
prune bool
}
func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
var opts deployOptions
cmd := &cobra.Command{
Use: "deploy [OPTIONS] STACK",
Aliases: []string{"up"},
Short: "Deploy a new stack or update an existing stack",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.namespace = args[0]
return runDeploy(dockerCli, opts)
},
}
flags := cmd.Flags()
addBundlefileFlag(&opts.bundlefile, flags)
addComposefileFlag(&opts.composefile, flags)
addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
flags.SetAnnotation("prune", "version", []string{"1.27"})
return cmd
}
func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error {
ctx := context.Background()
switch {
case opts.bundlefile == "" && opts.composefile == "":
return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
case opts.bundlefile != "" && opts.composefile != "":
return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
case opts.bundlefile != "":
return deployBundle(ctx, dockerCli, opts)
default:
return deployCompose(ctx, dockerCli, opts)
}
}
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
// a swarm manager. This is necessary because we must create networks before we
// create services, but the API call for creating a network does not return a
// proper status code when it can't create a network in the "global" scope.
func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli) error {
info, err := dockerCli.Client().Info(ctx)
if err != nil {
return err
}
if !info.Swarm.ControlAvailable {
return errors.New("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.")
}
return nil
}
// pruneServices removes services that are no longer referenced in the source
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) bool {
client := dockerCli.Client()
oldServices, err := getServices(ctx, client, namespace.Name())
if err != nil {
fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err)
return true
}
pruneServices := []swarm.Service{}
for _, service := range oldServices {
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
pruneServices = append(pruneServices, service)
}
}
return removeServices(ctx, dockerCli, pruneServices)
}
@@ -0,0 +1,91 @@
package stack
import (
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/compose/convert"
)
func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error {
bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
if err != nil {
return err
}
if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
return err
}
namespace := convert.NewNamespace(opts.namespace)
if opts.prune {
services := map[string]struct{}{}
for service := range bundle.Services {
services[service] = struct{}{}
}
pruneServices(ctx, dockerCli, namespace, services)
}
networks := make(map[string]types.NetworkCreate)
for _, service := range bundle.Services {
for _, networkName := range service.Networks {
networks[networkName] = types.NetworkCreate{
Labels: convert.AddStackLabel(namespace, nil),
}
}
}
services := make(map[string]swarm.ServiceSpec)
for internalName, service := range bundle.Services {
name := namespace.Scope(internalName)
var ports []swarm.PortConfig
for _, portSpec := range service.Ports {
ports = append(ports, swarm.PortConfig{
Protocol: swarm.PortConfigProtocol(portSpec.Protocol),
TargetPort: portSpec.Port,
})
}
nets := []swarm.NetworkAttachmentConfig{}
for _, networkName := range service.Networks {
nets = append(nets, swarm.NetworkAttachmentConfig{
Target: namespace.Scope(networkName),
Aliases: []string{internalName},
})
}
serviceSpec := swarm.ServiceSpec{
Annotations: swarm.Annotations{
Name: name,
Labels: convert.AddStackLabel(namespace, service.Labels),
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: swarm.ContainerSpec{
Image: service.Image,
Command: service.Command,
Args: service.Args,
Env: service.Env,
// Service Labels will not be copied to Containers
// automatically during the deployment so we apply
// it here.
Labels: convert.AddStackLabel(namespace, nil),
},
},
EndpointSpec: &swarm.EndpointSpec{
Ports: ports,
},
Networks: nets,
}
services[internalName] = serviceSpec
}
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
return err
}
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth)
}
@@ -0,0 +1,315 @@
package stack
import (
"fmt"
"io/ioutil"
"os"
"sort"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/compose/convert"
"github.com/docker/docker/cli/compose/loader"
composetypes "github.com/docker/docker/cli/compose/types"
apiclient "github.com/docker/docker/client"
dockerclient "github.com/docker/docker/client"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error {
configDetails, err := getConfigDetails(opts)
if err != nil {
return err
}
config, err := loader.Load(configDetails)
if err != nil {
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
return errors.Errorf("Compose file contains unsupported options:\n\n%s\n",
propertyWarnings(fpe.Properties))
}
return err
}
unsupportedProperties := loader.GetUnsupportedProperties(configDetails)
if len(unsupportedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
strings.Join(unsupportedProperties, ", "))
}
deprecatedProperties := loader.GetDeprecatedProperties(configDetails)
if len(deprecatedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
propertyWarnings(deprecatedProperties))
}
if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
return err
}
namespace := convert.NewNamespace(opts.namespace)
if opts.prune {
services := map[string]struct{}{}
for _, service := range config.Services {
services[service.Name] = struct{}{}
}
pruneServices(ctx, dockerCli, namespace, services)
}
serviceNetworks := getServicesDeclaredNetworks(config.Services)
networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks)
if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil {
return err
}
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
return err
}
secrets, err := convert.Secrets(namespace, config.Secrets)
if err != nil {
return err
}
if err := createSecrets(ctx, dockerCli, namespace, secrets); err != nil {
return err
}
services, err := convert.Services(namespace, config, dockerCli.Client())
if err != nil {
return err
}
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth)
}
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
serviceNetworks := map[string]struct{}{}
for _, serviceConfig := range serviceConfigs {
if len(serviceConfig.Networks) == 0 {
serviceNetworks["default"] = struct{}{}
continue
}
for network := range serviceConfig.Networks {
serviceNetworks[network] = struct{}{}
}
}
return serviceNetworks
}
func propertyWarnings(properties map[string]string) string {
var msgs []string
for name, description := range properties {
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
}
sort.Strings(msgs)
return strings.Join(msgs, "\n\n")
}
func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) {
var details composetypes.ConfigDetails
var err error
details.WorkingDir, err = os.Getwd()
if err != nil {
return details, err
}
configFile, err := getConfigFile(opts.composefile)
if err != nil {
return details, err
}
// TODO: support multiple files
details.ConfigFiles = []composetypes.ConfigFile{*configFile}
details.Environment, err = buildEnvironment(os.Environ())
if err != nil {
return details, err
}
return details, nil
}
func buildEnvironment(env []string) (map[string]string, error) {
result := make(map[string]string, len(env))
for _, s := range env {
// if value is empty, s is like "K=", not "K".
if !strings.Contains(s, "=") {
return result, errors.Errorf("unexpected environment %q", s)
}
kv := strings.SplitN(s, "=", 2)
result[kv[0]] = kv[1]
}
return result, nil
}
func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
config, err := loader.ParseYAML(bytes)
if err != nil {
return nil, err
}
return &composetypes.ConfigFile{
Filename: filename,
Config: config,
}, nil
}
func validateExternalNetworks(
ctx context.Context,
dockerCli *command.DockerCli,
externalNetworks []string) error {
client := dockerCli.Client()
for _, networkName := range externalNetworks {
network, err := client.NetworkInspect(ctx, networkName, false)
if err != nil {
if dockerclient.IsErrNetworkNotFound(err) {
return errors.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName)
}
return err
}
if network.Scope != "swarm" {
return errors.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm")
}
}
return nil
}
func createSecrets(
ctx context.Context,
dockerCli *command.DockerCli,
namespace convert.Namespace,
secrets []swarm.SecretSpec,
) error {
client := dockerCli.Client()
for _, secretSpec := range secrets {
secret, _, err := client.SecretInspectWithRaw(ctx, secretSpec.Name)
if err == nil {
// secret already exists, then we update that
if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
return err
}
} else if apiclient.IsErrSecretNotFound(err) {
// secret does not exist, then we create a new one.
if _, err := client.SecretCreate(ctx, secretSpec); err != nil {
return err
}
} else {
return err
}
}
return nil
}
func createNetworks(
ctx context.Context,
dockerCli *command.DockerCli,
namespace convert.Namespace,
networks map[string]types.NetworkCreate,
) error {
client := dockerCli.Client()
existingNetworks, err := getStackNetworks(ctx, client, namespace.Name())
if err != nil {
return err
}
existingNetworkMap := make(map[string]types.NetworkResource)
for _, network := range existingNetworks {
existingNetworkMap[network.Name] = network
}
for internalName, createOpts := range networks {
name := namespace.Scope(internalName)
if _, exists := existingNetworkMap[name]; exists {
continue
}
if createOpts.Driver == "" {
createOpts.Driver = defaultNetworkDriver
}
fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
return err
}
}
return nil
}
func deployServices(
ctx context.Context,
dockerCli *command.DockerCli,
services map[string]swarm.ServiceSpec,
namespace convert.Namespace,
sendAuth bool,
) error {
apiClient := dockerCli.Client()
out := dockerCli.Out()
existingServices, err := getServices(ctx, apiClient, namespace.Name())
if err != nil {
return err
}
existingServiceMap := make(map[string]swarm.Service)
for _, service := range existingServices {
existingServiceMap[service.Spec.Name] = service
}
for internalName, serviceSpec := range services {
name := namespace.Scope(internalName)
encodedAuth := ""
if sendAuth {
// Retrieve encoded auth token from the image reference
image := serviceSpec.TaskTemplate.ContainerSpec.Image
encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image)
if err != nil {
return err
}
}
if service, exists := existingServiceMap[name]; exists {
fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)
updateOpts := types.ServiceUpdateOptions{}
if sendAuth {
updateOpts.EncodedRegistryAuth = encodedAuth
}
response, err := apiClient.ServiceUpdate(
ctx,
service.ID,
service.Version,
serviceSpec,
updateOpts,
)
if err != nil {
return err
}
for _, warning := range response.Warnings {
fmt.Fprintln(dockerCli.Err(), warning)
}
} else {
fmt.Fprintf(out, "Creating service %s\n", name)
createOpts := types.ServiceCreateOptions{}
if sendAuth {
createOpts.EncodedRegistryAuth = encodedAuth
}
if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
return err
}
}
}
return nil
}
@@ -0,0 +1,27 @@
package stack
import (
"bytes"
"testing"
"github.com/docker/docker/cli/compose/convert"
"github.com/docker/docker/cli/internal/test"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestPruneServices(t *testing.T) {
ctx := context.Background()
namespace := convert.NewNamespace("foo")
services := map[string]struct{}{
"new": {},
"keep": {},
}
client := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}}
dockerCli := test.NewFakeCli(client, &bytes.Buffer{})
dockerCli.SetErr(&bytes.Buffer{})
pruneServices(ctx, dockerCli, namespace, services)
assert.Equal(t, buildObjectIDs([]string{objectName("foo", "remove")}), client.removedServices)
}
+122
View File
@@ -0,0 +1,122 @@
package stack
import (
"fmt"
"io"
"sort"
"strconv"
"text/tabwriter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/compose/convert"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
const (
listItemFmt = "%s\t%s\n"
)
type listOptions struct {
}
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := listOptions{}
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List stacks",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runList(dockerCli, opts)
},
}
return cmd
}
func runList(dockerCli *command.DockerCli, opts listOptions) error {
client := dockerCli.Client()
ctx := context.Background()
stacks, err := getStacks(ctx, client)
if err != nil {
return err
}
out := dockerCli.Out()
printTable(out, stacks)
return nil
}
type byName []*stack
func (n byName) Len() int { return len(n) }
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name }
func printTable(out io.Writer, stacks []*stack) {
writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
// Ignore flushing errors
defer writer.Flush()
sort.Sort(byName(stacks))
fmt.Fprintf(writer, listItemFmt, "NAME", "SERVICES")
for _, stack := range stacks {
fmt.Fprintf(
writer,
listItemFmt,
stack.Name,
strconv.Itoa(stack.Services),
)
}
}
type stack struct {
// Name is the name of the stack
Name string
// Services is the number of the services
Services int
}
func getStacks(
ctx context.Context,
apiclient client.APIClient,
) ([]*stack, error) {
services, err := apiclient.ServiceList(
ctx,
types.ServiceListOptions{Filters: getAllStacksFilter()})
if err != nil {
return nil, err
}
m := make(map[string]*stack, 0)
for _, service := range services {
labels := service.Spec.Labels
name, ok := labels[convert.LabelNamespace]
if !ok {
return nil, errors.Errorf("cannot get label %s for service %s",
convert.LabelNamespace, service.ID)
}
ztack, ok := m[name]
if !ok {
m[name] = &stack{
Name: name,
Services: 1,
}
} else {
ztack.Services++
}
}
var stacks []*stack
for _, stack := range m {
stacks = append(stacks, stack)
}
return stacks, nil
}
+51
View File
@@ -0,0 +1,51 @@
package stack
import (
"fmt"
"io"
"os"
"github.com/docker/docker/cli/command/bundlefile"
"github.com/pkg/errors"
"github.com/spf13/pflag"
)
func addComposefileFlag(opt *string, flags *pflag.FlagSet) {
flags.StringVarP(opt, "compose-file", "c", "", "Path to a Compose file")
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
}
func addBundlefileFlag(opt *string, flags *pflag.FlagSet) {
flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file")
flags.SetAnnotation("bundle-file", "experimental", nil)
}
func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) {
flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
}
func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
defaultPath := fmt.Sprintf("%s.dab", namespace)
if path == "" {
path = defaultPath
}
if _, err := os.Stat(path); err != nil {
return nil, errors.Errorf(
"Bundle %s not found. Specify the path with --file",
path)
}
fmt.Fprintf(stderr, "Loading bundle from %s\n", path)
reader, err := os.Open(path)
if err != nil {
return nil, err
}
defer reader.Close()
bundle, err := bundlefile.LoadFile(reader)
if err != nil {
return nil, errors.Errorf("Error reading %s: %v\n", path, err)
}
return bundle, err
}
+76
View File
@@ -0,0 +1,76 @@
package stack
import (
"fmt"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/formatter"
"github.com/docker/docker/cli/command/idresolver"
"github.com/docker/docker/cli/command/task"
"github.com/docker/docker/opts"
"github.com/spf13/cobra"
)
type psOptions struct {
filter opts.FilterOpt
noTrunc bool
namespace string
noResolve bool
quiet bool
format string
}
func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := psOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{
Use: "ps [OPTIONS] STACK",
Short: "List the tasks in the stack",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.namespace = args[0]
return runPS(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template")
return cmd
}
func runPS(dockerCli *command.DockerCli, opts psOptions) error {
namespace := opts.namespace
client := dockerCli.Client()
ctx := context.Background()
filter := getStackFilterFromOpt(opts.namespace, opts.filter)
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
if err != nil {
return err
}
if len(tasks) == 0 {
fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace)
return nil
}
format := opts.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet {
format = dockerCli.ConfigFile().TasksFormat
} else {
format = formatter.TableFormatKey
}
}
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format)
}
+121
View File
@@ -0,0 +1,121 @@
package stack
import (
"fmt"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type removeOptions struct {
namespaces []string
}
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
var opts removeOptions
cmd := &cobra.Command{
Use: "rm STACK [STACK...]",
Aliases: []string{"remove", "down"},
Short: "Remove one or more stacks",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.namespaces = args
return runRemove(dockerCli, opts)
},
}
return cmd
}
func runRemove(dockerCli command.Cli, opts removeOptions) error {
namespaces := opts.namespaces
client := dockerCli.Client()
ctx := context.Background()
var errs []string
for _, namespace := range namespaces {
services, err := getServices(ctx, client, namespace)
if err != nil {
return err
}
networks, err := getStackNetworks(ctx, client, namespace)
if err != nil {
return err
}
secrets, err := getStackSecrets(ctx, client, namespace)
if err != nil {
return err
}
if len(services)+len(networks)+len(secrets) == 0 {
fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace)
continue
}
hasError := removeServices(ctx, dockerCli, services)
hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
hasError = removeNetworks(ctx, dockerCli, networks) || hasError
if hasError {
errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace))
}
}
if len(errs) > 0 {
return errors.Errorf(strings.Join(errs, "\n"))
}
return nil
}
func removeServices(
ctx context.Context,
dockerCli command.Cli,
services []swarm.Service,
) bool {
var err error
for _, service := range services {
fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name)
if err = dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil {
fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err)
}
}
return err != nil
}
func removeNetworks(
ctx context.Context,
dockerCli command.Cli,
networks []types.NetworkResource,
) bool {
var err error
for _, network := range networks {
fmt.Fprintf(dockerCli.Err(), "Removing network %s\n", network.Name)
if err = dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil {
fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err)
}
}
return err != nil
}
func removeSecrets(
ctx context.Context,
dockerCli command.Cli,
secrets []swarm.Secret,
) bool {
var err error
for _, secret := range secrets {
fmt.Fprintf(dockerCli.Err(), "Removing secret %s\n", secret.Spec.Name)
if err = dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
}
}
return err != nil
}
@@ -0,0 +1,107 @@
package stack
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/docker/docker/cli/internal/test"
"github.com/stretchr/testify/assert"
)
func TestRemoveStack(t *testing.T) {
allServices := []string{
objectName("foo", "service1"),
objectName("foo", "service2"),
objectName("bar", "service1"),
objectName("bar", "service2"),
}
allServiceIDs := buildObjectIDs(allServices)
allNetworks := []string{
objectName("foo", "network1"),
objectName("bar", "network1"),
}
allNetworkIDs := buildObjectIDs(allNetworks)
allSecrets := []string{
objectName("foo", "secret1"),
objectName("foo", "secret2"),
objectName("bar", "secret1"),
}
allSecretIDs := buildObjectIDs(allSecrets)
cli := &fakeClient{
services: allServices,
networks: allNetworks,
secrets: allSecrets,
}
cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{}))
cmd.SetArgs([]string{"foo", "bar"})
assert.NoError(t, cmd.Execute())
assert.Equal(t, allServiceIDs, cli.removedServices)
assert.Equal(t, allNetworkIDs, cli.removedNetworks)
assert.Equal(t, allSecretIDs, cli.removedSecrets)
}
func TestSkipEmptyStack(t *testing.T) {
buf := new(bytes.Buffer)
allServices := []string{objectName("bar", "service1"), objectName("bar", "service2")}
allServiceIDs := buildObjectIDs(allServices)
allNetworks := []string{objectName("bar", "network1")}
allNetworkIDs := buildObjectIDs(allNetworks)
allSecrets := []string{objectName("bar", "secret1")}
allSecretIDs := buildObjectIDs(allSecrets)
cli := &fakeClient{
services: allServices,
networks: allNetworks,
secrets: allSecrets,
}
cmd := newRemoveCommand(test.NewFakeCli(cli, buf))
cmd.SetArgs([]string{"foo", "bar"})
assert.NoError(t, cmd.Execute())
assert.Contains(t, buf.String(), "Nothing found in stack: foo")
assert.Equal(t, allServiceIDs, cli.removedServices)
assert.Equal(t, allNetworkIDs, cli.removedNetworks)
assert.Equal(t, allSecretIDs, cli.removedSecrets)
}
func TestContinueAfterError(t *testing.T) {
allServices := []string{objectName("foo", "service1"), objectName("bar", "service1")}
allServiceIDs := buildObjectIDs(allServices)
allNetworks := []string{objectName("foo", "network1"), objectName("bar", "network1")}
allNetworkIDs := buildObjectIDs(allNetworks)
allSecrets := []string{objectName("foo", "secret1"), objectName("bar", "secret1")}
allSecretIDs := buildObjectIDs(allSecrets)
removedServices := []string{}
cli := &fakeClient{
services: allServices,
networks: allNetworks,
secrets: allSecrets,
serviceRemoveFunc: func(serviceID string) error {
removedServices = append(removedServices, serviceID)
if strings.Contains(serviceID, "foo") {
return errors.New("")
}
return nil
},
}
cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{}))
cmd.SetArgs([]string{"foo", "bar"})
assert.EqualError(t, cmd.Execute(), "Failed to remove some resources from stack: foo")
assert.Equal(t, allServiceIDs, removedServices)
assert.Equal(t, allNetworkIDs, cli.removedNetworks)
assert.Equal(t, allSecretIDs, cli.removedSecrets)
}
@@ -0,0 +1,97 @@
package stack
import (
"fmt"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/formatter"
"github.com/docker/docker/cli/command/service"
"github.com/docker/docker/opts"
"github.com/spf13/cobra"
)
type servicesOptions struct {
quiet bool
format string
filter opts.FilterOpt
namespace string
}
func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := servicesOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{
Use: "services [OPTIONS] STACK",
Short: "List the services in the stack",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.namespace = args[0]
return runServices(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template")
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
return cmd
}
func runServices(dockerCli *command.DockerCli, opts servicesOptions) error {
ctx := context.Background()
client := dockerCli.Client()
filter := getStackFilterFromOpt(opts.namespace, opts.filter)
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
if err != nil {
return err
}
out := dockerCli.Out()
// if no services in this stack, print message and exit 0
if len(services) == 0 {
fmt.Fprintf(out, "Nothing found in stack: %s\n", opts.namespace)
return nil
}
info := map[string]formatter.ServiceListInfo{}
if !opts.quiet {
taskFilter := filters.NewArgs()
for _, service := range services {
taskFilter.Add("service", service.ID)
}
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
if err != nil {
return err
}
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
if err != nil {
return err
}
info = service.GetServicesStatus(services, nodes, tasks)
}
format := opts.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet {
format = dockerCli.ConfigFile().ServicesFormat
} else {
format = formatter.TableFormatKey
}
}
servicesCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewServiceListFormat(format, opts.quiet),
}
return formatter.ServiceListWrite(servicesCtx, services, info)
}