Skip to content

Commit

Permalink
perf: optimise docker authentication config lookup (testcontainers#2646)
Browse files Browse the repository at this point in the history
Improve the performance of retrieving docker authentication
configuration by performing requests in parallel and only performing
lookups once.

For a simple double layer image with standard Google Cloud Registry
configuration this reduced the time spent doing authentication config
lookups from ~4.6s to ~400ms.
  • Loading branch information
stevenh authored Jul 16, 2024
1 parent eb22239 commit 70856f4
Showing 1 changed file with 52 additions and 27 deletions.
79 changes: 52 additions & 27 deletions docker_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"net/url"
"os"
"sync"

"github.com/cpuguy83/dockercfg"
"github.com/docker/docker/api/types/registry"
Expand Down Expand Up @@ -79,52 +80,76 @@ func defaultRegistry(ctx context.Context) string {
return info.IndexServerAddress
}

// authConfig represents the details of the auth config for a registry.
type authConfig struct {
key string
cfg registry.AuthConfig
}

// getDockerAuthConfigs returns a map with the auth configs from the docker config file
// using the registry as the key
func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
var getDockerAuthConfigs = sync.OnceValues(func() (map[string]registry.AuthConfig, error) {
cfg, err := getDockerConfig()
if err != nil {
return nil, err
}

cfgs := map[string]registry.AuthConfig{}
results := make(chan authConfig, len(cfg.AuthConfigs)+len(cfg.CredentialHelpers))
var wg sync.WaitGroup
wg.Add(len(cfg.AuthConfigs) + len(cfg.CredentialHelpers))
for k, v := range cfg.AuthConfigs {
ac := registry.AuthConfig{
Auth: v.Auth,
Email: v.Email,
IdentityToken: v.IdentityToken,
Password: v.Password,
RegistryToken: v.RegistryToken,
ServerAddress: v.ServerAddress,
Username: v.Username,
}
go func(k string, v dockercfg.AuthConfig) {
defer wg.Done()

ac := registry.AuthConfig{
Auth: v.Auth,
Email: v.Email,
IdentityToken: v.IdentityToken,
Password: v.Password,
RegistryToken: v.RegistryToken,
ServerAddress: v.ServerAddress,
Username: v.Username,
}

if v.Username == "" && v.Password == "" {
u, p, _ := dockercfg.GetRegistryCredentials(k)
ac.Username = u
ac.Password = p
}

if v.Auth == "" {
ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password))
}
results <- authConfig{key: k, cfg: ac}
}(k, v)
}

// in the case where the auth field in the .docker/conf.json is empty, and the user has credential helpers registered
// the auth comes from there
for k := range cfg.CredentialHelpers {
go func(k string) {
defer wg.Done()

if v.Username == "" && v.Password == "" {
ac := registry.AuthConfig{}
u, p, _ := dockercfg.GetRegistryCredentials(k)
ac.Username = u
ac.Password = p
}

if v.Auth == "" {
ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password))
}

cfgs[k] = ac
results <- authConfig{key: k, cfg: ac}
}(k)
}

// in the case where the auth field in the .docker/conf.json is empty, and the user has credential helpers registered
// the auth comes from there
for k := range cfg.CredentialHelpers {
ac := registry.AuthConfig{}
u, p, _ := dockercfg.GetRegistryCredentials(k)
ac.Username = u
ac.Password = p
go func() {
wg.Wait()
close(results)
}()

cfgs[k] = ac
for ac := range results {
cfgs[ac.key] = ac.cfg
}

return cfgs, nil
}
})

// getDockerConfig returns the docker config file. It will internally check, in this particular order:
// 1. the DOCKER_AUTH_CONFIG environment variable, unmarshalling it into a dockercfg.Config
Expand Down

0 comments on commit 70856f4

Please sign in to comment.