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 6 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
4 changes: 2 additions & 2 deletions cli/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,13 +384,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\": \"[email protected]\", \"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":"[email protected]"}`)
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\": \"[email protected]\", \"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.")
}
23 changes: 23 additions & 0 deletions dockerutil/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dockerutil_test

import (
"testing"

"github.com/stretchr/testify/require"

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

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

//nolint:gosec // this is a test
creds := `{ "auths": { "docker.registry.test": { "auth": "Zm9vQGJhci5jb206YWJjMTIz" } } }`
expectedUsername := "[email protected]"
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)
sreya marked this conversation as resolved.
Show resolved Hide resolved
}
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
113 changes: 94 additions & 19 deletions integration/integrationtest/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand All @@ -20,6 +21,7 @@ import (
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
"golang.org/x/xerrors"

"github.com/coder/envbox/buildlog"
Expand Down Expand Up @@ -361,6 +363,10 @@ type RegistryConfig struct {
HostKeyPath string
TLSPort string
Image string
Username string
Password string
// PasswordDir is the directory under which the htpasswd file is written.
PasswordDir string
}

type RegistryImage string
Expand All @@ -373,28 +379,50 @@ func (r RegistryImage) String() string {
return string(r)
}

func RunLocalDockerRegistry(t testing.TB, pool *dockertest.Pool, conf RegistryConfig) RegistryImage {
func RunLocalDockerRegistry(t *testing.T, pool *dockertest.Pool, conf RegistryConfig) RegistryImage {
t.Helper()

const (
certPath = "/certs/cert.pem"
keyPath = "/certs/key.pem"
authPath = "/auth/htpasswd"
)

resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: registryImage,
Tag: registryTag,
Env: []string{
var (
envs = []string{
EnvVar("REGISTRY_HTTP_ADDR", "0.0.0.0:443"),
}
binds []string
)

if conf.HostCertPath != "" && conf.HostKeyPath != "" {
envs = append(envs,
EnvVar("REGISTRY_HTTP_TLS_CERTIFICATE", certPath),
EnvVar("REGISTRY_HTTP_TLS_KEY", keyPath),
EnvVar("REGISTRY_HTTP_ADDR", "0.0.0.0:443"),
},
ExposedPorts: []string{"443/tcp"},
}, func(host *docker.HostConfig) {
host.Binds = []string{
)
binds = append(binds,
mountBinding(conf.HostCertPath, certPath),
mountBinding(conf.HostKeyPath, keyPath),
}
)
}

if conf.PasswordDir != "" {
authFile := GenerateRegistryAuth(t, conf.PasswordDir, conf.Username, conf.Password)
envs = append(envs,
EnvVar("REGISTRY_AUTH", "htpasswd"),
EnvVar("REGISTRY_AUTH_HTPASSWD_REALM", "Test Registry"),
EnvVar("REGISTRY_AUTH_HTPASSWD_PATH", authPath),
)
binds = append(binds, mountBinding(authFile, authPath))
}

resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: registryImage,
Tag: registryTag,
Env: envs,
ExposedPorts: []string{"443/tcp"},
}, func(host *docker.HostConfig) {
host.Binds = binds
host.ExtraHosts = []string{"host.docker.internal:host-gateway"}
host.PortBindings = map[docker.Port][]docker.PortBinding{
"443/tcp": {{
Expand All @@ -415,7 +443,13 @@ func RunLocalDockerRegistry(t testing.TB, pool *dockertest.Pool, conf RegistryCo
url := fmt.Sprintf("https://%s/v2/_catalog", host)

waitForRegistry(t, pool, resource, url)
return pushLocalImage(t, pool, host, conf.Image)
return pushLocalImage(t, pool, pushOptions{
Host: host,
RemoteImage: conf.Image,
Username: conf.Username,
Password: conf.Password,
ConfigDir: conf.PasswordDir,
})
}

func waitForRegistry(t testing.TB, pool *dockertest.Pool, resource *dockertest.Resource, url string) {
Expand Down Expand Up @@ -447,18 +481,26 @@ func waitForRegistry(t testing.TB, pool *dockertest.Pool, resource *dockertest.R
continue
}
_ = res.Body.Close()
if res.StatusCode == http.StatusOK {
if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusUnauthorized {
return
}
}
require.NoError(t, ctx.Err())
}

func pushLocalImage(t testing.TB, pool *dockertest.Pool, host, remoteImage string) RegistryImage {
type pushOptions struct {
Host string
RemoteImage string
Username string
Password string
ConfigDir string
}

func pushLocalImage(t *testing.T, pool *dockertest.Pool, opts pushOptions) RegistryImage {
t.Helper()

const registryHost = "127.0.0.1"
name := filepath.Base(remoteImage)
name := filepath.Base(opts.RemoteImage)
repoTag := strings.Split(name, ":")
tag := "latest"
if len(repoTag) == 2 {
Expand All @@ -469,25 +511,46 @@ func pushLocalImage(t testing.TB, pool *dockertest.Pool, host, remoteImage strin
t: t,
}
err := pool.Client.PullImage(docker.PullImageOptions{
Repository: strings.Split(remoteImage, ":")[0],
Repository: strings.Split(opts.RemoteImage, ":")[0],
Tag: tag,
OutputStream: tw,
}, docker.AuthConfiguration{})
require.NoError(t, err)

_, port, err := net.SplitHostPort(host)
_, port, err := net.SplitHostPort(opts.Host)
require.NoError(t, err)

err = pool.Client.TagImage(remoteImage, docker.TagImageOptions{
err = pool.Client.TagImage(opts.RemoteImage, docker.TagImageOptions{
Repo: fmt.Sprintf("%s:%s/%s", registryHost, port, name),
Tag: tag,
})
require.NoError(t, err)

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

auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", opts.Username, opts.Password)))

cfg := config{
Auths: map[string]dockerutil.AuthConfig{
net.JoinHostPort(registryHost, port): {
Username: opts.Username,
Password: opts.Password,
Auth: auth,
},
},
}
b, err := json.Marshal(cfg)
require.NoError(t, err)
configPath := filepath.Join(opts.ConfigDir, "config.json")
WriteFile(t, configPath, string(b))

// Idk what to tell you but the pool.Client.PushImage
// function is bugged or I'm just dumb...
image := fmt.Sprintf("%s:%s/%s:%s", registryHost, port, name, tag)
cmd := exec.Command("docker", "push", image)
//nolint:gosec
cmd := exec.Command("docker", "--config", opts.ConfigDir, "push", image)
cmd.Stderr = tw
cmd.Stdout = tw
err = cmd.Run()
Expand Down Expand Up @@ -516,3 +579,15 @@ func BindMount(src, dst string, ro bool) docker.HostMount {
Type: "bind",
}
}

func GenerateRegistryAuth(t *testing.T, directory, username, password string) string {
t.Helper()

p, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
require.NoError(t, err)

authFile := filepath.Join(directory, "credentials")
WriteFile(t, authFile, fmt.Sprintf("%s:%s", username, string(p)))

return authFile
}
Loading