From 81c6f259dbf37b181f22efd7ce3eb17dce8b0725 Mon Sep 17 00:00:00 2001 From: Valery Masiutsin Date: Sat, 5 Aug 2023 17:06:54 +0100 Subject: [PATCH 1/2] Trying to read docker host/socket location from the current docker context Resolves issues with lima/colima/docker desktop (they all have one thing in common - dockerd sockets are in custom locations) Signed-off-by: Valery Masiutsin --- go.mod | 1 + go.sum | 2 + internal/docker/client.go | 27 ++- internal/docker/config.go | 84 +++++++++ internal/docker/config_test.go | 208 +++++++++++++++++++++ internal/docker/test-fixtures/config0.json | 4 + internal/docker/test-fixtures/config1.json | 5 + internal/docker/test-fixtures/meta0.json | 1 + internal/docker/test-fixtures/meta1.json | 1 + internal/docker/test-fixtures/meta2.json | 1 + 10 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 internal/docker/config.go create mode 100644 internal/docker/config_test.go create mode 100644 internal/docker/test-fixtures/config0.json create mode 100644 internal/docker/test-fixtures/config1.json create mode 100644 internal/docker/test-fixtures/meta0.json create mode 100644 internal/docker/test-fixtures/meta1.json create mode 100644 internal/docker/test-fixtures/meta2.json diff --git a/go.mod b/go.mod index cbb9352c..a2fe857b 100644 --- a/go.mod +++ b/go.mod @@ -87,6 +87,7 @@ require ( require ( cloud.google.com/go/compute v1.19.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/fvbommel/sortorder v1.1.0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/kr/pretty v0.2.1 // indirect golang.org/x/mod v0.10.0 // indirect diff --git a/go.sum b/go.sum index 02ca1498..c177951e 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= +github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= diff --git a/internal/docker/client.go b/internal/docker/client.go index 17d5610e..66b6c055 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -6,12 +6,37 @@ import ( "os" "strings" + "github.com/anchore/stereoscope/internal/log" "github.com/docker/cli/cli/connhelper" "github.com/docker/docker/client" ) func GetClient() (*client.Client, error) { - var clientOpts = []client.Opt{ + _, found := os.LookupEnv("DOCKER_HOST") + if !found { + log.Debugf("no explicit DOCKER_HOST defined") + + log.Debugf("reading docker configuration: %s", configFileName) + + cfg, err := loadConfig(configFileName) + if err == nil { + host, err := processConfig(os.Getenv(envOverrideContext), contextsDir, cfg) + if err != nil { + return nil, err + } + log.Debugf("using host from docker configuration: %s", host) + + err = os.Setenv("DOCKER_HOST", host) + if err != nil { + return nil, err + } + } else { + log.Debugf("cant parse docker config, ignoring: %v", err) + } + + } + + clientOpts := []client.Opt{ client.FromEnv, client.WithAPIVersionNegotiation(), } diff --git a/internal/docker/config.go b/internal/docker/config.go new file mode 100644 index 00000000..ed527bba --- /dev/null +++ b/internal/docker/config.go @@ -0,0 +1,84 @@ +package docker + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + + "github.com/anchore/stereoscope/internal/log" + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/context/store" + "github.com/docker/docker/pkg/homedir" +) + +const ( + defaultContextName = "default" + dockerEndpoint = "docker" + + envOverrideContext = "DOCKER_CONTEXT" +) + +var ( + configFileDir = filepath.Join(homedir.Get(), ".docker") + contextsDir = filepath.Join(configFileDir, "contexts") + configFileName = filepath.Join(configFileDir, "config.json") +) + +func processConfig(overrideContext, dir string, cfg *configfile.ConfigFile) (string, error) { + dockerContext := resolveContextName(overrideContext, cfg) + + log.Debugf("current docker context: %s", dockerContext) + + return endpointFromContext(dir, dockerContext) +} + +func resolveContextName(contextOverride string, config *configfile.ConfigFile) string { + if contextOverride != "" { + return contextOverride + } + + if config != nil && config.CurrentContext != "" { + return config.CurrentContext + } + + return defaultContextName +} + +func loadConfig(filename string) (*configfile.ConfigFile, error) { + cfg := configfile.New(filename) + + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + err = cfg.LoadFromReader(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("cant parse docker config: %w", err) + } + + return cfg, err +} + +func endpointFromContext(dir, ctxName string) (string, error) { + st := store.New(dir, store.Config{}) + + meta, err := st.GetMetadata(ctxName) + if err != nil { + return "", fmt.Errorf("cant get docker config metadata: %w", err) + } + + // retrieving endpoint + ep, ok := meta.Endpoints[dockerEndpoint].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("cant get docker endpoint from metadata: %w", err) + } + + host, ok := ep["Host"].(string) + if !ok { + return "", fmt.Errorf("cant get docker host from metadata: %w", err) + } + + return host, nil +} diff --git a/internal/docker/config_test.go b/internal/docker/config_test.go new file mode 100644 index 00000000..6e2ee51b --- /dev/null +++ b/internal/docker/config_test.go @@ -0,0 +1,208 @@ +package docker + +import ( + "os" + "path/filepath" + "testing" + + "github.com/docker/cli/cli/config/configfile" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" +) + +func Test_loadConfig(t *testing.T) { + root := "test-fixtures" + + prepareConfig := func(t *testing.T, context, fname string) *configfile.ConfigFile { + t.Helper() + cfg := configfile.New(fname) + cfg.CurrentContext = context + + return cfg + } + + tests := []struct { + name string + filename string + want *configfile.ConfigFile + err error + wantErr bool + }{ + { + name: "config file not found", + filename: "some-nonexisting-file", + wantErr: true, + }, + { + name: "config file parsed normally", + filename: filepath.Join(root, "config0.json"), + wantErr: false, + want: prepareConfig(t, "colima", filepath.Join(root, "config0.json")), + }, + { + name: "config file cannot be parsed", + filename: filepath.Join(root, "config1.json"), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := loadConfig(tt.filename) + if tt.wantErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_resolveContextName(t *testing.T) { + type args struct { + contextOverride string + config *configfile.ConfigFile + } + tests := []struct { + name string + args args + want string + }{ + { + name: "respect contextOverride", + args: args{ + contextOverride: "contextFromEnvironment", + }, + want: "contextFromEnvironment", + }, + { + name: "returns default context name if config is nil", + args: args{}, + want: "default", + }, + { + name: "returns context from the config", + args: args{ + config: &configfile.ConfigFile{ + CurrentContext: "colima", + }, + }, + want: "colima", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveContextName(tt.args.contextOverride, tt.args.config) + + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_endpointFromContext(t *testing.T) { + root := "test-fixtures" + + tmpCtxDir := func(t *testing.T) string { + t.Helper() + d, err := os.MkdirTemp("", "tests") + if err != nil { + t.Fatalf("cant setup test: %v", err) + } + + ctxDir := filepath.Join(d, "contexts") + err = os.MkdirAll(ctxDir, 0o755) + if err != nil { + t.Fatalf("cant setup test: %v", err) + } + + return ctxDir + } + + readFixture := func(t *testing.T, name string) []byte { + t.Helper() + data, err := os.ReadFile(filepath.Join(root, name)) + if err != nil { + t.Fatalf("cant setup test: %v", err) + } + + return data + } + + // meta files stored under ~/.docker with names like + // ~/.docker/contexts/meta/f24fd3749c1368328e2b149bec149cb6795619f244c5b584e844961215dadd16/meta.json + writeTestMeta := func(t *testing.T, fixture, dir, contextName string) { + t.Helper() + data := readFixture(t, fixture) + dd := digest.FromString(contextName) + + base := filepath.Join(dir, "meta", dd.Encoded()) + + err := os.MkdirAll(base, 0o755) + if err != nil { + t.Fatalf("cant setup test: %v", err) + } + + outFname := filepath.Join(base, "meta.json") + + err = os.WriteFile(outFname, data, 0o600) + if err != nil { + t.Fatalf("cant setup test: %v", err) + } + + t.Logf("fixture %s written to: %s", fixture, outFname) + } + + tests := []struct { + name string + ctxName string + want string + fixture string + wantErr bool + }{ + { + name: "reads docker host from the meta data", + want: "unix:///some_weird_location/.colima/docker.sock", + ctxName: "colima", + fixture: "meta0.json", + }, + { + name: "cant read docker host from the meta data", + want: "", + ctxName: "colima", + fixture: "", + wantErr: true, + }, + { + name: "invalid endpoint name", + want: "", + ctxName: "colima", + fixture: "meta1.json", + wantErr: true, + }, + { + name: "no host defined", + want: "", + ctxName: "colima", + fixture: "meta2.json", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := tmpCtxDir(t) + if tt.fixture != "" { + writeTestMeta(t, tt.fixture, dir, tt.ctxName) + } + + got, err := endpointFromContext(dir, tt.ctxName) + if tt.wantErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/docker/test-fixtures/config0.json b/internal/docker/test-fixtures/config0.json new file mode 100644 index 00000000..0848b082 --- /dev/null +++ b/internal/docker/test-fixtures/config0.json @@ -0,0 +1,4 @@ +{ + "auths": {}, + "currentContext": "colima" +} \ No newline at end of file diff --git a/internal/docker/test-fixtures/config1.json b/internal/docker/test-fixtures/config1.json new file mode 100644 index 00000000..16ffaa44 --- /dev/null +++ b/internal/docker/test-fixtures/config1.json @@ -0,0 +1,5 @@ +BROKEN_JSON_FILE +{ + "auths": {}, + "currentContext": "colima" +} \ No newline at end of file diff --git a/internal/docker/test-fixtures/meta0.json b/internal/docker/test-fixtures/meta0.json new file mode 100644 index 00000000..743391e6 --- /dev/null +++ b/internal/docker/test-fixtures/meta0.json @@ -0,0 +1 @@ +{"Name":"colima","Metadata":{"Description":"colima"},"Endpoints":{"docker":{"Host":"unix:///some_weird_location/.colima/docker.sock","SkipTLSVerify":false}}} \ No newline at end of file diff --git a/internal/docker/test-fixtures/meta1.json b/internal/docker/test-fixtures/meta1.json new file mode 100644 index 00000000..5ee084b1 --- /dev/null +++ b/internal/docker/test-fixtures/meta1.json @@ -0,0 +1 @@ +{"Name":"colima","Metadata":{"Description":"colima"},"Endpoints":{"INVALID-NONDOCKER-ENDPOINT":{"Host":"unix:///some_weird_location/.colima/docker.sock","SkipTLSVerify":false}}} \ No newline at end of file diff --git a/internal/docker/test-fixtures/meta2.json b/internal/docker/test-fixtures/meta2.json new file mode 100644 index 00000000..7947cc5a --- /dev/null +++ b/internal/docker/test-fixtures/meta2.json @@ -0,0 +1 @@ +{"Name":"colima","Metadata":{"Description":"colima"},"Endpoints":{"docker":{"NO-HOSTNAME-DEFINED":"unix:///some_weird_location/.colima/docker.sock","SkipTLSVerify":false}}} \ No newline at end of file From b08807ac21585963445b9295fe289cf91a670747 Mon Sep 17 00:00:00 2001 From: Valery Masiutsin Date: Thu, 24 Aug 2023 01:05:35 +0100 Subject: [PATCH 2/2] Tidying go mod Signed-off-by: Valery Masiutsin --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a2fe857b..f84995ac 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect