Skip to content

Commit

Permalink
fix: parse image auth config correctly (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
sreya authored Oct 21, 2024
1 parent 569d5de commit e557d25
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 50 deletions.
10 changes: 5 additions & 5 deletions cli/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,10 +397,14 @@ func runDockerCVM(ctx context.Context, log slog.Logger, client dockerutil.Docker
if err != nil {
return xerrors.Errorf("set oom score: %w", err)
}
ref, err := name.NewTag(flags.innerImage)
if err != nil {
return xerrors.Errorf("parse ref: %w", err)
}

var dockerAuth dockerutil.AuthConfig
if flags.imagePullSecret != "" {
dockerAuth, err = dockerutil.ParseAuthConfig(flags.imagePullSecret)
dockerAuth, err = dockerutil.AuthConfigFromString(flags.imagePullSecret, ref.RegistryStr())
if err != nil {
return xerrors.Errorf("parse auth config: %w", err)
}
Expand All @@ -409,10 +413,6 @@ func runDockerCVM(ctx context.Context, log slog.Logger, client dockerutil.Docker
log.Info(ctx, "checking for docker config file", slog.F("path", flags.dockerConfig))
if _, err := fs.Stat(flags.dockerConfig); err == nil {
log.Info(ctx, "detected file", slog.F("image", flags.innerImage))
ref, err := name.NewTag(flags.innerImage)
if err != nil {
return xerrors.Errorf("parse ref: %w", err)
}
dockerAuth, err = dockerutil.AuthConfigFromPath(flags.dockerConfig, ref.RegistryStr())
if err != nil && !xerrors.Is(err, os.ErrNotExist) {
return xerrors.Errorf("auth config from file: %w", err)
Expand Down
96 changes: 94 additions & 2 deletions cli/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,98 @@ func TestDocker(t *testing.T) {
execer.AssertCommandsCalled(t)
})

t.Run("Images", func(t *testing.T) {
t.Parallel()

type testcase struct {
name string
image string
success bool
}

testcases := []testcase{
{
name: "Repository",
image: "ubuntu",
success: true,
},
{
name: "RepositoryPath",
image: "ubuntu/ubuntu",
success: true,
},

{
name: "RepositoryLatest",
image: "ubuntu:latest",
success: true,
},
{
name: "RepositoryTag",
image: "ubuntu:24.04",
success: true,
},
{
name: "RepositoryPathTag",
image: "ubuntu/ubuntu:18.04",
success: true,
},
{
name: "RegistryRepository",
image: "gcr.io/ubuntu",
success: true,
},
{
name: "RegistryRepositoryTag",
image: "gcr.io/ubuntu:24.04",
success: true,
},
}

for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

ctx, cmd := clitest.New(t, "docker",
"--image="+tc.image,
"--username=root",
"--agent-token=hi",
)

called := make(chan struct{})
execer := clitest.Execer(ctx)
client := clitest.DockerClient(t, ctx)
execer.AddCommands(&xunixfake.FakeCmd{
FakeCmd: &testingexec.FakeCmd{
Argv: []string{
"sysbox-mgr",
},
},
WaitFn: func() error { close(called); select {} }, //nolint:revive
})

var created bool
client.ContainerCreateFn = func(_ context.Context, conf *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *v1.Platform, _ string) (container.CreateResponse, error) {
created = true
require.Equal(t, tc.image, conf.Image)
return container.CreateResponse{}, nil
}

err := cmd.ExecuteContext(ctx)
if !tc.success {
require.Error(t, err)
return
}

<-called
require.NoError(t, err)
require.True(t, created, "container create fn not called")
execer.AssertCommandsCalled(t)
})
}
})

// Test that dockerd is configured correctly.
t.Run("DockerdConfigured", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -384,13 +476,13 @@ func TestDocker(t *testing.T) {
t.Parallel()

ctx, cmd := clitest.New(t, "docker",
"--image=ubuntu",
"--image=us.gcr.io/ubuntu",
"--username=root",
"--agent-token=hi",
fmt.Sprintf("--image-secret=%s", rawDockerAuth),
)

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"}`)
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\" }"}`)
authB64 := base64.URLEncoding.EncodeToString(raw)

client := clitest.DockerClient(t, ctx)
Expand Down
38 changes: 14 additions & 24 deletions dockerutil/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,29 @@ func (a AuthConfig) Base64() (string, error) {
return base64.URLEncoding.EncodeToString(authStr), nil
}

func AuthConfigFromPath(path string, registry string) (AuthConfig, error) {
func AuthConfigFromPath(path string, reg string) (AuthConfig, error) {
var config dockercfg.Config
err := dockercfg.FromFile(path, &config)
if err != nil {
return AuthConfig{}, xerrors.Errorf("load config: %w", err)
}

hostname := dockercfg.ResolveRegistryHost(registry)
return parseConfig(config, reg)
}

if config, ok := config.AuthConfigs[registry]; ok {
return AuthConfig(config), nil
func AuthConfigFromString(raw string, reg string) (AuthConfig, error) {
var cfg dockercfg.Config
err := json.Unmarshal([]byte(raw), &cfg)
if err != nil {
return AuthConfig{}, xerrors.Errorf("parse config: %w", err)
}
return parseConfig(cfg, reg)
}

username, secret, err := config.GetRegistryCredentials(hostname)
func parseConfig(cfg dockercfg.Config, registry string) (AuthConfig, error) {
hostname := dockercfg.ResolveRegistryHost(registry)

username, secret, err := cfg.GetRegistryCredentials(hostname)
if err != nil {
return AuthConfig{}, xerrors.Errorf("get credentials from helper: %w", err)
}
Expand All @@ -80,22 +89,3 @@ func AuthConfigFromPath(path string, registry string) (AuthConfig, error) {

return AuthConfig{}, xerrors.Errorf("no auth config found for registry %s: %w", registry, os.ErrNotExist)
}

func ParseAuthConfig(raw string) (AuthConfig, error) {
type dockerConfig struct {
AuthConfigs map[string]dockertypes.AuthConfig `json:"auths"`
}

var conf dockerConfig
if err := json.Unmarshal([]byte(raw), &conf); err != nil {
return AuthConfig{}, xerrors.Errorf("parse docker auth config secret: %w", err)
}
if len(conf.AuthConfigs) != 1 {
return AuthConfig{}, xerrors.Errorf("number of image pull auth configs not equal to 1 (%d)", len(conf.AuthConfigs))
}
for _, regConfig := range conf.AuthConfigs {
return AuthConfig(regConfig), nil
}

return AuthConfig{}, xerrors.New("no auth configs parsed.")
}
41 changes: 41 additions & 0 deletions dockerutil/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dockerutil_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/envbox/dockerutil"
)

func TestAuthConfigFromString(t *testing.T) {
t.Parallel()

t.Run("Auth", func(t *testing.T) {
t.Parallel()

//nolint:gosec // this is a test
creds := `{ "auths": { "docker.registry.test": { "auth": "Zm9vQGJhci5jb206YWJjMTIz" } } }`
expectedUsername := "foo@bar.com"
expectedPassword := "abc123"

cfg, err := dockerutil.AuthConfigFromString(creds, "docker.registry.test")
require.NoError(t, err)
require.Equal(t, expectedUsername, cfg.Username)
require.Equal(t, expectedPassword, cfg.Password)
})

t.Run("UsernamePassword", func(t *testing.T) {
t.Parallel()

//nolint:gosec // this is a test
creds := `{ "auths": { "docker.registry.test": { "username": "foobarbaz", "password": "123abc" } } }`
expectedUsername := "foobarbaz"
expectedPassword := "123abc"

cfg, err := dockerutil.AuthConfigFromString(creds, "docker.registry.test")
require.NoError(t, err)
require.Equal(t, expectedUsername, cfg.Username)
require.Equal(t, expectedPassword, cfg.Password)
})
}
22 changes: 22 additions & 0 deletions integration/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package integration_test

import (
"encoding/json"
"fmt"
"net"
"os"
Expand All @@ -17,6 +18,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/coder/envbox/cli"
"github.com/coder/envbox/dockerutil"
"github.com/coder/envbox/integration/integrationtest"
)

Expand Down Expand Up @@ -318,19 +320,39 @@ func TestDocker(t *testing.T) {
regKeyPath := filepath.Join(certDir, "registry_key.pem")
integrationtest.WriteCertificate(t, dockerCert, regCertPath, regKeyPath)

username := "coder"
password := "helloworld"

// Start up the docker registry and push an image
// to it that we can reference.
image := integrationtest.RunLocalDockerRegistry(t, pool, integrationtest.RegistryConfig{
HostCertPath: regCertPath,
HostKeyPath: regKeyPath,
Image: integrationtest.UbuntuImage,
TLSPort: strconv.Itoa(registryAddr.Port),
PasswordDir: dir,
Username: username,
Password: password,
})

type authConfigs struct {
Auths map[string]dockerutil.AuthConfig `json:"auths"`
}

auths := authConfigs{
Auths: map[string]dockerutil.AuthConfig{
image.Registry(): {Username: username, Password: password},
},
}

authStr, err := json.Marshal(auths)
require.NoError(t, err)

envs := []string{
integrationtest.EnvVar(cli.EnvAgentToken, "faketoken"),
integrationtest.EnvVar(cli.EnvAgentURL, fmt.Sprintf("https://%s:%d", "host.docker.internal", coderAddr.Port)),
integrationtest.EnvVar(cli.EnvExtraCertsPath, "/tmp/certs"),
integrationtest.EnvVar(cli.EnvBoxPullImageSecretEnvVar, string(authStr)),
}

// Run the envbox container.
Expand Down
Loading

0 comments on commit e557d25

Please sign in to comment.