Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read docker host/socket location from the current docker context #194

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
27 changes: 26 additions & 1 deletion internal/docker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand Down
84 changes: 84 additions & 0 deletions internal/docker/config.go
Original file line number Diff line number Diff line change
@@ -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
}
208 changes: 208 additions & 0 deletions internal/docker/config_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
4 changes: 4 additions & 0 deletions internal/docker/test-fixtures/config0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"auths": {},
"currentContext": "colima"
}
5 changes: 5 additions & 0 deletions internal/docker/test-fixtures/config1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
BROKEN_JSON_FILE
{
"auths": {},
"currentContext": "colima"
}
1 change: 1 addition & 0 deletions internal/docker/test-fixtures/meta0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Name":"colima","Metadata":{"Description":"colima"},"Endpoints":{"docker":{"Host":"unix:///some_weird_location/.colima/docker.sock","SkipTLSVerify":false}}}
1 change: 1 addition & 0 deletions internal/docker/test-fixtures/meta1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Name":"colima","Metadata":{"Description":"colima"},"Endpoints":{"INVALID-NONDOCKER-ENDPOINT":{"Host":"unix:///some_weird_location/.colima/docker.sock","SkipTLSVerify":false}}}
1 change: 1 addition & 0 deletions internal/docker/test-fixtures/meta2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Name":"colima","Metadata":{"Description":"colima"},"Endpoints":{"docker":{"NO-HOSTNAME-DEFINED":"unix:///some_weird_location/.colima/docker.sock","SkipTLSVerify":false}}}