mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-08 00:50:20 +09:00
172 lines
4.1 KiB
Go
172 lines
4.1 KiB
Go
package dockertestutil
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cenkalti/backoff/v5"
|
|
"github.com/ory/dockertest/v3"
|
|
"github.com/ory/dockertest/v3/docker"
|
|
)
|
|
|
|
const dockerHubServer = "https://index.docker.io/v1/"
|
|
|
|
type CredentialSource string
|
|
|
|
const (
|
|
CredentialSourceEnv CredentialSource = "env"
|
|
CredentialSourceConfig CredentialSource = "config"
|
|
CredentialSourceAnonymous CredentialSource = "anonymous"
|
|
)
|
|
|
|
// Credentials resolves Docker Hub credentials from
|
|
// DOCKERHUB_USERNAME/DOCKERHUB_TOKEN, then ~/.docker/config.json, then
|
|
// anonymous. The Docker Go SDKs do not read config.json on their own.
|
|
func Credentials() (string, string, CredentialSource) {
|
|
if u := os.Getenv("DOCKERHUB_USERNAME"); u != "" {
|
|
return u, os.Getenv("DOCKERHUB_TOKEN"), CredentialSourceEnv
|
|
}
|
|
|
|
user, pass, ok := credentialsFromConfig()
|
|
if ok {
|
|
return user, pass, CredentialSourceConfig
|
|
}
|
|
|
|
return "", "", CredentialSourceAnonymous
|
|
}
|
|
|
|
// AuthConfiguration returns Docker Hub auth for the dockertest pool.
|
|
func AuthConfiguration() docker.AuthConfiguration {
|
|
u, p, _ := Credentials()
|
|
|
|
return docker.AuthConfiguration{
|
|
Username: u,
|
|
Password: p,
|
|
ServerAddress: dockerHubServer,
|
|
}
|
|
}
|
|
|
|
// RegistryAuth returns base64-encoded credentials for the modern
|
|
// Docker SDK's image.PullOptions{RegistryAuth: ...}, or "" when none.
|
|
func RegistryAuth() (string, error) {
|
|
u, p, _ := Credentials()
|
|
if u == "" && p == "" {
|
|
return "", nil
|
|
}
|
|
|
|
auth := struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}{Username: u, Password: p}
|
|
|
|
b, err := json.Marshal(auth) //nolint:gosec // G117: password field holds the Docker Hub token, intentional
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshalling docker auth: %w", err)
|
|
}
|
|
|
|
return base64.URLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// PullWithAuth ensures imageRef is local, pulling with auth and
|
|
// retrying transient errors when it is not.
|
|
func PullWithAuth(pool *dockertest.Pool, imageRef string) error {
|
|
if img, _ := pool.Client.InspectImage(imageRef); img != nil {
|
|
return nil
|
|
}
|
|
|
|
repo, tag := splitImageRef(imageRef)
|
|
auth := AuthConfiguration()
|
|
|
|
_, err := backoff.Retry(
|
|
context.Background(),
|
|
func() (struct{}, error) {
|
|
err := pool.Client.PullImage(docker.PullImageOptions{
|
|
Repository: repo,
|
|
Tag: tag,
|
|
}, auth)
|
|
if err == nil {
|
|
return struct{}{}, nil
|
|
}
|
|
|
|
if isPermanentPullError(err) {
|
|
return struct{}{}, backoff.Permanent(err)
|
|
}
|
|
|
|
return struct{}{}, fmt.Errorf("pulling %s: %w", imageRef, err)
|
|
},
|
|
backoff.WithBackOff(backoff.NewExponentialBackOff()),
|
|
backoff.WithMaxElapsedTime(60*time.Second),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("pulling %s with auth (source=%s): %w", imageRef, AuthConfiguration().ServerAddress, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func splitImageRef(ref string) (string, string) {
|
|
if i := strings.LastIndex(ref, ":"); i >= 0 {
|
|
return ref[:i], ref[i+1:]
|
|
}
|
|
|
|
return ref, "latest"
|
|
}
|
|
|
|
func isPermanentPullError(err error) bool {
|
|
msg := strings.ToLower(err.Error())
|
|
|
|
return strings.Contains(msg, "manifest unknown") ||
|
|
strings.Contains(msg, "manifest not found") ||
|
|
strings.Contains(msg, "repository does not exist") ||
|
|
strings.Contains(msg, "name unknown") ||
|
|
strings.Contains(msg, "no such image")
|
|
}
|
|
|
|
// credentialsFromConfig reads the Hub entry from ~/.docker/config.json.
|
|
// Credential helpers (osxkeychain etc.) are not supported; use env vars.
|
|
func credentialsFromConfig() (string, string, bool) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
|
|
raw, err := os.ReadFile(filepath.Join(home, ".docker", "config.json"))
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
|
|
var cfg struct {
|
|
Auths map[string]struct {
|
|
Auth string `json:"auth"`
|
|
} `json:"auths"`
|
|
}
|
|
|
|
err = json.Unmarshal(raw, &cfg)
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
|
|
entry, found := cfg.Auths[dockerHubServer]
|
|
if !found || entry.Auth == "" {
|
|
return "", "", false
|
|
}
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(entry.Auth)
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
|
|
user, pass, ok := strings.Cut(string(decoded), ":")
|
|
if !ok {
|
|
return "", "", false
|
|
}
|
|
|
|
return user, pass, true
|
|
}
|