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:
Sebastiaan van Stijn
2024-05-31 22:01:29 +02:00
committed by GitHub
12 changed files with 148 additions and 36 deletions
+6 -1
View File
@@ -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,
}
+32 -14
View File
@@ -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) {
+3 -1
View File
@@ -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"},
+30
View File
@@ -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(),
+40 -6
View File
@@ -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}))
+5 -9
View File
@@ -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`)
+1 -1
View File
@@ -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
View File
@@ -2,7 +2,8 @@
{
"Name": "current",
"Metadata": {
"Description": "description of current"
"Description": "description of current",
"MyCustomMetadata": "MyCustomMetadataValue"
},
"Endpoints": {
"docker": {
+5
View File
@@ -0,0 +1,5 @@
"context1"
"context2"
"context3"
"current"
"default"
+5
View File
@@ -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"}
+3 -3
View File
@@ -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")
}
+16
View File
@@ -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)
}