Skip to content

Commit b7cc89c

Browse files
authored
feat: add private registry support (#6)
1 parent 3df3f77 commit b7cc89c

File tree

4 files changed

+102
-76
lines changed

4 files changed

+102
-76
lines changed

cli/docker.go

Lines changed: 33 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -100,24 +100,7 @@ var envboxPrivateMounts = map[string]struct{}{
100100
}
101101

102102
func dockerCmd() *cobra.Command {
103-
var (
104-
// Required flags.
105-
innerImage string
106-
innerUsername string
107-
innerContainerName string
108-
agentToken string
109-
110-
// Optional flags.
111-
innerEnvs string
112-
innerWorkDir string
113-
innerHostname string
114-
addTUN bool
115-
addFUSE bool
116-
dockerdBridgeCIDR string
117-
boostrapScript string
118-
ethlink string
119-
containerMounts string
120-
)
103+
var flag flags
121104

122105
cmd := &cobra.Command{
123106
Use: "docker",
@@ -144,12 +127,12 @@ func dockerCmd() *cobra.Command {
144127
}()
145128

146129
cidr := dockerutil.DefaultBridgeCIDR
147-
if dockerdBridgeCIDR != "" {
148-
cidr = dockerdBridgeCIDR
130+
if flag.dockerdBridgeCIDR != "" {
131+
cidr = flag.dockerdBridgeCIDR
149132
log.Debug(ctx, "using custom docker bridge CIDR", slog.F("cidr", cidr))
150133
}
151134

152-
dargs, err := dockerdArgs(ctx, log, ethlink, cidr, false)
135+
dargs, err := dockerdArgs(ctx, log, flag.ethlink, cidr, false)
153136
if err != nil {
154137
return xerrors.Errorf("dockerd args: %w", err)
155138
}
@@ -182,7 +165,7 @@ func dockerCmd() *cobra.Command {
182165
// directory is going to be on top of an overlayfs filesystem
183166
// we have to use the vfs storage driver.
184167
if xunix.IsNoSpaceErr(err) {
185-
args, err = dockerdArgs(ctx, log, ethlink, cidr, true)
168+
args, err = dockerdArgs(ctx, log, flag.ethlink, cidr, true)
186169
if err != nil {
187170
_ = envboxlog.YieldAndFailBuild("Failed to create Container-based Virtual Machine: " + err.Error())
188171
//nolint
@@ -219,26 +202,7 @@ func dockerCmd() *cobra.Command {
219202
return xerrors.Errorf("wait for dockerd: %w", err)
220203
}
221204

222-
flags := flags{
223-
// Required flags.
224-
innerImage: innerImage,
225-
innerUsername: innerUsername,
226-
innerContainerName: innerContainerName,
227-
agentToken: agentToken,
228-
229-
// Optional flags.
230-
innerEnvs: innerEnvs,
231-
innerWorkDir: innerWorkDir,
232-
innerHostname: innerHostname,
233-
addTUN: addTUN,
234-
addFUSE: addFUSE,
235-
dockerdBridgeCIDR: dockerdBridgeCIDR,
236-
boostrapScript: boostrapScript,
237-
ethlink: ethlink,
238-
containerMounts: containerMounts,
239-
}
240-
241-
err = runDockerCVM(ctx, log, client, flags)
205+
err = runDockerCVM(ctx, log, client, flag)
242206
if err != nil {
243207
// It's possible we failed because we ran out of disk while
244208
// pulling the image. We should restart the daemon and use
@@ -247,7 +211,7 @@ func dockerCmd() *cobra.Command {
247211
// is causing their disk to fill up.
248212
if xunix.IsNoSpaceErr(err) {
249213
log.Debug(ctx, "encountered 'no space left on device' error while starting workspace", slog.Error(err))
250-
args, err := dockerdArgs(ctx, log, ethlink, cidr, true)
214+
args, err := dockerdArgs(ctx, log, flag.ethlink, cidr, true)
251215
if err != nil {
252216
return xerrors.Errorf("dockerd args for restart: %w", err)
253217
}
@@ -265,7 +229,7 @@ func dockerCmd() *cobra.Command {
265229
}()
266230

267231
log.Debug(ctx, "reattempting container creation")
268-
err = runDockerCVM(ctx, log, client, flags)
232+
err = runDockerCVM(ctx, log, client, flag)
269233
}
270234
if err != nil {
271235
return xerrors.Errorf("run: %w", err)
@@ -280,21 +244,22 @@ func dockerCmd() *cobra.Command {
280244
}
281245

282246
// Required flags.
283-
cliflag.StringVarP(cmd.Flags(), &innerImage, "image", "", EnvInnerImage, "", "The image for the inner container. Required.")
284-
cliflag.StringVarP(cmd.Flags(), &innerUsername, "username", "", EnvInnerUsername, "", "The username to use for the inner container. Required.")
285-
cliflag.StringVarP(cmd.Flags(), &innerContainerName, "container-name", "", EnvInnerContainerName, "", "The name of the inner container. Required.")
286-
cliflag.StringVarP(cmd.Flags(), &agentToken, "agent-token", "", EnvAgentToken, "", "The token to be used by the workspace agent to estabish a connection with the control plane. Required.")
247+
cliflag.StringVarP(cmd.Flags(), &flag.innerImage, "image", "", EnvInnerImage, "", "The image for the inner container. Required.")
248+
cliflag.StringVarP(cmd.Flags(), &flag.innerUsername, "username", "", EnvInnerUsername, "", "The username to use for the inner container. Required.")
249+
cliflag.StringVarP(cmd.Flags(), &flag.innerContainerName, "container-name", "", EnvInnerContainerName, "", "The name of the inner container. Required.")
250+
cliflag.StringVarP(cmd.Flags(), &flag.agentToken, "agent-token", "", EnvAgentToken, "", "The token to be used by the workspace agent to estabish a connection with the control plane. Required.")
287251

288252
// Optional flags.
289-
cliflag.StringVarP(cmd.Flags(), &innerEnvs, "envs", "", EnvInnerEnvs, "", "Comma separated list of envs to add to the inner container.")
290-
cliflag.StringVarP(cmd.Flags(), &innerWorkDir, "work-dir", "", EnvInnerWorkDir, "", "The working directory of the inner container.")
291-
cliflag.StringVarP(cmd.Flags(), &innerHostname, "hostname", "", EnvInnerHostname, "", "The hostname to use for the inner container.")
292-
cliflag.StringVarP(cmd.Flags(), &dockerdBridgeCIDR, "bridge-cidr", "", EnvBridgeCIDR, "", "The CIDR to use for the docker bridge.")
293-
cliflag.StringVarP(cmd.Flags(), &boostrapScript, "boostrap-script", "", EnvBootstrap, "", "The script to use to bootstrap the container. This should typically install and start the agent.")
294-
cliflag.StringVarP(cmd.Flags(), &containerMounts, "mounts", "", EnvMounts, "", "Comma separated list of mounts in the form of '<source>:<target>[:options]' (e.g. /var/lib/docker:/var/lib/docker:ro,/usr/src:/usr/src).")
295-
cliflag.StringVarP(cmd.Flags(), &ethlink, "ethlink", "", "", defaultNetLink, "The ethernet link to query for the MTU that is passed to docerd. Used for tests.")
296-
cliflag.BoolVarP(cmd.Flags(), &addTUN, "add-tun", "", EnvAddTun, false, "Add a TUN device to the inner container.")
297-
cliflag.BoolVarP(cmd.Flags(), &addFUSE, "add-fuse", "", EnvAddFuse, false, "Add a FUSE device to the inner container.")
253+
cliflag.StringVarP(cmd.Flags(), &flag.innerEnvs, "envs", "", EnvInnerEnvs, "", "Comma separated list of envs to add to the inner container.")
254+
cliflag.StringVarP(cmd.Flags(), &flag.innerWorkDir, "work-dir", "", EnvInnerWorkDir, "", "The working directory of the inner container.")
255+
cliflag.StringVarP(cmd.Flags(), &flag.innerHostname, "hostname", "", EnvInnerHostname, "", "The hostname to use for the inner container.")
256+
cliflag.StringVarP(cmd.Flags(), &flag.imagePullSecret, "image-secret", "", EnvBoxPullImageSecretEnvVar, "", fmt.Sprintf("The secret to use to pull the image. It is highly encouraged to provide this via the %s environment variable.", EnvBoxPullImageSecretEnvVar))
257+
cliflag.StringVarP(cmd.Flags(), &flag.dockerdBridgeCIDR, "bridge-cidr", "", EnvBridgeCIDR, "", "The CIDR to use for the docker bridge.")
258+
cliflag.StringVarP(cmd.Flags(), &flag.boostrapScript, "boostrap-script", "", EnvBootstrap, "", "The script to use to bootstrap the container. This should typically install and start the agent.")
259+
cliflag.StringVarP(cmd.Flags(), &flag.containerMounts, "mounts", "", EnvMounts, "", "Comma separated list of mounts in the form of '<source>:<target>[:options]' (e.g. /var/lib/docker:/var/lib/docker:ro,/usr/src:/usr/src).")
260+
cliflag.StringVarP(cmd.Flags(), &flag.ethlink, "ethlink", "", "", defaultNetLink, "The ethernet link to query for the MTU that is passed to docerd. Used for tests.")
261+
cliflag.BoolVarP(cmd.Flags(), &flag.addTUN, "add-tun", "", EnvAddTun, false, "Add a TUN device to the inner container.")
262+
cliflag.BoolVarP(cmd.Flags(), &flag.addFUSE, "add-fuse", "", EnvAddFuse, false, "Add a FUSE device to the inner container.")
298263

299264
return cmd
300265
}
@@ -309,6 +274,7 @@ type flags struct {
309274
innerEnvs string
310275
innerWorkDir string
311276
innerHostname string
277+
imagePullSecret string
312278
addTUN bool
313279
addFUSE bool
314280
dockerdBridgeCIDR string
@@ -327,6 +293,14 @@ func runDockerCVM(ctx context.Context, log slog.Logger, client dockerutil.Docker
327293
return xerrors.Errorf("set oom score: %w", err)
328294
}
329295

296+
var dockerAuth dockerutil.AuthConfig
297+
if flags.imagePullSecret != "" {
298+
dockerAuth, err = dockerutil.ParseAuthConfig(flags.imagePullSecret)
299+
if err != nil {
300+
return xerrors.Errorf("parse auth config: %w", err)
301+
}
302+
}
303+
330304
envs := defaultContainerEnvs(flags.agentToken)
331305

332306
innerEnvsTokens := strings.Split(flags.innerEnvs, ",")
@@ -392,8 +366,8 @@ func runDockerCVM(ctx context.Context, log slog.Logger, client dockerutil.Docker
392366
err = dockerutil.PullImage(ctx, &dockerutil.PullImageConfig{
393367
Client: client,
394368
Image: flags.innerImage,
395-
Auth: dockerutil.DockerAuth{}, // TODO
396-
ProgressFn: func(e dockerutil.ImagePullEvent) error { return nil }, // TODO
369+
Auth: dockerAuth,
370+
ProgressFn: func(e dockerutil.ImagePullEvent) error { return nil },
397371
})
398372
if err != nil {
399373
return xerrors.Errorf("pull image: %w", err)

cli/docker_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
//go:build !integration
2+
// +build !integration
3+
14
package cli_test
25

36
import (
47
"bufio"
8+
"bytes"
59
"context"
10+
"encoding/base64"
611
"fmt"
12+
"io"
713
"net"
814
"os"
915
"strings"
@@ -377,4 +383,34 @@ func TestDocker(t *testing.T) {
377383
require.NoError(t, err)
378384
require.True(t, called, "container create fn not called")
379385
})
386+
387+
t.Run("DockerAuth", func(t *testing.T) {
388+
t.Parallel()
389+
390+
name := namesgenerator.GetRandomName(1)
391+
ctx, cmd := clitest.New(t, "docker",
392+
"--image=ubuntu",
393+
"--username=root",
394+
fmt.Sprintf("--container-name=%s", name),
395+
"--agent-token=hi",
396+
fmt.Sprintf("--image-secret=%s", rawDockerAuth),
397+
)
398+
399+
raw := []byte(`{"username":"_json_key","password":"{\"type\": \"service_account\", \"project_id\": \"some-test\", \"private_key_id\": \"blahblah\", \"private_key\": \"-----BEGIN PRIVATE KEY-----mykey-----END PRIVATE KEY-----\", \"client_email\": \"test@test.iam.gserviceaccount.com\", \"client_id\": \"123\", \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\", \"token_uri\": \"https://oauth2.googleapis.com/token\", \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\", \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/test.iam.gserviceaccount.com\" }","auth":"X2pzb25fa2V5OnsKCgkidHlwZSI6ICJzZXJ2aWNlX2FjY291bnQiLAoJInByb2plY3RfaWQiOiAic29tZS10ZXN0IiwKCSJwcml2YXRlX2tleV9pZCI6ICJibGFoYmxhaCIsCgkicHJpdmF0ZV9rZXkiOiAiLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCm15a2V5LS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQoiLAoJImNsaWVudF9lbWFpbCI6ICJ0ZXN0QHRlc3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAoJImNsaWVudF9pZCI6ICIxMjMiLAoJImF1dGhfdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoIiwKCSJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4iLAoJImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMiLAoJImNsaWVudF94NTA5X2NlcnRfdXJsIjogImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL3JvYm90L3YxL21ldGFkYXRhL3g1MDkvdGVzdC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIKfQo=","email":"test@test.iam.gserviceaccount.com"}`)
400+
authB64 := base64.StdEncoding.EncodeToString(raw)
401+
402+
client := clitest.DockerClient(t, ctx)
403+
client.ImagePullFn = func(_ context.Context, ref string, options dockertypes.ImagePullOptions) (io.ReadCloser, error) {
404+
// Assert that we call the image pull function with the credentials.
405+
require.Equal(t, authB64, options.RegistryAuth)
406+
return io.NopCloser(bytes.NewReader(nil)), nil
407+
}
408+
409+
err := cmd.ExecuteContext(ctx)
410+
require.NoError(t, err)
411+
})
380412
}
413+
414+
// rawDockerAuth is sample input for a kubernetes secret to a gcr.io private
415+
// registry.
416+
const rawDockerAuth = `{"auths":{"us.gcr.io":{"username":"_json_key","password":"{\"type\": \"service_account\", \"project_id\": \"some-test\", \"private_key_id\": \"blahblah\", \"private_key\": \"-----BEGIN PRIVATE KEY-----mykey-----END PRIVATE KEY-----\", \"client_email\": \"test@test.iam.gserviceaccount.com\", \"client_id\": \"123\", \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\", \"token_uri\": \"https://oauth2.googleapis.com/token\", \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\", \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/test.iam.gserviceaccount.com\" }","email":"test@test.iam.gserviceaccount.com","auth":"X2pzb25fa2V5OnsKCgkidHlwZSI6ICJzZXJ2aWNlX2FjY291bnQiLAoJInByb2plY3RfaWQiOiAic29tZS10ZXN0IiwKCSJwcml2YXRlX2tleV9pZCI6ICJibGFoYmxhaCIsCgkicHJpdmF0ZV9rZXkiOiAiLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCm15a2V5LS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQoiLAoJImNsaWVudF9lbWFpbCI6ICJ0ZXN0QHRlc3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAoJImNsaWVudF9pZCI6ICIxMjMiLAoJImF1dGhfdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoIiwKCSJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4iLAoJImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMiLAoJImNsaWVudF94NTA5X2NlcnRfdXJsIjogImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL3JvYm90L3YxL21ldGFkYXRhL3g1MDkvdGVzdC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIKfQo="}}}`

dockerutil/client.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package dockerutil
22

33
import (
44
"context"
5+
"encoding/base64"
6+
"encoding/json"
57

8+
dockertypes "github.com/docker/docker/api/types"
69
dockerclient "github.com/docker/docker/client"
710
"golang.org/x/xerrors"
811
)
@@ -31,3 +34,32 @@ func Client(ctx context.Context) (DockerClient, error) {
3134
//nolint we should panic if this isn't the case.
3235
return client.(DockerClient), nil
3336
}
37+
38+
type AuthConfig dockertypes.AuthConfig
39+
40+
func (a AuthConfig) Base64() (string, error) {
41+
authStr, err := json.Marshal(a)
42+
if err != nil {
43+
return "", xerrors.Errorf("json marshal: %w", err)
44+
}
45+
return base64.StdEncoding.EncodeToString(authStr), nil
46+
}
47+
48+
func ParseAuthConfig(raw string) (AuthConfig, error) {
49+
type dockerConfig struct {
50+
AuthConfigs map[string]dockertypes.AuthConfig `json:"auths"`
51+
}
52+
53+
var conf dockerConfig
54+
if err := json.Unmarshal([]byte(raw), &conf); err != nil {
55+
return AuthConfig{}, xerrors.Errorf("parse docker auth config secret: %w", err)
56+
}
57+
if len(conf.AuthConfigs) != 1 {
58+
return AuthConfig{}, xerrors.Errorf("number of image pull auth configs not equal to 1 (%d)", len(conf.AuthConfigs))
59+
}
60+
for _, regConfig := range conf.AuthConfigs {
61+
return AuthConfig(regConfig), nil
62+
}
63+
64+
return AuthConfig{}, xerrors.New("no auth configs parsed.")
65+
}

dockerutil/image.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package dockerutil
33
import (
44
"bytes"
55
"context"
6-
"encoding/base64"
76
"encoding/json"
87
"io"
98
"time"
@@ -22,7 +21,7 @@ const diskFullStorageDriver = "vfs"
2221
type PullImageConfig struct {
2322
Client DockerClient
2423
Image string
25-
Auth DockerAuth
24+
Auth AuthConfig
2625
ProgressFn ImagePullProgressFn
2726
}
2827

@@ -40,21 +39,6 @@ type ImagePullEvent struct {
4039
// image pull progress.
4140
type ImagePullProgressFn func(e ImagePullEvent) error
4241

43-
type DockerAuth dockertypes.AuthConfig
44-
45-
func (d DockerAuth) Base64() (string, error) {
46-
if d == (DockerAuth{}) {
47-
return "", nil
48-
}
49-
50-
authStr, err := json.Marshal(d)
51-
if err != nil {
52-
return "", xerrors.Errorf("marshal auth: %w", err)
53-
}
54-
55-
return base64.StdEncoding.EncodeToString(authStr), nil
56-
}
57-
5842
// PullImage pulls the provided image.
5943
func PullImage(ctx context.Context, config *PullImageConfig) error {
6044
authStr, err := config.Auth.Base64()

0 commit comments

Comments
 (0)