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 <guillaume.derouville@gmail.com>
This commit is contained in:
guillaume 2022-01-19 01:37:45 +01:00
parent 7a79395bd1
commit da7b77ed5c
2 changed files with 330 additions and 7 deletions

View File

@ -2,16 +2,18 @@ package solver
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"sync" "sync"
"github.com/docker/distribution/reference"
bkauth "github.com/moby/buildkit/session/auth" bkauth "github.com/moby/buildkit/session/auth"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
const defaultDockerDomain = "docker.io"
// RegistryAuthProvider is a buildkit provider for registry authentication // RegistryAuthProvider is a buildkit provider for registry authentication
// Adapted from: https://github.com/moby/buildkit/blob/master/session/auth/authprovider/authprovider.go // Adapted from: https://github.com/moby/buildkit/blob/master/session/auth/authprovider/authprovider.go
type RegistryAuthProvider struct { 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) { func (a *RegistryAuthProvider) Credentials(ctx context.Context, req *bkauth.CredentialsRequest) (*bkauth.CredentialsResponse, error) {
host := req.Host host := req.Host
if host == "registry-1.docker.io" { if host == "registry-1.docker.io" {
host = "docker.io" host = defaultDockerDomain
} }
a.m.RLock() a.m.RLock()
@ -53,7 +55,6 @@ func (a *RegistryAuthProvider) Credentials(ctx context.Context, req *bkauth.Cred
if err != nil { if err != nil {
return nil, err return nil, err
} }
if u == host { if u == host {
return auth, nil return auth, nil
} }
@ -62,16 +63,57 @@ func (a *RegistryAuthProvider) Credentials(ctx context.Context, req *bkauth.Cred
return &bkauth.CredentialsResponse{}, nil return &bkauth.CredentialsResponse{}, nil
} }
// Parsing function based on splitReposSearchTerm
// "github.com/docker/docker/registry"
func parseAuthHost(host string) (string, error) { func parseAuthHost(host string) (string, error) {
host = strings.TrimPrefix(host, "http://") host = strings.TrimPrefix(host, "http://")
host = strings.TrimPrefix(host, "https://") 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 { // if ":" > 1, trim after last ":" found
return "", err 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) { func (a *RegistryAuthProvider) FetchToken(ctx context.Context, req *bkauth.FetchTokenRequest) (rr *bkauth.FetchTokenResponse, err error) {

281
solver/registryauth_test.go Normal file
View File

@ -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)
}
}
}