mirror of https://github.com/docker/cli.git
Merge pull request #4939 from Benehiko/prompt-termination
feat: standardize error for prompt
This commit is contained in:
commit
9ca30bd2ac
|
@ -2,6 +2,7 @@ package builder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ import (
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
units "github.com/docker/go-units"
|
units "github.com/docker/go-units"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
@ -67,9 +69,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||||
warning = allCacheWarning
|
warning = allCacheWarning
|
||||||
}
|
}
|
||||||
if !options.force {
|
if !options.force {
|
||||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||||
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
|
if !r {
|
||||||
|
return 0, "", errdefs.Cancelled(errors.New("builder prune has been cancelled"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
|
report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
|
||||||
|
|
|
@ -5,10 +5,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuilderPromptTermination(t *testing.T) {
|
func TestBuilderPromptTermination(t *testing.T) {
|
||||||
|
@ -21,8 +19,5 @@ func TestBuilderPromptTermination(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := NewPruneCommand(cli)
|
cmd := NewPruneCommand(cli)
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
t.Helper()
|
|
||||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,9 @@ import (
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
units "github.com/docker/go-units"
|
units "github.com/docker/go-units"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,9 +56,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||||
|
|
||||||
if !options.force {
|
if !options.force {
|
||||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||||
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
|
if !r {
|
||||||
|
return 0, "", errdefs.Cancelled(errors.New("container prune has been cancelled"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)
|
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)
|
||||||
|
|
|
@ -4,12 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContainerPrunePromptTermination(t *testing.T) {
|
func TestContainerPrunePromptTermination(t *testing.T) {
|
||||||
|
@ -22,8 +20,5 @@ func TestContainerPrunePromptTermination(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := NewPruneCommand(cli)
|
cmd := NewPruneCommand(cli)
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
t.Helper()
|
|
||||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,9 @@ import (
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
units "github.com/docker/go-units"
|
units "github.com/docker/go-units"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -68,9 +70,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||||
warning = allImageWarning
|
warning = allImageWarning
|
||||||
}
|
}
|
||||||
if !options.force {
|
if !options.force {
|
||||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||||
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
|
if !r {
|
||||||
|
return 0, "", errdefs.Cancelled(errors.New("image prune has been cancelled"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
report, err := dockerCli.Client().ImagesPrune(ctx, pruneFilters)
|
report, err := dockerCli.Client().ImagesPrune(ctx, pruneFilters)
|
||||||
|
|
|
@ -4,9 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
@ -94,13 +95,18 @@ func TestNewPruneCommandSuccess(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := NewPruneCommand(cli)
|
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
|
||||||
cmd.SetOut(io.Discard)
|
// when prompted, answer "Y" to confirm the prune.
|
||||||
cmd.SetArgs(tc.args)
|
// will not be prompted if --force is used.
|
||||||
err := cmd.Execute()
|
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader("Y\n"))))
|
||||||
assert.NilError(t, err)
|
cmd := NewPruneCommand(cli)
|
||||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,8 +120,5 @@ func TestPrunePromptTermination(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := NewPruneCommand(cli)
|
cmd := NewPruneCommand(cli)
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
t.Helper()
|
|
||||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,9 +52,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||||
|
|
||||||
if !options.force {
|
if !options.force {
|
||||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||||
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if !r {
|
||||||
|
return "", errdefs.Cancelled(errors.New("network prune cancelled has been cancelled"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
report, err := dockerCli.Client().NetworksPrune(ctx, pruneFilters)
|
report, err := dockerCli.Client().NetworksPrune(ctx, pruneFilters)
|
||||||
|
|
|
@ -4,12 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNetworkPrunePromptTermination(t *testing.T) {
|
func TestNetworkPrunePromptTermination(t *testing.T) {
|
||||||
|
@ -22,8 +20,5 @@ func TestNetworkPrunePromptTermination(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := NewPruneCommand(cli)
|
cmd := NewPruneCommand(cli)
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
t.Helper()
|
|
||||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
|
@ -115,8 +114,5 @@ func TestNetworkRemovePromptTermination(t *testing.T) {
|
||||||
})
|
})
|
||||||
cmd := newRemoveCommand(cli)
|
cmd := newRemoveCommand(cli)
|
||||||
cmd.SetArgs([]string{"existing-network"})
|
cmd.SetArgs([]string{"existing-network"})
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
t.Helper()
|
|
||||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
Upgrading plugin foo/bar from localhost:5000/foo/bar:v0.1.0 to localhost:5000/foo/bar:v1.0.0
|
Upgrading plugin foo/bar from localhost:5000/foo/bar:v0.1.0 to localhost:5000/foo/bar:v1.0.0
|
||||||
Plugin images do not match, are you sure? [y/N]
|
Plugin images do not match, are you sure? [y/N]
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -63,11 +64,12 @@ func runUpgrade(ctx context.Context, dockerCli command.Cli, opts pluginOptions)
|
||||||
|
|
||||||
fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote))
|
fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote))
|
||||||
if !opts.skipRemoteCheck && remote.String() != old.String() {
|
if !opts.skipRemoteCheck && remote.String() != old.String() {
|
||||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?"); !r || err != nil {
|
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "canceling upgrade request")
|
return err
|
||||||
}
|
}
|
||||||
return errors.New("canceling upgrade request")
|
if !r {
|
||||||
|
return errdefs.Cancelled(errors.New("plugin upgrade has been cancelled"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,9 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,9 +32,6 @@ func TestUpgradePromptTermination(t *testing.T) {
|
||||||
// need to set a remote address that does not match the plugin
|
// need to set a remote address that does not match the plugin
|
||||||
// reference sent by the `pluginInspectFunc`
|
// reference sent by the `pluginInspectFunc`
|
||||||
cmd.SetArgs([]string{"foo/bar", "localhost:5000/foo/bar:v1.0.0"})
|
cmd.SetArgs([]string{"foo/bar", "localhost:5000/foo/bar:v1.0.0"})
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
t.Helper()
|
|
||||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
|
||||||
})
|
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "plugin-upgrade-terminate.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "plugin-upgrade-terminate.golden")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,10 @@ import (
|
||||||
"github.com/docker/cli/cli/command/volume"
|
"github.com/docker/cli/cli/command/volume"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/docker/api/types/versions"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
"github.com/fvbommel/sortorder"
|
"github.com/fvbommel/sortorder"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -75,9 +77,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||||
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
|
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
|
||||||
}
|
}
|
||||||
if !options.force {
|
if !options.force {
|
||||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options)); !r || err != nil {
|
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options))
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !r {
|
||||||
|
return errdefs.Cancelled(errors.New("system prune has been cancelled"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pruneFuncs := []func(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){
|
pruneFuncs := []func(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){
|
||||||
container.RunPrune,
|
container.RunPrune,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
@ -18,7 +17,7 @@ func TestPrunePromptPre131DoesNotIncludeBuildCache(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{version: "1.30"})
|
cli := test.NewFakeCli(&fakeClient{version: "1.30"})
|
||||||
cmd := newPruneCommand(cli)
|
cmd := newPruneCommand(cli)
|
||||||
cmd.SetArgs([]string{})
|
cmd.SetArgs([]string{})
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.ErrorContains(t, cmd.Execute(), "system prune has been cancelled")
|
||||||
expected := `WARNING! This will remove:
|
expected := `WARNING! This will remove:
|
||||||
- all stopped containers
|
- all stopped containers
|
||||||
- all networks not used by at least one container
|
- all networks not used by at least one container
|
||||||
|
@ -36,7 +35,7 @@ func TestPrunePromptFilters(t *testing.T) {
|
||||||
cmd := newPruneCommand(cli)
|
cmd := newPruneCommand(cli)
|
||||||
cmd.SetArgs([]string{"--filter", "until=24h", "--filter", "label=hello-world", "--filter", "label!=foo=bar", "--filter", "label=bar=baz"})
|
cmd.SetArgs([]string{"--filter", "until=24h", "--filter", "label=hello-world", "--filter", "label!=foo=bar", "--filter", "label=bar=baz"})
|
||||||
|
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.ErrorContains(t, cmd.Execute(), "system prune has been cancelled")
|
||||||
expected := `WARNING! This will remove:
|
expected := `WARNING! This will remove:
|
||||||
- all stopped containers
|
- all stopped containers
|
||||||
- all networks not used by at least one container
|
- all networks not used by at least one container
|
||||||
|
@ -69,8 +68,5 @@ func TestSystemPrunePromptTermination(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd := newPruneCommand(cli)
|
cmd := newPruneCommand(cli)
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
t.Helper()
|
|
||||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/image"
|
"github.com/docker/cli/cli/command/image"
|
||||||
"github.com/docker/cli/cli/trust"
|
"github.com/docker/cli/cli/trust"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/theupdateframework/notary/client"
|
"github.com/theupdateframework/notary/client"
|
||||||
|
@ -45,12 +46,10 @@ func revokeTrust(ctx context.Context, dockerCLI command.Cli, remote string, opti
|
||||||
if imgRefAndAuth.Tag() == "" && !options.forceYes {
|
if imgRefAndAuth.Tag() == "" && !options.forceYes {
|
||||||
deleteRemote, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
|
deleteRemote, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
return err
|
||||||
return errors.Wrap(err, "aborting action")
|
|
||||||
}
|
}
|
||||||
if !deleteRemote {
|
if !deleteRemote {
|
||||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
return errdefs.Cancelled(errors.New("trust revoke has been cancelled"))
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
|
||||||
"github.com/docker/cli/cli/trust"
|
"github.com/docker/cli/cli/trust"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/cli/internal/test/notary"
|
"github.com/docker/cli/internal/test/notary"
|
||||||
|
@ -58,6 +57,8 @@ func TestTrustRevokeCommandErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrustRevokeCommand(t *testing.T) {
|
func TestTrustRevokeCommand(t *testing.T) {
|
||||||
|
revokeCancelledError := "trust revoke has been cancelled"
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
doc string
|
doc string
|
||||||
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
|
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
|
||||||
|
@ -69,7 +70,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||||
doc: "OfflineErrors_Confirm",
|
doc: "OfflineErrors_Confirm",
|
||||||
notaryRepository: notary.GetOfflineNotaryRepository,
|
notaryRepository: notary.GetOfflineNotaryRepository,
|
||||||
args: []string{"reg-name.io/image"},
|
args: []string{"reg-name.io/image"},
|
||||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
|
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] ",
|
||||||
|
expectedErr: revokeCancelledError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "OfflineErrors_Offline",
|
doc: "OfflineErrors_Offline",
|
||||||
|
@ -87,7 +89,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||||
doc: "UninitializedErrors_Confirm",
|
doc: "UninitializedErrors_Confirm",
|
||||||
notaryRepository: notary.GetUninitializedNotaryRepository,
|
notaryRepository: notary.GetUninitializedNotaryRepository,
|
||||||
args: []string{"reg-name.io/image"},
|
args: []string{"reg-name.io/image"},
|
||||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
|
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] ",
|
||||||
|
expectedErr: revokeCancelledError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "UninitializedErrors_NoTrustData",
|
doc: "UninitializedErrors_NoTrustData",
|
||||||
|
@ -105,7 +108,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||||
doc: "EmptyNotaryRepo_Confirm",
|
doc: "EmptyNotaryRepo_Confirm",
|
||||||
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
|
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
|
||||||
args: []string{"reg-name.io/image"},
|
args: []string{"reg-name.io/image"},
|
||||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
|
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] ",
|
||||||
|
expectedErr: revokeCancelledError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "EmptyNotaryRepo_NoSignedTags",
|
doc: "EmptyNotaryRepo_NoSignedTags",
|
||||||
|
@ -123,7 +127,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||||
doc: "AllSigConfirmation",
|
doc: "AllSigConfirmation",
|
||||||
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
|
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
|
||||||
args: []string{"alpine"},
|
args: []string{"alpine"},
|
||||||
expectedMessage: "Please confirm you would like to delete all signature data for alpine? [y/N] \nAborting action.",
|
expectedMessage: "Please confirm you would like to delete all signature data for alpine? [y/N] ",
|
||||||
|
expectedErr: revokeCancelledError,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,9 +141,9 @@ func TestTrustRevokeCommand(t *testing.T) {
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
if tc.expectedErr != "" {
|
if tc.expectedErr != "" {
|
||||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedErr)
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedErr)
|
||||||
return
|
} else {
|
||||||
|
assert.NilError(t, cmd.Execute())
|
||||||
}
|
}
|
||||||
assert.NilError(t, cmd.Execute())
|
|
||||||
assert.Check(t, is.Contains(cli.OutBuffer().String(), tc.expectedMessage))
|
assert.Check(t, is.Contains(cli.OutBuffer().String(), tc.expectedMessage))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -159,10 +164,6 @@ func TestRevokeTrustPromptTermination(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{})
|
cli := test.NewFakeCli(&fakeClient{})
|
||||||
cmd := newRevokeCommand(cli)
|
cmd := newRevokeCommand(cli)
|
||||||
cmd.SetArgs([]string{"example/trust-demo"})
|
cmd.SetArgs([]string{"example/trust-demo"})
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
t.Helper()
|
|
||||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
|
||||||
})
|
|
||||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "trust-revoke-prompt-termination.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "trust-revoke-prompt-termination.golden")
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,10 +132,12 @@ func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, si
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := maybePromptForSignerRemoval(ctx, dockerCLI, repoName, signerName, isLastSigner, forceYes)
|
ok, err := maybePromptForSignerRemoval(ctx, dockerCLI, repoName, signerName, isLastSigner, forceYes)
|
||||||
if err != nil || !ok {
|
if err != nil {
|
||||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
|
if err := notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
Please confirm you would like to delete all signature data for example/trust-demo? [y/N]
|
Please confirm you would like to delete all signature data for example/trust-demo? [y/N]
|
||||||
Aborting action.
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
mounttypes "github.com/docker/docker/api/types/mount"
|
mounttypes "github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/docker/api/types/versions"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/moby/sys/sequential"
|
"github.com/moby/sys/sequential"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
@ -75,9 +76,7 @@ func PrettyPrint(i any) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromptError error
|
var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated"))
|
||||||
|
|
||||||
var ErrPromptTerminated = PromptError(errors.New("prompt terminated"))
|
|
||||||
|
|
||||||
// PromptForConfirmation requests and checks confirmation from the user.
|
// PromptForConfirmation requests and checks confirmation from the user.
|
||||||
// This will display the provided message followed by ' [y/N] '. If the user
|
// This will display the provided message followed by ' [y/N] '. If the user
|
||||||
|
@ -123,6 +122,8 @@ func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, m
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-notifyCtx.Done():
|
case <-notifyCtx.Done():
|
||||||
|
// print a newline on termination
|
||||||
|
_, _ = fmt.Fprintln(outs, "")
|
||||||
return false, ErrPromptTerminated
|
return false, ErrPromptTerminated
|
||||||
case r := <-result:
|
case r := <-result:
|
||||||
return r, nil
|
return r, nil
|
||||||
|
|
|
@ -32,10 +32,6 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
|
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errdefs.IsCancelled(err) {
|
|
||||||
fmt.Fprintln(dockerCli.Out(), output)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if output != "" {
|
if output != "" {
|
||||||
|
@ -81,8 +77,12 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||||
warning = allVolumesWarning
|
warning = allVolumesWarning
|
||||||
}
|
}
|
||||||
if !options.force {
|
if !options.force {
|
||||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||||
return 0, "", errdefs.Cancelled(errors.New("user cancelled operation"))
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
if !r {
|
||||||
|
return 0, "", errdefs.Cancelled(errors.New("volume prune has been cancelled"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -155,6 +155,7 @@ func TestVolumePrunePromptYes(t *testing.T) {
|
||||||
|
|
||||||
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input))))
|
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input))))
|
||||||
cmd := NewPruneCommand(cli)
|
cmd := NewPruneCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-yes.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-yes.golden")
|
||||||
}
|
}
|
||||||
|
@ -171,7 +172,8 @@ func TestVolumePrunePromptNo(t *testing.T) {
|
||||||
|
|
||||||
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input))))
|
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input))))
|
||||||
cmd := NewPruneCommand(cli)
|
cmd := NewPruneCommand(cli)
|
||||||
assert.NilError(t, cmd.Execute())
|
cmd.SetArgs([]string{})
|
||||||
|
assert.ErrorContains(t, cmd.Execute(), "volume prune has been cancelled")
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-no.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-no.golden")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,7 +198,7 @@ func TestVolumePrunePromptTerminate(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd := NewPruneCommand(cli)
|
cmd := NewPruneCommand(cli)
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli, nil)
|
cmd.SetArgs([]string{})
|
||||||
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-terminate.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-terminate.golden")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
WARNING! This will remove anonymous local volumes not used by at least one container.
|
WARNING! This will remove anonymous local volumes not used by at least one container.
|
||||||
Are you sure you want to continue? [y/N]
|
Are you sure you want to continue? [y/N]
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/docker/cli/cli/version"
|
"github.com/docker/cli/cli/version"
|
||||||
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
|
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/docker/api/types/versions"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -46,6 +47,9 @@ func main() {
|
||||||
}
|
}
|
||||||
os.Exit(sterr.StatusCode)
|
os.Exit(sterr.StatusCode)
|
||||||
}
|
}
|
||||||
|
if errdefs.IsCancelled(err) {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
fmt.Fprintln(dockerCli.Err(), err)
|
fmt.Fprintln(dockerCli.Err(), err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,25 @@
|
||||||
package global
|
package global
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/cli/e2e/internal/fixtures"
|
||||||
|
"github.com/docker/cli/e2e/testutils"
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/cli/internal/test/environment"
|
"github.com/docker/cli/internal/test/environment"
|
||||||
|
"github.com/docker/docker/api/types/versions"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
"gotest.tools/v3/icmd"
|
"gotest.tools/v3/icmd"
|
||||||
|
"gotest.tools/v3/poll"
|
||||||
"gotest.tools/v3/skip"
|
"gotest.tools/v3/skip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,3 +76,185 @@ func TestTCPSchemeUsesHTTPProxyEnv(t *testing.T) {
|
||||||
assert.Equal(t, strings.TrimSpace(result.Stdout()), "99.99.9")
|
assert.Equal(t, strings.TrimSpace(result.Stdout()), "99.99.9")
|
||||||
assert.Equal(t, received, "docker.acme.example.com:2376")
|
assert.Equal(t, received, "docker.acme.example.com:2376")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test that the prompt command exits with 0
|
||||||
|
// when the user sends SIGINT/SIGTERM to the process
|
||||||
|
func TestPromptExitCode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
dir := fixtures.SetupConfigFile(t)
|
||||||
|
t.Cleanup(dir.Remove)
|
||||||
|
|
||||||
|
defaultCmdOpts := []icmd.CmdOp{
|
||||||
|
fixtures.WithConfig(dir.Path()),
|
||||||
|
fixtures.WithNotary,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
run func(t *testing.T) icmd.Cmd
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "volume prune",
|
||||||
|
run: func(t *testing.T) icmd.Cmd {
|
||||||
|
t.Helper()
|
||||||
|
return icmd.Command("docker", "volume", "prune")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "network prune",
|
||||||
|
run: func(t *testing.T) icmd.Cmd {
|
||||||
|
t.Helper()
|
||||||
|
return icmd.Command("docker", "network", "prune")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container prune",
|
||||||
|
run: func(t *testing.T) icmd.Cmd {
|
||||||
|
t.Helper()
|
||||||
|
return icmd.Command("docker", "container", "prune")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image prune",
|
||||||
|
run: func(t *testing.T) icmd.Cmd {
|
||||||
|
t.Helper()
|
||||||
|
return icmd.Command("docker", "image", "prune")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "system prune",
|
||||||
|
run: func(t *testing.T) icmd.Cmd {
|
||||||
|
t.Helper()
|
||||||
|
return icmd.Command("docker", "system", "prune")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "revoke trust",
|
||||||
|
run: func(t *testing.T) icmd.Cmd {
|
||||||
|
t.Helper()
|
||||||
|
return icmd.Command("docker", "trust", "revoke", "example/trust-demo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plugin install",
|
||||||
|
run: func(t *testing.T) icmd.Cmd {
|
||||||
|
t.Helper()
|
||||||
|
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
|
||||||
|
|
||||||
|
pluginDir := testutils.SetupPlugin(t, ctx)
|
||||||
|
t.Cleanup(pluginDir.Remove)
|
||||||
|
|
||||||
|
plugin := "registry:5000/plugin-content-trust-install:latest"
|
||||||
|
|
||||||
|
icmd.RunCommand("docker", "plugin", "create", plugin, pluginDir.Path()).Assert(t, icmd.Success)
|
||||||
|
icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||||
|
icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||||
|
return icmd.Command("docker", "plugin", "install", plugin)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plugin upgrade",
|
||||||
|
run: func(t *testing.T) icmd.Cmd {
|
||||||
|
t.Helper()
|
||||||
|
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
|
||||||
|
|
||||||
|
pluginLatestDir := testutils.SetupPlugin(t, ctx)
|
||||||
|
t.Cleanup(pluginLatestDir.Remove)
|
||||||
|
pluginNextDir := testutils.SetupPlugin(t, ctx)
|
||||||
|
t.Cleanup(pluginNextDir.Remove)
|
||||||
|
|
||||||
|
plugin := "registry:5000/plugin-content-trust-upgrade"
|
||||||
|
|
||||||
|
icmd.RunCommand("docker", "plugin", "create", plugin+":latest", pluginLatestDir.Path()).Assert(t, icmd.Success)
|
||||||
|
icmd.RunCommand("docker", "plugin", "create", plugin+":next", pluginNextDir.Path()).Assert(t, icmd.Success)
|
||||||
|
icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||||
|
icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin+":next"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||||
|
icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||||
|
icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin+":next"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||||
|
icmd.RunCmd(icmd.Command("docker", "plugin", "install", "--disable", "--grant-all-permissions", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||||
|
return icmd.Command("docker", "plugin", "upgrade", plugin+":latest", plugin+":next")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
t.Run("case="+tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
bufioWriter := bufio.NewWriter(buf)
|
||||||
|
|
||||||
|
writeDone := make(chan struct{})
|
||||||
|
w := test.NewWriterWithHook(bufioWriter, func(p []byte) {
|
||||||
|
writeDone <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
drainChCtx, drainChCtxCancel := context.WithCancel(ctx)
|
||||||
|
t.Cleanup(drainChCtxCancel)
|
||||||
|
|
||||||
|
drainChannel(drainChCtx, writeDone)
|
||||||
|
|
||||||
|
r, _ := io.Pipe()
|
||||||
|
defer r.Close()
|
||||||
|
result := icmd.StartCmd(tc.run(t),
|
||||||
|
append(defaultCmdOpts,
|
||||||
|
icmd.WithStdout(w),
|
||||||
|
icmd.WithStderr(w),
|
||||||
|
icmd.WithStdin(r))...)
|
||||||
|
|
||||||
|
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return poll.Error(ctx.Err())
|
||||||
|
default:
|
||||||
|
|
||||||
|
if err := bufioWriter.Flush(); err != nil {
|
||||||
|
return poll.Continue(err.Error())
|
||||||
|
}
|
||||||
|
if strings.Contains(buf.String(), "[y/N]") {
|
||||||
|
return poll.Success()
|
||||||
|
}
|
||||||
|
|
||||||
|
return poll.Continue("command did not prompt for confirmation, instead prompted:\n%s\n", buf.String())
|
||||||
|
}
|
||||||
|
}, poll.WithDelay(100*time.Millisecond), poll.WithTimeout(1*time.Second))
|
||||||
|
|
||||||
|
drainChCtxCancel()
|
||||||
|
|
||||||
|
assert.NilError(t, result.Cmd.Process.Signal(syscall.SIGINT))
|
||||||
|
|
||||||
|
proc, err := result.Cmd.Process.Wait()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, proc.ExitCode(), 0, "expected exit code to be 0, got %d", proc.ExitCode())
|
||||||
|
|
||||||
|
processCtx, processCtxCancel := context.WithTimeout(ctx, time.Second)
|
||||||
|
t.Cleanup(processCtxCancel)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-processCtx.Done():
|
||||||
|
t.Fatal("timed out waiting for new line after process exit")
|
||||||
|
case <-writeDone:
|
||||||
|
buf.Reset()
|
||||||
|
assert.NilError(t, bufioWriter.Flush())
|
||||||
|
assert.Equal(t, buf.String(), "\n", "expected a new line after the process exits from SIGINT")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func drainChannel(ctx context.Context, ch <-chan struct{}) {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ch:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/e2e/internal/fixtures"
|
"github.com/docker/cli/e2e/internal/fixtures"
|
||||||
|
"github.com/docker/cli/e2e/testutils"
|
||||||
"github.com/docker/cli/internal/test/environment"
|
"github.com/docker/cli/internal/test/environment"
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/docker/api/types/versions"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
"gotest.tools/v3/fs"
|
|
||||||
"gotest.tools/v3/icmd"
|
"gotest.tools/v3/icmd"
|
||||||
"gotest.tools/v3/skip"
|
"gotest.tools/v3/skip"
|
||||||
)
|
)
|
||||||
|
@ -31,8 +25,11 @@ func TestInstallWithContentTrust(t *testing.T) {
|
||||||
dir := fixtures.SetupConfigFile(t)
|
dir := fixtures.SetupConfigFile(t)
|
||||||
defer dir.Remove()
|
defer dir.Remove()
|
||||||
|
|
||||||
pluginDir := preparePluginDir(t)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer pluginDir.Remove()
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
pluginDir := testutils.SetupPlugin(t, ctx)
|
||||||
|
t.Cleanup(pluginDir.Remove)
|
||||||
|
|
||||||
icmd.RunCommand("docker", "plugin", "create", pluginName, pluginDir.Path()).Assert(t, icmd.Success)
|
icmd.RunCommand("docker", "plugin", "create", pluginName, pluginDir.Path()).Assert(t, icmd.Success)
|
||||||
result := icmd.RunCmd(icmd.Command("docker", "plugin", "push", pluginName),
|
result := icmd.RunCmd(icmd.Command("docker", "plugin", "push", pluginName),
|
||||||
|
@ -73,46 +70,3 @@ func TestInstallWithContentTrustUntrusted(t *testing.T) {
|
||||||
Err: "Error: remote trust data does not exist",
|
Err: "Error: remote trust data does not exist",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func preparePluginDir(t *testing.T) *fs.Dir {
|
|
||||||
t.Helper()
|
|
||||||
p := &types.PluginConfig{
|
|
||||||
Interface: types.PluginConfigInterface{
|
|
||||||
Socket: "basic.sock",
|
|
||||||
Types: []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}},
|
|
||||||
},
|
|
||||||
Entrypoint: []string{"/basic"},
|
|
||||||
}
|
|
||||||
configJSON, err := json.Marshal(p)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
binPath, err := ensureBasicPluginBin()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
dir := fs.NewDir(t, "plugin_test",
|
|
||||||
fs.WithFile("config.json", string(configJSON), fs.WithMode(0o644)),
|
|
||||||
fs.WithDir("rootfs", fs.WithMode(0o755)),
|
|
||||||
)
|
|
||||||
icmd.RunCommand("/bin/cp", binPath, dir.Join("rootfs", p.Entrypoint[0])).Assert(t, icmd.Success)
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureBasicPluginBin() (string, error) {
|
|
||||||
name := "docker-basic-plugin"
|
|
||||||
p, err := exec.LookPath(name)
|
|
||||||
if err == nil {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
goBin, err := exec.LookPath("/usr/local/go/bin/go")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
installPath := filepath.Join(os.Getenv("GOPATH"), "bin", name)
|
|
||||||
cmd := exec.Command(goBin, "build", "-o", installPath, "./basic")
|
|
||||||
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return "", errors.Wrapf(err, "error building basic plugin bin: %s", string(out))
|
|
||||||
}
|
|
||||||
return installPath, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
package testutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"embed"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
"gotest.tools/v3/fs"
|
||||||
|
"gotest.tools/v3/icmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed plugins/*
|
||||||
|
var plugins embed.FS
|
||||||
|
|
||||||
|
// SetupPlugin builds a plugin and creates a temporary
|
||||||
|
// directory with the plugin's config.json and rootfs.
|
||||||
|
func SetupPlugin(t *testing.T, ctx context.Context) *fs.Dir {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
p := &types.PluginConfig{
|
||||||
|
Linux: types.PluginConfigLinux{
|
||||||
|
Capabilities: []string{"CAP_SYS_ADMIN"},
|
||||||
|
},
|
||||||
|
Interface: types.PluginConfigInterface{
|
||||||
|
Socket: "basic.sock",
|
||||||
|
Types: []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}},
|
||||||
|
},
|
||||||
|
Entrypoint: []string{"/basic"},
|
||||||
|
}
|
||||||
|
configJSON, err := json.Marshal(p)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
binPath, err := buildPlugin(t, ctx)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
dir := fs.NewDir(t, "plugin_test",
|
||||||
|
fs.WithFile("config.json", string(configJSON), fs.WithMode(0o644)),
|
||||||
|
fs.WithDir("rootfs", fs.WithMode(0o755)),
|
||||||
|
)
|
||||||
|
|
||||||
|
icmd.RunCommand("/bin/cp", binPath, dir.Join("rootfs", p.Entrypoint[0])).Assert(t, icmd.Success)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPlugin uses Go to build a plugin from one of the source files in the plugins directory.
|
||||||
|
// It returns the path to the built plugin binary.
|
||||||
|
// To allow for multiple plugins to be built in parallel, the plugin is compiled with a unique
|
||||||
|
// identifier in the binary. This is done by setting a linker flag with the -ldflags option.
|
||||||
|
func buildPlugin(t *testing.T, ctx context.Context) (string, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
randomName, err := randomString()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
goBin, err := exec.LookPath("/usr/local/go/bin/go")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
installPath := filepath.Join(os.Getenv("GOPATH"), "bin", randomName)
|
||||||
|
|
||||||
|
pluginContent, err := plugins.ReadFile("plugins/basic.go")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dir := fs.NewDir(t, "plugin_build")
|
||||||
|
if err := os.WriteFile(dir.Join("main.go"), pluginContent, 0o644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer dir.Remove()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, goBin, "build", "-ldflags",
|
||||||
|
fmt.Sprintf("-X 'main.UNIQUEME=%s'", randomName),
|
||||||
|
"-o", installPath, dir.Join("main.go"))
|
||||||
|
|
||||||
|
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
|
||||||
|
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return "", errors.Wrapf(err, "error building basic plugin bin: %s", string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
return installPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomString() (string, error) {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(b), nil
|
||||||
|
}
|
|
@ -9,6 +9,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// set by the compile flags to get around content sha being the same
|
||||||
|
var (
|
||||||
|
UNIQUEME string
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
p, err := filepath.Abs(filepath.Join("run", "docker", "plugins"))
|
p, err := filepath.Abs(filepath.Join("run", "docker", "plugins"))
|
||||||
if err != nil {
|
if err != nil {
|
|
@ -7,12 +7,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli *FakeCli, assertFunc func(*testing.T, error)) {
|
func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli *FakeCli) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
|
@ -73,11 +74,6 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
|
||||||
t.Logf("command stderr:\n%s\n", cli.ErrBuffer().String())
|
t.Logf("command stderr:\n%s\n", cli.ErrBuffer().String())
|
||||||
t.Fatalf("command %s did not return after SIGINT", cmd.Name())
|
t.Fatalf("command %s did not return after SIGINT", cmd.Name())
|
||||||
case err := <-errChan:
|
case err := <-errChan:
|
||||||
if assertFunc != nil {
|
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||||
assertFunc(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,27 +4,22 @@ import (
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriterWithHook is an io.Writer that calls a hook function
|
type writerWithHook struct {
|
||||||
// after every write.
|
|
||||||
// This is useful in testing to wait for a write to complete,
|
|
||||||
// or to check what was written.
|
|
||||||
// To create a WriterWithHook use the NewWriterWithHook function.
|
|
||||||
type WriterWithHook struct {
|
|
||||||
actualWriter io.Writer
|
actualWriter io.Writer
|
||||||
hook func([]byte)
|
hook func([]byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write writes p to the actual writer and then calls the hook function.
|
func (w *writerWithHook) Write(p []byte) (n int, err error) {
|
||||||
func (w *WriterWithHook) Write(p []byte) (n int, err error) {
|
|
||||||
defer w.hook(p)
|
defer w.hook(p)
|
||||||
return w.actualWriter.Write(p)
|
return w.actualWriter.Write(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ io.Writer = (*WriterWithHook)(nil)
|
var _ io.Writer = (*writerWithHook)(nil)
|
||||||
|
|
||||||
// NewWriterWithHook returns a new WriterWithHook that still writes to the actualWriter
|
// NewWriterWithHook returns a io.Writer that still
|
||||||
// but also calls the hook function after every write.
|
// writes to the actualWriter but also calls the hook function
|
||||||
// The hook function is useful for testing, or waiting for a write to complete.
|
// after every write. It is useful to use this function when
|
||||||
func NewWriterWithHook(actualWriter io.Writer, hook func([]byte)) *WriterWithHook {
|
// you need to wait for a writer to complete writing inside a test.
|
||||||
return &WriterWithHook{actualWriter: actualWriter, hook: hook}
|
func NewWriterWithHook(actualWriter io.Writer, hook func([]byte)) *writerWithHook {
|
||||||
|
return &writerWithHook{actualWriter: actualWriter, hook: hook}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue