Merge pull request #3847 from thaJeztah/context_lazy_evaluate

context: implement lazy loading, and other improvements
This commit is contained in:
Sebastiaan van Stijn 2022-11-29 00:22:02 +01:00 committed by GitHub
commit ab794859fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 124 additions and 45 deletions

View File

@ -2,12 +2,14 @@ package command
import ( import (
"context" "context"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
@ -78,6 +80,8 @@ type DockerCli struct {
contentTrust bool contentTrust bool
contextStore store.Store contextStore store.Store
currentContext string currentContext string
init sync.Once
initErr error
dockerEndpoint docker.Endpoint dockerEndpoint docker.Endpoint
contextStoreConfig store.Config contextStoreConfig store.Config
initTimeout time.Duration initTimeout time.Duration
@ -91,6 +95,7 @@ func (cli *DockerCli) DefaultVersion() string {
// CurrentVersion returns the API version currently negotiated, or the default // CurrentVersion returns the API version currently negotiated, or the default
// version otherwise. // version otherwise.
func (cli *DockerCli) CurrentVersion() string { func (cli *DockerCli) CurrentVersion() string {
_ = cli.initialize()
if cli.client == nil { if cli.client == nil {
return api.DefaultVersion return api.DefaultVersion
} }
@ -99,6 +104,10 @@ func (cli *DockerCli) CurrentVersion() string {
// Client returns the APIClient // Client returns the APIClient
func (cli *DockerCli) Client() client.APIClient { func (cli *DockerCli) Client() client.APIClient {
if err := cli.initialize(); err != nil {
_, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", err)
os.Exit(1)
}
return cli.client return cli.client
} }
@ -143,7 +152,7 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
// ServerInfo returns the server version details for the host this client is // ServerInfo returns the server version details for the host this client is
// connected to // connected to
func (cli *DockerCli) ServerInfo() ServerInfo { func (cli *DockerCli) ServerInfo() ServerInfo {
// TODO(thaJeztah) make ServerInfo() lazily load the info (ping only when needed) _ = cli.initialize()
return cli.serverInfo return cli.serverInfo
} }
@ -203,8 +212,6 @@ func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClien
// Initialize the dockerCli runs initialization that must happen after command // Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed. // line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error { func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error {
var err error
for _, o := range ops { for _, o := range ops {
if err := o(cli); err != nil { if err := o(cli); err != nil {
return err return err
@ -232,18 +239,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize
return ResolveDefaultContext(cli.options, cli.contextStoreConfig) return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
}, },
} }
cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, resolveContextName(opts, cli.configFile))
if err != nil {
return errors.Wrap(err, "unable to resolve docker endpoint")
}
if cli.client == nil {
cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
if err != nil {
return err
}
}
cli.initializeFromClient()
return nil return nil
} }
@ -282,6 +277,9 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
} }
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) { func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
if s == nil {
return docker.Endpoint{}, fmt.Errorf("no context store initialized")
}
ctxMeta, err := s.GetMetadata(contextName) ctxMeta, err := s.GetMetadata(contextName)
if err != nil { if err != nil {
return docker.Endpoint{}, err return docker.Endpoint{}, err
@ -331,7 +329,7 @@ func (cli *DockerCli) getInitTimeout() time.Duration {
func (cli *DockerCli) initializeFromClient() { func (cli *DockerCli) initializeFromClient() {
ctx := context.Background() ctx := context.Background()
if !strings.HasPrefix(cli.DockerEndpoint().Host, "ssh://") { if !strings.HasPrefix(cli.dockerEndpoint.Host, "ssh://") {
// @FIXME context.WithTimeout doesn't work with connhelper / ssh connections // @FIXME context.WithTimeout doesn't work with connhelper / ssh connections
// time="2020-04-10T10:16:26Z" level=warning msg="commandConn.CloseWrite: commandconn: failed to wait: signal: killed" // time="2020-04-10T10:16:26Z" level=warning msg="commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
var cancel func() var cancel func()
@ -428,9 +426,39 @@ func resolveContextName(opts *cliflags.ClientOptions, config *configfile.ConfigF
// DockerEndpoint returns the current docker endpoint // DockerEndpoint returns the current docker endpoint
func (cli *DockerCli) DockerEndpoint() docker.Endpoint { func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
if err := cli.initialize(); err != nil {
// Note that we're not terminating here, as this function may be used
// in cases where we're able to continue.
_, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr)
}
return cli.dockerEndpoint return cli.dockerEndpoint
} }
func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) {
cn := cli.CurrentContext()
if cn == DefaultContextName {
return resolveDefaultDockerEndpoint(cli.options)
}
return resolveDockerEndpoint(cli.contextStore, cn)
}
func (cli *DockerCli) initialize() error {
cli.init.Do(func() {
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
if cli.initErr != nil {
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
return
}
if cli.client == nil {
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
return
}
}
cli.initializeFromClient()
})
return cli.initErr
}
// Apply all the operation on the cli // Apply all the operation on the cli
func (cli *DockerCli) Apply(ops ...DockerCliOption) error { func (cli *DockerCli) Apply(ops ...DockerCliOption) error {
for _, op := range ops { for _, op := range ops {

View File

@ -118,7 +118,7 @@ func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
} }
func TestInitializeFromClient(t *testing.T) { func TestInitializeFromClient(t *testing.T) {
defaultVersion := "v1.55" const defaultVersion = "v1.55"
testcases := []struct { testcases := []struct {
doc string doc string
@ -160,7 +160,8 @@ func TestInitializeFromClient(t *testing.T) {
} }
cli := &DockerCli{client: apiclient} cli := &DockerCli{client: apiclient}
cli.initializeFromClient() err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err)
assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer) assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
assert.Equal(t, apiclient.negotiated, testcase.negotiated) assert.Equal(t, apiclient.negotiated, testcase.negotiated)
}) })
@ -202,6 +203,7 @@ func TestInitializeFromClientHangs(t *testing.T) {
cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond} cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
err := cli.Initialize(flags.NewClientOptions()) err := cli.Initialize(flags.NewClientOptions())
assert.Check(t, err) assert.Check(t, err)
cli.CurrentVersion()
close(initializedCh) close(initializedCh)
}() }()

View File

@ -50,26 +50,54 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
} }
var ( var (
curContext = dockerCli.CurrentContext() curContext = dockerCli.CurrentContext()
curFound bool
contexts []*formatter.ClientContext contexts []*formatter.ClientContext
) )
for _, rawMeta := range contextMap { for _, rawMeta := range contextMap {
isCurrent := rawMeta.Name == curContext isCurrent := rawMeta.Name == curContext
if isCurrent {
curFound = true
}
meta, err := command.GetDockerContext(rawMeta) meta, err := command.GetDockerContext(rawMeta)
if err != nil { if err != nil {
return err // Add a stub-entry to the list, including the error-message
// indicating that the context couldn't be loaded.
contexts = append(contexts, &formatter.ClientContext{
Name: rawMeta.Name,
Current: isCurrent,
Error: err.Error(),
})
continue
} }
var errMsg string
dockerEndpoint, err := docker.EndpointFromContext(rawMeta) dockerEndpoint, err := docker.EndpointFromContext(rawMeta)
if err != nil { if err != nil {
return err errMsg = err.Error()
} }
desc := formatter.ClientContext{ desc := formatter.ClientContext{
Name: rawMeta.Name, Name: rawMeta.Name,
Current: isCurrent, Current: isCurrent,
Description: meta.Description, Description: meta.Description,
DockerEndpoint: dockerEndpoint.Host, DockerEndpoint: dockerEndpoint.Host,
Error: errMsg,
} }
contexts = append(contexts, &desc) contexts = append(contexts, &desc)
} }
if !curFound {
// The currently specified context wasn't found. We add a stub-entry
// to the list, including the error-message indicating that the context
// wasn't found.
var errMsg string
_, err := dockerCli.ContextStore().GetMetadata(curContext)
if err != nil {
errMsg = err.Error()
}
contexts = append(contexts, &formatter.ClientContext{
Name: curContext,
Current: true,
Error: errMsg,
})
}
sort.Slice(contexts, func(i, j int) bool { sort.Slice(contexts, func(i, j int) bool {
return sortorder.NaturalLess(contexts[i].Name, contexts[j].Name) return sortorder.NaturalLess(contexts[i].Name, contexts[j].Name)
}) })

View File

@ -39,3 +39,11 @@ func TestListQuiet(t *testing.T) {
assert.NilError(t, runList(cli, &listOptions{quiet: true})) assert.NilError(t, runList(cli, &listOptions{quiet: true}))
golden.Assert(t, cli.OutBuffer().String(), "quiet-list.golden") golden.Assert(t, cli.OutBuffer().String(), "quiet-list.golden")
} }
func TestListError(t *testing.T) {
cli := makeFakeCli(t)
cli.SetCurrentContext("nosuchcontext")
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{}))
golden.Assert(t, cli.OutBuffer().String(), "list-with-error.golden")
}

