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

fix: parse image auth config correctly #103

Merged
merged 7 commits into from
Oct 21, 2024
Merged
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
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)
}
sreya marked this conversation as resolved.
Show resolved Hide resolved

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
Loading