Skip to content

Commit d8207cc

Browse files
authored
feat(context): create and delete docker contexts (#80)
* chore: check the current docker socket path has a valid schema * feat: check for rootless docker using the XDG_RUNTIME_DIR env var * feat(config): save a config file to disk * feat: add and delete docker contexts * chore: set default context if the deleted one is the current * docs: document new APIs * fix: proper rootless env var check * chore: proper variable name * chore: rename error message * fix: proper order in go.work * chore: proper error message * chore: store config filepath in the struct * docs: document saving the config * chore: simplify
1 parent 3ce6430 commit d8207cc

27 files changed

+829
-37
lines changed

config/README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ go get github.com/docker/go-sdk/config
1616

1717
#### Directory
1818

19-
It will return the current Docker config directory.
19+
It returns the current Docker config directory.
2020

2121
```go
2222
dir, err := config.Dir()
@@ -29,7 +29,7 @@ fmt.Printf("current docker config directory: %s", dir)
2929

3030
#### Filepath
3131

32-
It will return the path to the Docker config file.
32+
It returns the path to the Docker config file.
3333

3434
```go
3535
filepath, err := config.Filepath()
@@ -42,7 +42,7 @@ fmt.Printf("current docker config file path: %s", filepath)
4242

4343
#### Load
4444

45-
It will return the Docker config.
45+
It returns the Docker config.
4646

4747
```go
4848
cfg, err := config.Load()
@@ -53,11 +53,21 @@ if err != nil {
5353
fmt.Printf("docker config: %+v", cfg)
5454
```
5555

56+
#### Save
57+
58+
Once you have loaded a config, you can save it back to the file system.
59+
60+
```go
61+
if err := cfg.Save(); err != nil {
62+
log.Fatalf("failed to save docker config: %v", err)
63+
}
64+
```
65+
5666
### Auth
5767

5868
#### AuthConfigs
5969

60-
It will return a maps of the registry credentials for the given Docker images, indexed by the registry hostname.
70+
It returns a maps of the registry credentials for the given Docker images, indexed by the registry hostname.
6171

6272
```go
6373
authConfigs, err := config.AuthConfigs("nginx:latest")
@@ -70,7 +80,7 @@ fmt.Printf("registry credentials: %+v", authConfigs)
7080

7181
#### Auth Configs For Hostname
7282

73-
It will return the registry credentials for the given Docker registry.
83+
It returns the registry credentials for the given Docker registry.
7484

7585
```go
7686
authConfig, err := config.AuthConfigForHostname("https://index.docker.io/v1/")

config/auth_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,11 @@ func validateAuthForHostname(t *testing.T, hostname, expectedUser, expectedPass
8787
t.Helper()
8888

8989
creds, err := AuthConfigForHostname(hostname)
90-
require.ErrorIs(t, err, expectedErr)
90+
if expectedErr != nil {
91+
require.ErrorContains(t, err, expectedErr.Error())
92+
} else {
93+
require.NoError(t, err)
94+
}
9195
require.Equal(t, expectedUser, creds.Username)
9296
require.Equal(t, expectedPass, creds.Password)
9397
if creds.ServerAddress != "" {
@@ -100,7 +104,11 @@ func validateAuthForImage(t *testing.T, imageRef, expectedUser, expectedPass, ex
100104
t.Helper()
101105

102106
authConfigs, err := AuthConfigs(imageRef)
103-
require.ErrorIs(t, err, expectedErr)
107+
if expectedErr != nil {
108+
require.ErrorContains(t, err, expectedErr.Error())
109+
} else {
110+
require.NoError(t, err)
111+
}
104112

105113
creds, ok := authConfigs[expectedRegistry]
106114
require.Equal(t, (expectedErr == nil), ok)
@@ -194,7 +202,7 @@ func TestRegistryCredentialsForImage(t *testing.T) {
194202

195203
t.Run("config/not-found", func(t *testing.T) {
196204
t.Setenv(EnvOverrideDir, filepath.Join("testdata", "missing"))
197-
validateAuthForImage(t, "userpass.io/repo/image:tag", "", "", "", os.ErrNotExist)
205+
validateAuthForImage(t, "userpass.io/repo/image:tag", "", "", "", errors.New("file does not exist"))
198206
})
199207
}
200208

@@ -258,7 +266,7 @@ func TestRegistryCredentialsForHostname(t *testing.T) {
258266

259267
t.Run("config/not-found", func(t *testing.T) {
260268
t.Setenv(EnvOverrideDir, filepath.Join("testdata", "missing"))
261-
validateAuthForHostname(t, "userpass.io", "", "", os.ErrNotExist)
269+
validateAuthForHostname(t, "userpass.io", "", "", errors.New("file does not exist"))
262270
})
263271
}
264272

config/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"errors"
99
"fmt"
10+
"os"
1011
"strings"
1112
"sync"
1213
"time"
@@ -194,6 +195,16 @@ func (c *Config) ParseProxyConfig(host string, runOpts map[string]*string) map[s
194195
return m
195196
}
196197

198+
// Save saves the config to the file system
199+
func (c *Config) Save() error {
200+
data, err := json.Marshal(c)
201+
if err != nil {
202+
return fmt.Errorf("json marshal: %w", err)
203+
}
204+
205+
return os.WriteFile(c.filepath, data, 0o644)
206+
}
207+
197208
// resolveAuthConfigForHostname performs the actual auth config resolution
198209
func (c *Config) resolveAuthConfigForHostname(hostname string) (AuthConfig, error) {
199210
// Check credential helpers first

config/config_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package config
22

33
import (
4+
"os"
5+
"path/filepath"
46
"sync"
57
"testing"
68

@@ -101,3 +103,29 @@ func TestConfig_CacheKeyGeneration(t *testing.T) {
101103

102104
require.NotEqual(t, stats1.CacheKey, stats2.CacheKey)
103105
}
106+
107+
func TestConfigSave(t *testing.T) {
108+
tmpDir := t.TempDir()
109+
setupHome(t, tmpDir)
110+
111+
dockerDir := filepath.Join(tmpDir, ".docker")
112+
113+
err := os.MkdirAll(dockerDir, 0o755)
114+
require.NoError(t, err)
115+
116+
_, err = os.Create(filepath.Join(dockerDir, FileName))
117+
require.NoError(t, err)
118+
119+
c := Config{
120+
filepath: filepath.Join(dockerDir, FileName),
121+
CurrentContext: "test",
122+
AuthConfigs: map[string]AuthConfig{},
123+
}
124+
125+
require.NoError(t, c.Save())
126+
127+
cfg, err := Load()
128+
require.NoError(t, err)
129+
require.Equal(t, c.CurrentContext, cfg.CurrentContext)
130+
require.Equal(t, c.AuthConfigs, cfg.AuthConfigs)
131+
}

config/load.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ func getHomeDir() (string, error) {
4646
func Dir() (string, error) {
4747
dir := os.Getenv(EnvOverrideDir)
4848
if dir != "" {
49-
if err := fileExists(dir); err != nil {
50-
return "", fmt.Errorf("file exists: %w", err)
49+
if !fileExists(dir) {
50+
return "", fmt.Errorf("file does not exist (%s)", dir)
5151
}
5252
return dir, nil
5353
}
@@ -58,19 +58,19 @@ func Dir() (string, error) {
5858
}
5959

6060
configDir := filepath.Join(home, configFileDir)
61-
if err := fileExists(configDir); err != nil {
62-
return "", fmt.Errorf("file exists: %w", err)
61+
if !fileExists(configDir) {
62+
return "", fmt.Errorf("file does not exist (%s)", configDir)
6363
}
6464

6565
return configDir, nil
6666
}
6767

68-
func fileExists(path string) error {
68+
func fileExists(path string) bool {
6969
if _, err := os.Stat(path); os.IsNotExist(err) {
70-
return fmt.Errorf("file does not exist: %w", err)
70+
return false
7171
}
7272

73-
return nil
73+
return true
7474
}
7575

7676
// Filepath returns the path to the docker cli config file,
@@ -82,8 +82,8 @@ func Filepath() (string, error) {
8282
}
8383

8484
configFilePath := filepath.Join(dir, FileName)
85-
if err := fileExists(configFilePath); err != nil {
86-
return "", fmt.Errorf("config file: %w", err)
85+
if !fileExists(configFilePath) {
86+
return "", fmt.Errorf("config file does not exist (%s)", configFilePath)
8787
}
8888

8989
return configFilePath, nil
@@ -113,6 +113,9 @@ func Load() (Config, error) {
113113
return cfg, fmt.Errorf("load config: %w", err)
114114
}
115115

116+
// store the location of the config file into the config, for future use
117+
cfg.filepath = p
118+
116119
return cfg, nil
117120
}
118121

config/load_test.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ import (
1414
var dockerConfig string
1515

1616
func TestLoad(t *testing.T) {
17-
var expectedConfig Config
18-
err := json.Unmarshal([]byte(dockerConfig), &expectedConfig)
19-
require.NoError(t, err)
20-
2117
t.Run("HOME", func(t *testing.T) {
2218
t.Run("valid", func(t *testing.T) {
2319
setupHome(t, "testdata")
2420

21+
var expectedConfig Config
22+
err := json.Unmarshal([]byte(dockerConfig), &expectedConfig)
23+
require.NoError(t, err)
24+
25+
expectedConfig.filepath = filepath.Join("testdata", ".docker", FileName)
26+
2527
cfg, err := Load()
2628
require.NoError(t, err)
2729
require.Equal(t, expectedConfig, cfg)
@@ -31,7 +33,7 @@ func TestLoad(t *testing.T) {
3133
setupHome(t, "testdata", "not-found")
3234

3335
cfg, err := Load()
34-
require.ErrorIs(t, err, os.ErrNotExist)
36+
require.ErrorContains(t, err, "file does not exist")
3537
require.Empty(t, cfg)
3638
})
3739

@@ -49,6 +51,10 @@ func TestLoad(t *testing.T) {
4951
setupHome(t, "testdata", "not-found")
5052
t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig)
5153

54+
var expectedConfig Config
55+
err := json.Unmarshal([]byte(dockerConfig), &expectedConfig)
56+
require.NoError(t, err)
57+
5258
cfg, err := Load()
5359
require.NoError(t, err)
5460
require.Equal(t, expectedConfig, cfg)
@@ -69,6 +75,12 @@ func TestLoad(t *testing.T) {
6975
setupHome(t, "testdata", "not-found")
7076
t.Setenv(EnvOverrideDir, filepath.Join("testdata", ".docker"))
7177

78+
var expectedConfig Config
79+
err := json.Unmarshal([]byte(dockerConfig), &expectedConfig)
80+
require.NoError(t, err)
81+
82+
expectedConfig.filepath = filepath.Join("testdata", ".docker", FileName)
83+
7284
cfg, err := Load()
7385
require.NoError(t, err)
7486
require.Equal(t, expectedConfig, cfg)
@@ -105,7 +117,7 @@ func TestDir(t *testing.T) {
105117
setupHome(t, "testdata", "not-found")
106118

107119
dir, err := Dir()
108-
require.ErrorIs(t, err, os.ErrNotExist)
120+
require.ErrorContains(t, err, "file does not exist")
109121
require.Empty(t, dir)
110122
})
111123
})
@@ -124,7 +136,7 @@ func TestDir(t *testing.T) {
124136
setupDockerConfigs(t, "testdata", "not-found")
125137

126138
dir, err := Dir()
127-
require.ErrorIs(t, err, os.ErrNotExist)
139+
require.ErrorContains(t, err, "file does not exist")
128140
require.Empty(t, dir)
129141
})
130142
})

config/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919

2020
// Config represents the on disk format of the docker CLI's config file.
2121
type Config struct {
22+
filepath string `json:"-"`
2223
AuthConfigs map[string]AuthConfig `json:"auths"`
2324
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
2425
PsFormat string `json:"psFormat,omitempty"`

context/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,47 @@ if err != nil {
7777
}
7878

7979
fmt.Printf("contexts: %v", contexts)
80+
```
81+
82+
### Add Context
83+
84+
It adds a new context to the Docker configuration, identified by a name. It's possible to pass options to customize the context definition.
85+
86+
```go
87+
ctx, err := context.New("my-context")
88+
if err != nil {
89+
log.Printf("failed to add context: %v", err)
90+
return
91+
}
92+
93+
fmt.Printf("context added: %s", ctx.Name)
94+
```
95+
96+
### Available Options
97+
98+
The following options are available to customize the context definition:
99+
100+
- `WithHost(host string) CreateContextOption` sets the host for the context.
101+
- `WithDescription(description string) CreateContextOption` sets the description for the context.
102+
- `WithAdditionalFields(fields map[string]any) CreateContextOption` sets the additional fields for the context.
103+
- `WithSkipTLSVerify() CreateContextOption` sets the skipTLSVerify flag to true.
104+
- `AsCurrent() CreateContextOption` sets the context as the current context, saving the current context to the Docker configuration.
105+
106+
### Delete Context
107+
108+
It deletes a context from the Docker configuration.
109+
110+
```go
111+
ctx, err := context.New("my-context")
112+
if err != nil {
113+
log.Printf("error adding context: %s", err)
114+
return
115+
}
116+
117+
if err := ctx.Delete(); err != nil {
118+
log.Printf("failed to delete context: %v", err)
119+
return
120+
}
121+
122+
fmt.Printf("context deleted: %s", ctx.Name)
80123
```

0 commit comments

Comments
 (0)