View File

@ -0,0 +1,3 @@
NAME DESCRIPTION DOCKER ENDPOINT ERROR
default Current DOCKER_HOST based configuration unix:///var/run/docker.sock
nosuchcontext * context "nosuchcontext": context not found: …

View File

@ -1,4 +1,4 @@
NAME DESCRIPTION DOCKER ENDPOINT NAME DESCRIPTION DOCKER ENDPOINT ERROR
current * description of current https://someswarmserver.example.com current * description of current https://someswarmserver.example.com
default Current DOCKER_HOST based configuration unix:///var/run/docker.sock default Current DOCKER_HOST based configuration unix:///var/run/docker.sock
other description of other https://someswarmserver.example.com other description of other https://someswarmserver.example.com

View File

@ -1,20 +1,22 @@
package formatter package formatter
const ( const (
// ClientContextTableFormat is the default client context format // ClientContextTableFormat is the default client context format.
ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}" ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.Error}}"
dockerEndpointHeader = "DOCKER ENDPOINT" dockerEndpointHeader = "DOCKER ENDPOINT"
quietContextFormat = "{{.Name}}" quietContextFormat = "{{.Name}}"
maxErrLength = 45
) )
// NewClientContextFormat returns a Format for rendering using a Context // NewClientContextFormat returns a Format for rendering using a Context
func NewClientContextFormat(source string, quiet bool) Format { func NewClientContextFormat(source string, quiet bool) Format {
if quiet { if quiet {
return Format(quietContextFormat) return quietContextFormat
} }
if source == TableFormatKey { if source == TableFormatKey {
return Format(ClientContextTableFormat) return ClientContextTableFormat
} }
return Format(source) return Format(source)
} }
@ -25,6 +27,7 @@ type ClientContext struct {
Description string Description string
DockerEndpoint string DockerEndpoint string
Current bool Current bool
Error string
} }
// ClientContextWrite writes formatted contexts using the Context // ClientContextWrite writes formatted contexts using the Context
@ -51,6 +54,7 @@ func newClientContextContext() *clientContextContext {
"Name": NameHeader, "Name": NameHeader,
"Description": DescriptionHeader, "Description": DescriptionHeader,
"DockerEndpoint": dockerEndpointHeader, "DockerEndpoint": dockerEndpointHeader,
"Error": ErrorHeader,
} }
return &ctx return &ctx
} }
@ -75,6 +79,12 @@ func (c *clientContextContext) DockerEndpoint() string {
return c.c.DockerEndpoint return c.c.DockerEndpoint
} }
// Error returns the truncated error (if any) that occurred when loading the context.
func (c *clientContextContext) Error() string {
// TODO(thaJeztah) add "--no-trunc" option to context ls and set default to 30 cols to match "docker service ps"
return Ellipsis(c.c.Error, maxErrLength)
}
// KubernetesEndpoint returns the kubernetes endpoint. // KubernetesEndpoint returns the kubernetes endpoint.
// //
// Deprecated: support for kubernetes endpoints in contexts has been removed, and this formatting option will always be empty. // Deprecated: support for kubernetes endpoints in contexts has been removed, and this formatting option will always be empty.

