Merge pull request #5095 from thaJeztah/26.1_backport_context_type_stub
[26.1 backport] context list: temporarily add ContextType to JSON output
This commit is contained in:
@@ -24,6 +24,10 @@ type CreateOptions struct {
|
||||
Description string
|
||||
Docker map[string]string
|
||||
From string
|
||||
|
||||
// Additional Metadata to store in the context. This option is not
|
||||
// currently exposed to the user.
|
||||
metaData map[string]any
|
||||
}
|
||||
|
||||
func longCreateDescription() string {
|
||||
@@ -94,7 +98,8 @@ func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error {
|
||||
docker.DockerEndpoint: dockerEP,
|
||||
},
|
||||
Metadata: command.DockerContext{
|
||||
Description: o.Description,
|
||||
Description: o.Description,
|
||||
AdditionalFields: o.metaData,
|
||||
},
|
||||
Name: o.Name,
|
||||
}
|
||||
|
||||
@@ -8,14 +8,18 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestExportImportWithFile(t *testing.T) {
|
||||
contextFile := filepath.Join(t.TempDir(), "exported")
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "test")
|
||||
createTestContext(t, cli, "test", map[string]any{
|
||||
"MyCustomMetadata": t.Name(),
|
||||
})
|
||||
cli.ErrBuffer().Reset()
|
||||
assert.NilError(t, RunExport(cli, &ExportOptions{
|
||||
ContextName: "test",
|
||||
@@ -29,18 +33,26 @@ func TestExportImportWithFile(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
context2, err := cli.ContextStore().GetMetadata("test2")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
|
||||
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
|
||||
assert.Equal(t, "test", context1.Name)
|
||||
assert.Equal(t, "test2", context2.Name)
|
||||
|
||||
assert.Equal(t, "test2\n", cli.OutBuffer().String())
|
||||
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
|
||||
assert.Check(t, is.DeepEqual(context1.Metadata, command.DockerContext{
|
||||
Description: "description of test",
|
||||
AdditionalFields: map[string]any{"MyCustomMetadata": t.Name()},
|
||||
}))
|
||||
|
||||
assert.Check(t, is.DeepEqual(context1.Endpoints, context2.Endpoints))
|
||||
assert.Check(t, is.DeepEqual(context1.Metadata, context2.Metadata))
|
||||
assert.Check(t, is.Equal("test", context1.Name))
|
||||
assert.Check(t, is.Equal("test2", context2.Name))
|
||||
|
||||
assert.Check(t, is.Equal("test2\n", cli.OutBuffer().String()))
|
||||
assert.Check(t, is.Equal("Successfully imported context \"test2\"\n", cli.ErrBuffer().String()))
|
||||
}
|
||||
|
||||
func TestExportImportPipe(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "test")
|
||||
createTestContext(t, cli, "test", map[string]any{
|
||||
"MyCustomMetadata": t.Name(),
|
||||
})
|
||||
cli.ErrBuffer().Reset()
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, RunExport(cli, &ExportOptions{
|
||||
@@ -56,13 +68,19 @@ func TestExportImportPipe(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
context2, err := cli.ContextStore().GetMetadata("test2")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
|
||||
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
|
||||
assert.Equal(t, "test", context1.Name)
|
||||
assert.Equal(t, "test2", context2.Name)
|
||||
|
||||
assert.Equal(t, "test2\n", cli.OutBuffer().String())
|
||||
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
|
||||
assert.Check(t, is.DeepEqual(context1.Metadata, command.DockerContext{
|
||||
Description: "description of test",
|
||||
AdditionalFields: map[string]any{"MyCustomMetadata": t.Name()},
|
||||
}))
|
||||
|
||||
assert.Check(t, is.DeepEqual(context1.Endpoints, context2.Endpoints))
|
||||
assert.Check(t, is.DeepEqual(context1.Metadata, context2.Metadata))
|
||||
assert.Check(t, is.Equal("test", context1.Name))
|
||||
assert.Check(t, is.Equal("test2", context2.Name))
|
||||
|
||||
assert.Check(t, is.Equal("test2\n", cli.OutBuffer().String()))
|
||||
assert.Check(t, is.Equal("Successfully imported context \"test2\"\n", cli.ErrBuffer().String()))
|
||||
}
|
||||
|
||||
func TestExportExistingFile(t *testing.T) {
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
|
||||
func TestInspect(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "current", map[string]any{
|
||||
"MyCustomMetadata": "MyCustomMetadataValue",
|
||||
})
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runInspect(cli, inspectOptions{
|
||||
refs: []string{"current"},
|
||||
|
||||
@@ -66,6 +66,8 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
Name: rawMeta.Name,
|
||||
Current: isCurrent,
|
||||
Error: err.Error(),
|
||||
|
||||
ContextType: getContextType(nil, opts.format),
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -80,6 +82,8 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
Description: meta.Description,
|
||||
DockerEndpoint: dockerEndpoint.Host,
|
||||
Error: errMsg,
|
||||
|
||||
ContextType: getContextType(meta.AdditionalFields, opts.format),
|
||||
}
|
||||
contexts = append(contexts, &desc)
|
||||
}
|
||||
@@ -96,6 +100,8 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
Name: curContext,
|
||||
Current: true,
|
||||
Error: errMsg,
|
||||
|
||||
ContextType: getContextType(nil, opts.format),
|
||||
})
|
||||
}
|
||||
sort.Slice(contexts, func(i, j int) bool {
|
||||
@@ -111,6 +117,30 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getContextType sets the LegacyContextType field for compatibility with
|
||||
// Visual Studio, which depends on this field from the "cloud integration"
|
||||
// wrapper.
|
||||
//
|
||||
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/contextmetadata.go#L28-L34
|
||||
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/store.go#L34-L51
|
||||
//
|
||||
// TODO(thaJeztah): remove this and [ClientContext.ContextType] once Visual Studio is updated to no longer depend on this.
|
||||
func getContextType(meta map[string]any, format string) string {
|
||||
if format != formatter.JSONFormat && format != formatter.JSONFormatKey {
|
||||
// We only need the ContextType field when formatting as JSON,
|
||||
// which is the format-string used by Visual Studio to detect the
|
||||
// context-type.
|
||||
return ""
|
||||
}
|
||||
if ct, ok := meta["Type"]; ok {
|
||||
// If the context on-disk has a context-type (ecs, aci), return it.
|
||||
return ct.(string)
|
||||
}
|
||||
|
||||
// Use the default context-type.
|
||||
return "moby"
|
||||
}
|
||||
|
||||
func format(dockerCli command.Cli, opts *listOptions, contexts []*formatter.ClientContext) error {
|
||||
contextCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
|
||||
@@ -4,36 +4,70 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func createTestContext(t *testing.T, cli command.Cli, name string) {
|
||||
func createTestContexts(t *testing.T, cli command.Cli, name ...string) {
|
||||
t.Helper()
|
||||
for _, n := range name {
|
||||
createTestContext(t, cli, n, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func createTestContext(t *testing.T, cli command.Cli, name string, metaData map[string]any) {
|
||||
t.Helper()
|
||||
|
||||
err := RunCreate(cli, &CreateOptions{
|
||||
Name: name,
|
||||
Description: "description of " + name,
|
||||
Docker: map[string]string{keyHost: "https://someswarmserver.example.com"},
|
||||
|
||||
metaData: metaData,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContext(t, cli, "unset")
|
||||
createTestContexts(t, cli, "current", "other", "unset")
|
||||
cli.SetCurrentContext("current")
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{}))
|
||||
golden.Assert(t, cli.OutBuffer().String(), "list.golden")
|
||||
}
|
||||
|
||||
func TestListJSON(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current", nil)
|
||||
createTestContext(t, cli, "context1", map[string]any{"Type": "aci"})
|
||||
createTestContext(t, cli, "context2", map[string]any{"Type": "ecs"})
|
||||
createTestContext(t, cli, "context3", map[string]any{"Type": "moby"})
|
||||
cli.SetCurrentContext("current")
|
||||
|
||||
t.Run("format={{json .}}", func(t *testing.T) {
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{format: formatter.JSONFormat}))
|
||||
golden.Assert(t, cli.OutBuffer().String(), "list-json.golden")
|
||||
})
|
||||
|
||||
t.Run("format=json", func(t *testing.T) {
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{format: formatter.JSONFormatKey}))
|
||||
golden.Assert(t, cli.OutBuffer().String(), "list-json.golden")
|
||||
})
|
||||
|
||||
t.Run("format={{ json .Name }}", func(t *testing.T) {
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{format: `{{ json .Name }}`}))
|
||||
golden.Assert(t, cli.OutBuffer().String(), "list-json-name.golden")
|
||||
})
|
||||
}
|
||||
|
||||
func TestListQuiet(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
cli.SetCurrentContext("current")
|
||||
cli.OutBuffer().Reset()
|
||||
assert.NilError(t, runList(cli, &listOptions{quiet: true}))
|
||||
|
||||
@@ -13,8 +13,7 @@ import (
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
assert.NilError(t, RunRemove(cli, RemoveOptions{}, []string{"other"}))
|
||||
_, err := cli.ContextStore().GetMetadata("current")
|
||||
assert.NilError(t, err)
|
||||
@@ -24,8 +23,7 @@ func TestRemove(t *testing.T) {
|
||||
|
||||
func TestRemoveNotAContext(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
err := RunRemove(cli, RemoveOptions{}, []string{"not-a-context"})
|
||||
assert.ErrorContains(t, err, `context "not-a-context" does not exist`)
|
||||
|
||||
@@ -35,8 +33,7 @@ func TestRemoveNotAContext(t *testing.T) {
|
||||
|
||||
func TestRemoveCurrent(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
cli.SetCurrentContext("current")
|
||||
err := RunRemove(cli, RemoveOptions{}, []string{"current"})
|
||||
assert.ErrorContains(t, err, `context "current" is in use, set -f flag to force remove`)
|
||||
@@ -50,8 +47,7 @@ func TestRemoveCurrentForce(t *testing.T) {
|
||||
assert.NilError(t, testCfg.Save())
|
||||
|
||||
cli := makeFakeCli(t, withCliConfig(testCfg))
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContexts(t, cli, "current", "other")
|
||||
cli.SetCurrentContext("current")
|
||||
assert.NilError(t, RunRemove(cli, RemoveOptions{Force: true}, []string{"current"}))
|
||||
reloadedConfig, err := config.Load(configDir)
|
||||
@@ -61,7 +57,7 @@ func TestRemoveCurrentForce(t *testing.T) {
|
||||
|
||||
func TestRemoveDefault(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "other")
|
||||
createTestContext(t, cli, "other", nil)
|
||||
cli.SetCurrentContext("current")
|
||||
err := RunRemove(cli, RemoveOptions{}, []string{"default"})
|
||||
assert.ErrorContains(t, err, `default: context "default" cannot be removed`)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestShow(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "current")
|
||||
createTestContext(t, cli, "current", nil)
|
||||
cli.SetCurrentContext("current")
|
||||
|
||||
cli.OutBuffer().Reset()
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@
|
||||
{
|
||||
"Name": "current",
|
||||
"Metadata": {
|
||||
"Description": "description of current"
|
||||
"Description": "description of current",
|
||||
"MyCustomMetadata": "MyCustomMetadataValue"
|
||||
},
|
||||
"Endpoints": {
|
||||
"docker": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"context1"
|
||||
"context2"
|
||||
"context3"
|
||||
"current"
|
||||
"default"
|
||||
@@ -0,0 +1,5 @@
|
||||
{"Name":"context1","Description":"description of context1","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"aci"}
|
||||
{"Name":"context2","Description":"description of context2","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"ecs"}
|
||||
{"Name":"context3","Description":"description of context3","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"moby"}
|
||||
{"Name":"current","Description":"description of current","DockerEndpoint":"https://someswarmserver.example.com","Current":true,"Error":"","ContextType":"moby"}
|
||||
{"Name":"default","Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","Current":false,"Error":"","ContextType":"moby"}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestUpdateDescriptionOnly(t *testing.T) {
|
||||
@@ -34,7 +34,7 @@ func TestUpdateDescriptionOnly(t *testing.T) {
|
||||
|
||||
func TestUpdateDockerOnly(t *testing.T) {
|
||||
cli := makeFakeCli(t)
|
||||
createTestContext(t, cli, "test")
|
||||
createTestContext(t, cli, "test", nil)
|
||||
assert.NilError(t, RunUpdate(cli, &UpdateOptions{
|
||||
Name: "test",
|
||||
Docker: map[string]string{
|
||||
@@ -46,7 +46,7 @@ func TestUpdateDockerOnly(t *testing.T) {
|
||||
dc, err := command.GetDockerContext(c)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, dc.Description, "description of test")
|
||||
assert.Check(t, cmp.Contains(c.Endpoints, docker.DockerEndpoint))
|
||||
assert.Check(t, is.Contains(c.Endpoints, docker.DockerEndpoint))
|
||||
assert.Equal(t, c.Endpoints[docker.DockerEndpoint].(docker.EndpointMeta).Host, "tcp://some-host")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package formatter
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
// ClientContextTableFormat is the default client context format.
|
||||
ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.Error}}"
|
||||
@@ -28,6 +30,13 @@ type ClientContext struct {
|
||||
DockerEndpoint string
|
||||
Current bool
|
||||
Error string
|
||||
|
||||
// ContextType is a temporary field for compatibility with
|
||||
// Visual Studio, which depends on this from the "cloud integration"
|
||||
// wrapper.
|
||||
//
|
||||
// Deprecated: this type is only for backward-compatibility. Do not use.
|
||||
ContextType string `json:"ContextType,omitempty"`
|
||||
}
|
||||
|
||||
// ClientContextWrite writes formatted contexts using the Context
|
||||
@@ -60,6 +69,13 @@ func newClientContextContext() *clientContextContext {
|
||||
}
|
||||
|
||||
func (c *clientContextContext) MarshalJSON() ([]byte, error) {
|
||||
if c.c.ContextType != "" {
|
||||
// We only have ContextType set for plain "json" or "{{json .}}" formatting,
|
||||
// so we should be able to just use the default json.Marshal with no
|
||||
// special handling.
|
||||
return json.Marshal(c.c)
|
||||
}
|
||||
// FIXME(thaJeztah): why do we need a special marshal function here?
|
||||
return MarshalJSON(c)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user