mirror of https://github.com/docker/cli.git
Validate stack-names for empty values
Add validation for stack names to prevent an empty name resulting in _all_ stacks to be returned after filtering, which can result in removal of services for all stacks if `--prune`, or `docker stack rm` is used. Before this change; docker stack deploy -c docker-compose.yml one docker stack deploy -c docker-compose.yml two docker stack deploy -c docker-compose.yml three docker stack deploy -c docker-compose.yml --prune '' Removing service one_web Removing service two_web Removing service three_web After this change: docker stack deploy -c docker-compose.yml one docker stack deploy -c docker-compose.yml two docker stack deploy -c docker-compose.yml three docker stack deploy -c docker-compose.yml --prune '' invalid stack name: "" Other stack commands were updated as well: Before this change; docker stack deploy -c docker-compose.yml '' Creating network _default failed to create network _default: Error response from daemon: rpc error: code = InvalidArgument desc = name must be valid as a DNS name component docker stack ps '' nothing found in stack: docker stack rm '' Removing service one_web Removing service three_web Removing service two_web After this change: docker stack deploy -c docker-compose.yml '' invalid stack name: "" docker stack ps '' invalid stack name: "" docker stack rm '' invalid stack name: "" Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
05f04bb997
commit
d38f397da1
|
@ -2,6 +2,9 @@ package swarm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/compose/convert"
|
"github.com/docker/cli/cli/compose/convert"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
|
@ -48,3 +51,28 @@ func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace
|
||||||
func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) {
|
func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) {
|
||||||
return apiclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
|
return apiclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateStackName checks if the provided string is a valid stack name (namespace).
|
||||||
|
//
|
||||||
|
// It currently only does a rudimentary check if the string is empty, or consists
|
||||||
|
// of only whitespace and quoting characters.
|
||||||
|
func validateStackName(namespace string) error {
|
||||||
|
v := strings.TrimFunc(namespace, quotesOrWhitespace)
|
||||||
|
if len(v) == 0 {
|
||||||
|
return fmt.Errorf("invalid stack name: %q", namespace)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateStackNames(namespaces []string) error {
|
||||||
|
for _, ns := range namespaces {
|
||||||
|
if err := validateStackName(ns); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func quotesOrWhitespace(r rune) bool {
|
||||||
|
return unicode.IsSpace(r) || r == '"' || r == '\''
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,9 @@ const (
|
||||||
func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
|
func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := validateStackName(opts.Namespace); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
|
if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
||||||
|
if err := validateStackName(opts.Namespace); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
bundle, err := loadBundlefile(dockerCli.Err(), opts.Namespace, opts.Bundlefile)
|
bundle, err := loadBundlefile(dockerCli.Err(), opts.Namespace, opts.Bundlefile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -18,6 +18,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
||||||
|
if err := validateStackName(opts.Namespace); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
config, err := loader.LoadComposefile(dockerCli, opts)
|
config, err := loader.LoadComposefile(dockerCli, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
"github.com/docker/cli/cli/compose/convert"
|
"github.com/docker/cli/cli/compose/convert"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
@ -26,6 +27,15 @@ func TestPruneServices(t *testing.T) {
|
||||||
assert.Check(t, is.DeepEqual(buildObjectIDs([]string{objectName("foo", "remove")}), client.removedServices))
|
assert.Check(t, is.DeepEqual(buildObjectIDs([]string{objectName("foo", "remove")}), client.removedServices))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeployWithEmptyName(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := &fakeClient{}
|
||||||
|
dockerCli := test.NewFakeCli(client)
|
||||||
|
|
||||||
|
err := deployCompose(ctx, dockerCli, options.Deploy{Namespace: "' '", Prune: true})
|
||||||
|
assert.Check(t, is.Error(err, `invalid stack name: "' '"`))
|
||||||
|
}
|
||||||
|
|
||||||
// TestServiceUpdateResolveImageChanged tests that the service's
|
// TestServiceUpdateResolveImageChanged tests that the service's
|
||||||
// image digest, and "ForceUpdate" is preserved if the image did not change in
|
// image digest, and "ForceUpdate" is preserved if the image did not change in
|
||||||
// the compose file
|
// the compose file
|
||||||
|
|
|
@ -13,19 +13,21 @@ import (
|
||||||
|
|
||||||
// RunPS is the swarm implementation of docker stack ps
|
// RunPS is the swarm implementation of docker stack ps
|
||||||
func RunPS(dockerCli command.Cli, opts options.PS) error {
|
func RunPS(dockerCli command.Cli, opts options.PS) error {
|
||||||
namespace := opts.Namespace
|
if err := validateStackName(opts.Namespace); err != nil {
|
||||||
client := dockerCli.Client()
|
return err
|
||||||
ctx := context.Background()
|
}
|
||||||
|
|
||||||
filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
|
filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
client := dockerCli.Client()
|
||||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
|
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tasks) == 0 {
|
if len(tasks) == 0 {
|
||||||
return fmt.Errorf("nothing found in stack: %s", namespace)
|
return fmt.Errorf("nothing found in stack: %s", opts.Namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
format := opts.Format
|
format := opts.Format
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"github.com/gotestyourself/gotestyourself/assert"
|
||||||
|
is "github.com/gotestyourself/gotestyourself/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunPSWithEmptyName(t *testing.T) {
|
||||||
|
client := &fakeClient{}
|
||||||
|
dockerCli := test.NewFakeCli(client)
|
||||||
|
|
||||||
|
err := RunPS(dockerCli, options.PS{Namespace: "' '"})
|
||||||
|
assert.Check(t, is.Error(err, `invalid stack name: "' '"`))
|
||||||
|
}
|
|
@ -16,12 +16,15 @@ import (
|
||||||
|
|
||||||
// RunRemove is the swarm implementation of docker stack remove
|
// RunRemove is the swarm implementation of docker stack remove
|
||||||
func RunRemove(dockerCli command.Cli, opts options.Remove) error {
|
func RunRemove(dockerCli command.Cli, opts options.Remove) error {
|
||||||
namespaces := opts.Namespaces
|
if err := validateStackNames(opts.Namespaces); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
client := dockerCli.Client()
|
client := dockerCli.Client()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
var errs []string
|
var errs []string
|
||||||
for _, namespace := range namespaces {
|
for _, namespace := range opts.Namespaces {
|
||||||
services, err := getStackServices(ctx, client, namespace)
|
services, err := getStackServices(ctx, client, namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"github.com/gotestyourself/gotestyourself/assert"
|
||||||
|
is "github.com/gotestyourself/gotestyourself/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunRemoveWithEmptyName(t *testing.T) {
|
||||||
|
client := &fakeClient{}
|
||||||
|
dockerCli := test.NewFakeCli(client)
|
||||||
|
|
||||||
|
err := RunRemove(dockerCli, options.Remove{Namespaces: []string{"good", "' '", "alsogood"}})
|
||||||
|
assert.Check(t, is.Error(err, `invalid stack name: "' '"`))
|
||||||
|
}
|
|
@ -14,6 +14,9 @@ import (
|
||||||
|
|
||||||
// RunServices is the swarm implementation of docker stack services
|
// RunServices is the swarm implementation of docker stack services
|
||||||
func RunServices(dockerCli command.Cli, opts options.Services) error {
|
func RunServices(dockerCli command.Cli, opts options.Services) error {
|
||||||
|
if err := validateStackName(opts.Namespace); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client := dockerCli.Client()
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"github.com/gotestyourself/gotestyourself/assert"
|
||||||
|
is "github.com/gotestyourself/gotestyourself/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunServicesWithEmptyName(t *testing.T) {
|
||||||
|
client := &fakeClient{}
|
||||||
|
dockerCli := test.NewFakeCli(client)
|
||||||
|
|
||||||
|
err := RunServices(dockerCli, options.Services{Namespace: "' '"})
|
||||||
|
assert.Check(t, is.Error(err, `invalid stack name: "' '"`))
|
||||||
|
}
|
Loading…
Reference in New Issue