From da7b77ed5c3f1248e8108afbebdcbb61fa81ab20 Mon Sep 17 00:00:00 2001 From: guillaume Date: Wed, 19 Jan 2022 01:37:45 +0100 Subject: [PATCH] Re-implement docker registry parsing Dagger used to rely on registry.ParseNormalize function to extract registry domains from images / registry URLs. However, it contained some flaws for private registries. This PR fixes that by implementing a test suite around it, and tweaks the splitReposSearchTerm function from the docker CLI. The logic of splitReposSearchTerm is kept, and enhanced to fit to all of our use cases. In case of a bad matching, a clear error is returned Signed-off-by: guillaume --- solver/registryauth.go | 56 ++++++- solver/registryauth_test.go | 281 ++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+), 7 deletions(-) create mode 100644 solver/registryauth_test.go diff --git a/solver/registryauth.go b/solver/registryauth.go index cf2684fa..04607758 100644 --- a/solver/registryauth.go +++ b/solver/registryauth.go @@ -2,16 +2,18 @@ package solver import ( "context" + "fmt" "strings" "sync" - "github.com/docker/distribution/reference" bkauth "github.com/moby/buildkit/session/auth" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) +const defaultDockerDomain = "docker.io" + // RegistryAuthProvider is a buildkit provider for registry authentication // Adapted from: https://github.com/moby/buildkit/blob/master/session/auth/authprovider/authprovider.go type RegistryAuthProvider struct { @@ -42,7 +44,7 @@ func (a *RegistryAuthProvider) Register(server *grpc.Server) { func (a *RegistryAuthProvider) Credentials(ctx context.Context, req *bkauth.CredentialsRequest) (*bkauth.CredentialsResponse, error) { host := req.Host if host == "registry-1.docker.io" { - host = "docker.io" + host = defaultDockerDomain } a.m.RLock() @@ -53,7 +55,6 @@ func (a *RegistryAuthProvider) Credentials(ctx context.Context, req *bkauth.Cred if err != nil { return nil, err } - if u == host { return auth, nil } @@ -62,16 +63,57 @@ func (a *RegistryAuthProvider) Credentials(ctx context.Context, req *bkauth.Cred return &bkauth.CredentialsResponse{}, nil } +// Parsing function based on splitReposSearchTerm +// "github.com/docker/docker/registry" func parseAuthHost(host string) (string, error) { host = strings.TrimPrefix(host, "http://") host = strings.TrimPrefix(host, "https://") + host = strings.TrimSuffix(host, "/") - ref, err := reference.ParseNormalizedNamed(host) + // Remove everything after @ + nameParts := strings.SplitN(host, "@", 2) + host = nameParts[0] - if err != nil { - return "", err + // if ":" > 1, trim after last ":" found + if strings.Count(host, ":") > 1 { + host = host[:strings.LastIndex(host, ":")] } - return reference.Domain(ref), nil + + // if ":" > 0, trim after last ":" found if it contains "." + // ex: samalba/hipache:1.15, registry.com:5000:1.0 + if strings.Count(host, ":") > 0 { + tmpStr := host[strings.LastIndex(host, ":"):] + if strings.Count(tmpStr, ".") > 0 { + host = host[:strings.LastIndex(host, ":")] + } + } + + nameParts = strings.SplitN(host, "/", 2) + var domain string + switch { + // Localhost registry parsing + case strings.Contains(nameParts[0], "localhost"): + domain = nameParts[0] + // If the split returned an array of len 1 that doesn't contain any . + // ex: ubuntu + case len(nameParts) == 1 && !strings.Contains(nameParts[0], "."): + domain = defaultDockerDomain + // if the split does not contain "." nor ":", but contains images + // ex: samalba/hipache, samalba/hipache:1.15, samalba/hipache@sha:... + case !strings.Contains(nameParts[0], ".") && !strings.Contains(nameParts[0], ":"): + domain = defaultDockerDomain + case nameParts[0] == "registry-1.docker.io": + domain = defaultDockerDomain + case nameParts[0] == "index.docker.io": + domain = defaultDockerDomain + // Private remaining registry parsing + case strings.Contains(nameParts[0], "."): + domain = nameParts[0] + // Fail by default + default: + return "", fmt.Errorf("failed parsing [%s] expected host format: [%s]", nameParts[0], "registrydomain.extension") + } + return domain, nil } func (a *RegistryAuthProvider) FetchToken(ctx context.Context, req *bkauth.FetchTokenRequest) (rr *bkauth.FetchTokenResponse, err error) { diff --git a/solver/registryauth_test.go b/solver/registryauth_test.go new file mode 100644 index 00000000..33e30a37 --- /dev/null +++ b/solver/registryauth_test.go @@ -0,0 +1,281 @@ +package solver + +import ( + "testing" +) + +func TestParseAuthHost(t *testing.T) { + type hcase struct { + Host, Domain string + } + + scases := []hcase{ + // Short + { + Host: "foo", + Domain: "docker.io", + }, + { + Host: "foo:1.1", + Domain: "docker.io", + }, + { + Host: "foo@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "docker.io", + }, + + // Short image + { + Host: "foo/bar", + Domain: "docker.io", + }, + { + Host: "foo/bar:1.1", + Domain: "docker.io", + }, + { + Host: "foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "docker.io", + }, + + // Private registry + { + Host: "registry.com", + Domain: "registry.com", + }, + { + Host: "registry.com:1.1", + Domain: "registry.com", + }, + { + Host: "registry.com@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "registry.com", + }, + + // Private image + { + Host: "registry.com/foo/bar", + Domain: "registry.com", + }, + { + Host: "registry.com/foo/bar:1.1", + Domain: "registry.com", + }, + { + Host: "registry.com/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "registry.com", + }, + + // Private registry with port + { + Host: "registry.com:5000", + Domain: "registry.com:5000", + }, + { + Host: "registry.com:5000:1.1", + Domain: "registry.com:5000", + }, + { + Host: "registry.com:5000@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "registry.com:5000", + }, + + // Private image with port + { + Host: "registry.com:5000/foo/bar", + Domain: "registry.com:5000", + }, + { + Host: "registry.com:5000/foo/bar:1.1", + Domain: "registry.com:5000", + }, + { + Host: "registry.com:5000/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "registry.com:5000", + }, + + // docker.io short + { + Host: "docker.io", + Domain: "docker.io", + }, + { + Host: "docker.io:1.1", + Domain: "docker.io", + }, + { + Host: "docker.io@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "docker.io", + }, + + // docker.io image + { + Host: "docker.io/foo/bar", + Domain: "docker.io", + }, + { + Host: "docker.io/foo/bar:1.1", + Domain: "docker.io", + }, + { + Host: "docker.io/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "docker.io", + }, + + // registry-1.docker.io short + { + Host: "registry-1.docker.io", + Domain: "docker.io", + }, + { + Host: "registry-1.docker.io:1.1", + Domain: "docker.io", + }, + { + Host: "registry-1.docker.io@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "docker.io", + }, + + // registry-1.docker.io image + { + Host: "registry-1.docker.io/foo/bar", + Domain: "docker.io", + }, + { + Host: "registry-1.docker.io/foo/bar:1.1", + Domain: "docker.io", + }, + { + Host: "registry-1.docker.io/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "docker.io", + }, + + // index.docker.io short + { + Host: "index.docker.io", + Domain: "docker.io", + }, + { + Host: "index.docker.io:1.1", + Domain: "docker.io", + }, + { + Host: "index.docker.io@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "docker.io", + }, + + // index.docker.io image + { + Host: "index.docker.io/foo/bar", + Domain: "docker.io", + }, + { + Host: "index.docker.io/foo/bar:1.1", + Domain: "docker.io", + }, + { + Host: "index.docker.io/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "docker.io", + }, + + // localhost repository + { + Host: "localhost", + Domain: "localhost", + }, + { + Host: "localhost:1.1", + Domain: "localhost", + }, + { + Host: "localhost@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "localhost", + }, + + // localhost image + { + Host: "localhost/foo/bar", + Domain: "localhost", + }, + { + Host: "localhost/foo/bar:1.1", + Domain: "localhost", + }, + { + Host: "localhost/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "localhost", + }, + + // localhost repository with port + { + Host: "localhost:5000", + Domain: "localhost:5000", + }, + { + Host: "localhost:5000:1.1", + Domain: "localhost:5000", + }, + { + Host: "localhost:5000@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "localhost:5000", + }, + + // localhost image with port + { + Host: "localhost:5000/foo/bar", + Domain: "localhost:5000", + }, + { + Host: "localhost:5000/foo/bar:1.1", + Domain: "localhost:5000", + }, + { + Host: "localhost:5000/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb", + Domain: "localhost:5000", + }, + + // empty host + { + Host: "", + Domain: "docker.io", + }, + { + Host: "/jo", + Domain: "docker.io", + }, + } + + fcases := []hcase{ + { + Host: ":/jo", + }, + } + + type output struct { + expected, actual string + } + + successRefs := []output{} + for _, scase := range scases { + named, err := parseAuthHost(scase.Host) + if err != nil { + t.Fatalf("Invalid normalized reference for [%q]. Got %q", scase, err) + } + successRefs = append(successRefs, output{ + actual: named, + expected: scase.Domain, + }) + } + for _, r := range successRefs { + if r.expected != r.actual { + t.Fatalf("Invalid normalized reference for [%q]. Expected %q, got %q", r, r.expected, r.actual) + } + } + + for _, fcase := range fcases { + named, err := parseAuthHost(fcase.Host) + if err == nil { + t.Fatalf("Invalid normalized reference for [%q]. Expected failure for %q", fcase, named) + } + } +}