View File

@ -16,6 +16,7 @@ const (
StatusHeader = "STATUS" StatusHeader = "STATUS"
PortsHeader = "PORTS" PortsHeader = "PORTS"
ImageHeader = "IMAGE" ImageHeader = "IMAGE"
ErrorHeader = "ERROR"
ContainerIDHeader = "CONTAINER ID" ContainerIDHeader = "CONTAINER ID"
) )

View File

@ -20,7 +20,6 @@ const (
taskIDHeader = "ID" taskIDHeader = "ID"
desiredStateHeader = "DESIRED STATE" desiredStateHeader = "DESIRED STATE"
currentStateHeader = "CURRENT STATE" currentStateHeader = "CURRENT STATE"
errorHeader = "ERROR"
maxErrLength = 30 maxErrLength = 30
) )
@ -61,7 +60,7 @@ func FormatWrite(ctx formatter.Context, tasks []swarm.Task, names map[string]str
"Node": nodeHeader, "Node": nodeHeader,
"DesiredState": desiredStateHeader, "DesiredState": desiredStateHeader,
"CurrentState": currentStateHeader, "CurrentState": currentStateHeader,
"Error": errorHeader, "Error": formatter.ErrorHeader,
"Ports": formatter.PortsHeader, "Ports": formatter.PortsHeader,
} }
return ctx.Write(&taskCtx, render) return ctx.Write(&taskCtx, render)
@ -124,11 +123,11 @@ func (c *taskContext) CurrentState() string {
func (c *taskContext) Error() string { func (c *taskContext) Error() string {
// Trim and quote the error message. // Trim and quote the error message.
taskErr := c.task.Status.Err taskErr := c.task.Status.Err
if c.trunc && len(taskErr) > maxErrLength { if c.trunc {
taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1]) taskErr = formatter.Ellipsis(taskErr, maxErrLength)
} }
if len(taskErr) > 0 { if len(taskErr) > 0 {
taskErr = fmt.Sprintf("\"%s\"", taskErr) taskErr = fmt.Sprintf(`"%s"`, taskErr)
} }
return taskErr return taskErr
} }

