diff --git a/remotes/docker/config/hosts.go b/remotes/docker/config/hosts.go index 100f6be1f..60bf9148e 100644 --- a/remotes/docker/config/hosts.go +++ b/remotes/docker/config/hosts.go @@ -48,6 +48,8 @@ type hostConfig struct { clientPairs [][2]string skipVerify *bool + header http.Header + // TODO: API ("docker" or "oci") // TODO: API Version ("v1", "v2") // TODO: Add credential configuration (domain alias, username) @@ -143,6 +145,7 @@ func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHos rhosts[i].Host = host.host rhosts[i].Path = host.path rhosts[i].Capabilities = host.capabilities + rhosts[i].Header = host.header if host.caCerts != nil || host.clientPairs != nil || host.skipVerify != nil { tr := defaultTransport.Clone() @@ -203,7 +206,6 @@ func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHos rhosts[i].Client = client rhosts[i].Authorizer = authorizer } - } return rhosts, nil @@ -274,6 +276,8 @@ type hostFileConfig struct { SkipVerify *bool `toml:"skip_verify"` + Header map[string]toml.Primitive `toml:"header"` + // API (default: "docker") // API Version (default: "v2") // Credentials: helper? name? username? alternate domain? token? @@ -433,6 +437,31 @@ func parseHostsFile(ctx context.Context, baseDir string, b []byte) ([]hostConfig return nil, errors.Errorf("invalid type %v for \"client\"", t) } } + + headerKey := append(baseKey, "header") + if md.IsDefined(headerKey...) { + header := http.Header{} + for key, prim := range hostConfig.Header { + switch t := md.Type(append(headerKey, key)...); t { + case "String": + var value string + if err := md.PrimitiveDecode(prim, &value); err != nil { + return nil, errors.Wrapf(err, "failed to decode header %q", key) + } + header[key] = []string{value} + case "Array": + var value []string + if err := md.PrimitiveDecode(prim, &value); err != nil { + return nil, errors.Wrapf(err, "failed to decode header %q", key) + } + + header[key] = value + default: + return nil, errors.Errorf("invalid type %v for header %q", t, key) + } + } + hosts[i].header = header + } } return hosts, nil diff --git a/remotes/docker/config/hosts_test.go b/remotes/docker/config/hosts_test.go index 18857a000..194d3d8af 100644 --- a/remotes/docker/config/hosts_test.go +++ b/remotes/docker/config/hosts_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "fmt" + "net/http" "path/filepath" "testing" @@ -76,11 +77,15 @@ func TestParseHostFile(t *testing.T) { const testtoml = ` server = "https://test-default.registry" ca = "/etc/path/default" +[header] + x-custom-1 = "custom header" [host."https://mirror.registry"] capabilities = ["pull"] ca = "/etc/certs/mirror.pem" skip_verify = false + [host."https://mirror.registry".header] + x-custom-2 = ["value1", "value2"] [host."https://mirror-bak.registry/us"] capabilities = ["pull"] @@ -109,6 +114,7 @@ ca = "/etc/path/default" capabilities: docker.HostCapabilityPull, caCerts: []string{filepath.FromSlash("/etc/certs/mirror.pem")}, skipVerify: &fb, + header: http.Header{"x-custom-2": {"value1", "value2"}}, }, { scheme: "https", @@ -159,6 +165,7 @@ ca = "/etc/path/default" path: "/v2", capabilities: allCaps, caCerts: []string{filepath.FromSlash("/etc/path/default")}, + header: http.Header{"x-custom-1": {"custom header"}}, }, } hosts, err := parseHostsFile(ctx, "", []byte(testtoml)) @@ -241,6 +248,20 @@ func compareHostConfig(j, k hostConfig) bool { return false } + if len(j.header) != len(k.header) { + return false + } + for key := range j.header { + if len(j.header[key]) != len(k.header[key]) { + return false + } + for i := range j.header[key] { + if j.header[key][i] != k.header[key][i] { + return false + } + } + } + return true } @@ -258,6 +279,7 @@ func printHostConfig(hc []hostConfig) string { } else { fmt.Fprintf(b, "\t\tskip-verify: %t\n", *hc[i].skipVerify) } + fmt.Fprintf(b, "\t\theader: %#v\n", hc[i].header) } return b.String() } diff --git a/remotes/docker/registry.go b/remotes/docker/registry.go index ae24f41e1..ffc939b40 100644 --- a/remotes/docker/registry.go +++ b/remotes/docker/registry.go @@ -70,6 +70,7 @@ type RegistryHost struct { Scheme string Path string Capabilities HostCapabilities + Header http.Header } // RegistryHosts fetches the registry hosts for a given namespace, diff --git a/remotes/docker/resolver.go b/remotes/docker/resolver.go index 503f72aaf..32b6abd90 100644 --- a/remotes/docker/resolver.go +++ b/remotes/docker/resolver.go @@ -450,6 +450,9 @@ func (r *dockerBase) request(host RegistryHost, method string, ps ...string) *re for key, value := range r.header { header[key] = append(header[key], value...) } + for key, value := range host.Header { + header[key] = append(header[key], value...) + } parts := append([]string{"/", host.Path, r.namespace}, ps...) p := path.Join(parts...) // Join strips trailing slash, re-add ending "/" if included