View File

@ -59,7 +59,7 @@ func parseTypedOrMap(payload []byte, getter TypeGetter) (interface{}, error) {
func (s *metadataStore) get(name string) (Metadata, error) { func (s *metadataStore) get(name string) (Metadata, error) {
m, err := s.getByID(contextdirOf(name)) m, err := s.getByID(contextdirOf(name))
if err != nil { if err != nil {
return m, errors.Wrapf(err, "load context %q", name) return m, errors.Wrapf(err, "context %q", name)
} }
return m, nil return m, nil
} }
@ -68,7 +68,7 @@ func (s *metadataStore) getByID(id contextdir) (Metadata, error) {
bytes, err := os.ReadFile(filepath.Join(s.contextDir(id), metaFile)) bytes, err := os.ReadFile(filepath.Join(s.contextDir(id), metaFile))
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return Metadata{}, errdefs.NotFound(errors.Wrap(err, "context does not exist")) return Metadata{}, errdefs.NotFound(errors.Wrap(err, "context not found"))
} }
return Metadata{}, err return Metadata{}, err
} }

View File

@ -1,3 +1,3 @@
NAME DESCRIPTION DOCKER ENDPOINT NAME DESCRIPTION DOCKER ENDPOINT ERROR
default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock
remote my remote cluster ssh://someserver remote my remote cluster ssh://someserver

View File

@ -1,3 +1,3 @@
NAME DESCRIPTION DOCKER ENDPOINT NAME DESCRIPTION DOCKER ENDPOINT ERROR
default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock
test unix:///var/run/docker.sock test unix:///var/run/docker.sock

View File

@ -1,3 +1,3 @@
NAME DESCRIPTION DOCKER ENDPOINT NAME DESCRIPTION DOCKER ENDPOINT ERROR
default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock
remote my remote cluster ssh://someserver remote my remote cluster ssh://someserver