mirror of
https://github.com/lingble/talos.git
synced 2025-10-29 19:52:44 +00:00
fix: registry mirror fallback handling
Fixes #9613 This has two changes: * adjust Talos registry resolver to match containerd (CRI) resolver: use by default upstream as a fallback * add a machine config option to skip upstream as a fallback, and adjust CRI configuration accordingly See https://github.com/containerd/containerd/blob/main/docs/hosts.md#registry-configuration---examples for details on CRI's `hosts.toml`. Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
@@ -69,6 +69,18 @@ This command allows you to view the cgroup resource consumption and limits for a
|
||||
title = "udevd"
|
||||
description = """\
|
||||
Talos previously used `eudev` to provide `udevd`, now it uses `systemd-udevd` instead.
|
||||
"""
|
||||
|
||||
[notes.registry-mirrors]
|
||||
title = "Registry Mirrors"
|
||||
description = """\
|
||||
In versions before Talos 1.9, there was a discrepancy between the way Talos itself and CRI plugin resolves registry mirrors:
|
||||
Talos will never fall back to the default registry if endpoints are configured, while CRI plugin will.
|
||||
|
||||
> Note: Talos Linux pulls images for the `installer`, `kubelet`, `etcd`, while all workload images are pulled by the CRI plugin.
|
||||
|
||||
In Talos 1.9 this was fixed, so that by default an upstream registry is used as a fallback in all cases, while new registry mirror
|
||||
configuration option `.skipFallback` can be used to disable this behavior both for Talos and CRI plugin.
|
||||
"""
|
||||
|
||||
[make_deps]
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/containerd/containerd/v2/core/remotes/docker"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/siderolabs/gen/optional"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/config"
|
||||
)
|
||||
@@ -42,7 +43,7 @@ type HostsFile struct {
|
||||
|
||||
// GenerateHosts generates a structure describing contents of the containerd hosts configuration.
|
||||
//
|
||||
//nolint:gocyclo,cyclop
|
||||
//nolint:gocyclo
|
||||
func GenerateHosts(cfg config.Registries, basePath string) (*HostsConfig, error) {
|
||||
config := &HostsConfig{
|
||||
Directories: map[string]*HostsDirectory{},
|
||||
@@ -106,65 +107,41 @@ func GenerateHosts(cfg config.Registries, basePath string) (*HostsConfig, error)
|
||||
|
||||
directory := &HostsDirectory{}
|
||||
|
||||
// toml marshaling doesn't guarantee proper order of map keys, so instead we should marshal
|
||||
// each time and append to the output
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
for i, endpoint := range endpoints.Endpoints() {
|
||||
hostsToml := HostsToml{
|
||||
HostConfigs: map[string]*HostToml{},
|
||||
}
|
||||
var hostsConfig HostsConfiguration
|
||||
|
||||
for _, endpoint := range endpoints.Endpoints() {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing endpoint %q for host %q: %w", endpoint, registryName, err)
|
||||
}
|
||||
|
||||
hostsToml.HostConfigs[endpoint] = &HostToml{
|
||||
hostEntry := HostEntry{
|
||||
Host: endpoint,
|
||||
HostToml: HostToml{
|
||||
Capabilities: []string{"pull", "resolve"}, // TODO: we should make it configurable eventually
|
||||
OverridePath: endpoints.OverridePath(),
|
||||
},
|
||||
}
|
||||
|
||||
configureEndpoint(u.Host, directoryName, hostsToml.HostConfigs[endpoint], directory)
|
||||
configureEndpoint(u.Host, directoryName, &hostEntry.HostToml, directory)
|
||||
|
||||
var tomlBuf bytes.Buffer
|
||||
hostsConfig.HostEntries = append(hostsConfig.HostEntries, hostEntry)
|
||||
}
|
||||
|
||||
if err := toml.NewEncoder(&tomlBuf).SetIndentTables(true).Encode(hostsToml); err != nil {
|
||||
if endpoints.SkipFallback() {
|
||||
hostsConfig.DisableFallback()
|
||||
}
|
||||
|
||||
cfgOut, err := hostsConfig.RenderTOML()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tomlBytes := tomlBuf.Bytes()
|
||||
|
||||
// this is an ugly hack, and neither TOML format nor go-toml library make it easier
|
||||
//
|
||||
// we need to marshal each endpoint in the order they are specified in the config, but go-toml defines
|
||||
// the tree as map[string]interface{} and doesn't guarantee the order of keys
|
||||
//
|
||||
// so we marshal each entry separately and combine the output, which results in something like:
|
||||
//
|
||||
// [host]
|
||||
// [host."foo.bar"]
|
||||
// [host]
|
||||
// [host."bar.foo"]
|
||||
//
|
||||
// but this is invalid TOML, as `[host]' is repeated, so we do an ugly hack and remove it below
|
||||
const hostPrefix = "[host]\n"
|
||||
|
||||
if i > 0 {
|
||||
if bytes.HasPrefix(tomlBytes, []byte(hostPrefix)) {
|
||||
tomlBytes = tomlBytes[len(hostPrefix):]
|
||||
}
|
||||
}
|
||||
|
||||
buf.Write(tomlBytes)
|
||||
}
|
||||
|
||||
directory.Files = append(directory.Files,
|
||||
&HostsFile{
|
||||
Name: "hosts.toml",
|
||||
Mode: 0o600,
|
||||
Contents: buf.Bytes(),
|
||||
Contents: cfgOut,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -199,17 +176,18 @@ func GenerateHosts(cfg config.Registries, basePath string) (*HostsConfig, error)
|
||||
|
||||
defaultHost = "https://" + defaultHost
|
||||
|
||||
hostsToml := HostsToml{
|
||||
HostConfigs: map[string]*HostToml{
|
||||
defaultHost: {},
|
||||
},
|
||||
rootEntry := HostEntry{
|
||||
Host: defaultHost,
|
||||
}
|
||||
|
||||
configureEndpoint(hostname, directoryName, hostsToml.HostConfigs[defaultHost], directory)
|
||||
configureEndpoint(hostname, directoryName, &rootEntry.HostToml, directory)
|
||||
|
||||
var tomlBuf bytes.Buffer
|
||||
hostsToml := HostsConfiguration{
|
||||
RootEntry: optional.Some(rootEntry),
|
||||
}
|
||||
|
||||
if err = toml.NewEncoder(&tomlBuf).SetIndentTables(true).Encode(hostsToml); err != nil {
|
||||
cfgOut, err := hostsToml.RenderTOML()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -217,7 +195,7 @@ func GenerateHosts(cfg config.Registries, basePath string) (*HostsConfig, error)
|
||||
&HostsFile{
|
||||
Name: "hosts.toml",
|
||||
Mode: 0o600,
|
||||
Contents: tomlBuf.Bytes(),
|
||||
Contents: cfgOut,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -241,10 +219,106 @@ func hostDirectory(host string) string {
|
||||
return host
|
||||
}
|
||||
|
||||
// HostsToml describes the contents of the `hosts.toml` file.
|
||||
type HostsToml struct {
|
||||
// HostEntry describes the configuration for a single host.
|
||||
type HostEntry struct {
|
||||
Host string
|
||||
HostToml
|
||||
}
|
||||
|
||||
// HostsConfiguration describes the configuration of `hosts.toml` file in the format not compatible with TOML.
|
||||
//
|
||||
// The hosts entries should come in order, and go-toml only supports map[string]any, so we need to do some tricks.
|
||||
type HostsConfiguration struct {
|
||||
RootEntry optional.Optional[HostEntry] // might be missing
|
||||
|
||||
HostEntries []HostEntry
|
||||
}
|
||||
|
||||
// DisableFallback disables the fallback to the default host.
|
||||
func (hc *HostsConfiguration) DisableFallback() {
|
||||
if len(hc.HostEntries) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// push the last entry as the root entry
|
||||
hc.RootEntry = optional.Some(hc.HostEntries[len(hc.HostEntries)-1])
|
||||
|
||||
hc.HostEntries = hc.HostEntries[:len(hc.HostEntries)-1]
|
||||
}
|
||||
|
||||
// RenderTOML renders the configuration to TOML format.
|
||||
func (hc *HostsConfiguration) RenderTOML() ([]byte, error) {
|
||||
var out bytes.Buffer
|
||||
|
||||
// toml marshaling doesn't guarantee proper order of map keys, so instead we should marshal
|
||||
// each time and append to the output
|
||||
|
||||
if rootEntry, ok := hc.RootEntry.Get(); ok {
|
||||
server := HostsTomlServer{
|
||||
Server: rootEntry.Host,
|
||||
HostToml: rootEntry.HostToml,
|
||||
}
|
||||
|
||||
if err := toml.NewEncoder(&out).SetIndentTables(true).Encode(server); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for i, entry := range hc.HostEntries {
|
||||
hostEntry := HostsTomlHost{
|
||||
HostConfigs: map[string]HostToml{
|
||||
entry.Host: entry.HostToml,
|
||||
},
|
||||
}
|
||||
|
||||
var tomlBuf bytes.Buffer
|
||||
|
||||
if err := toml.NewEncoder(&tomlBuf).SetIndentTables(true).Encode(hostEntry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tomlBytes := tomlBuf.Bytes()
|
||||
|
||||
// this is an ugly hack, and neither TOML format nor go-toml library make it easier
|
||||
//
|
||||
// we need to marshal each endpoint in the order they are specified in the config, but go-toml defines
|
||||
// the tree as map[string]interface{} and doesn't guarantee the order of keys
|
||||
//
|
||||
// so we marshal each entry separately and combine the output, which results in something like:
|
||||
//
|
||||
// [host]
|
||||
// [host."foo.bar"]
|
||||
// [host]
|
||||
// [host."bar.foo"]
|
||||
//
|
||||
// but this is invalid TOML, as `[host]' is repeated, so we do an ugly hack and remove it below
|
||||
const hostPrefix = "[host]\n"
|
||||
|
||||
if i > 0 {
|
||||
if bytes.HasPrefix(tomlBytes, []byte(hostPrefix)) {
|
||||
tomlBytes = tomlBytes[len(hostPrefix):]
|
||||
}
|
||||
}
|
||||
|
||||
out.Write(tomlBytes)
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
// HostsTomlServer describes only 'server' part of the `hosts.toml` file.
|
||||
type HostsTomlServer struct {
|
||||
// top-level entry is used as the last one in the fallback chain.
|
||||
Server string `toml:"server,omitempty"`
|
||||
HostConfigs map[string]*HostToml `toml:"host"`
|
||||
HostToml // embedded, matches the server
|
||||
}
|
||||
|
||||
// HostsTomlHost describes the `hosts.toml` file entry for hosts.
|
||||
//
|
||||
// It is supposed to be marshaled as a single-entry map to keep the order correct.
|
||||
type HostsTomlHost struct {
|
||||
// Note: this doesn't match the TOML format, but allows use to keep endpoints ordered properly.
|
||||
HostConfigs map[string]HostToml `toml:"host"`
|
||||
}
|
||||
|
||||
// HostToml is a single entry in `hosts.toml`.
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestGenerateHostsWithTLS(t *testing.T) {
|
||||
{
|
||||
Name: "hosts.toml",
|
||||
Mode: 0o600,
|
||||
Contents: []byte("[host]\n [host.'https://some.host:123']\n ca = '/etc/cri/conf.d/hosts/some.host_123_/some.host:123-ca.crt'\n client = [['/etc/cri/conf.d/hosts/some.host_123_/some.host:123-client.crt', '/etc/cri/conf.d/hosts/some.host_123_/some.host:123-client.key']]\n skip_verify = true\n"), //nolint:lll
|
||||
Contents: []byte("server = 'https://some.host:123'\nca = '/etc/cri/conf.d/hosts/some.host_123_/some.host:123-ca.crt'\nclient = [['/etc/cri/conf.d/hosts/some.host_123_/some.host:123-client.crt', '/etc/cri/conf.d/hosts/some.host_123_/some.host:123-client.key']]\nskip_verify = true\n"), //nolint:lll
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -92,7 +92,7 @@ func TestGenerateHostsWithTLS(t *testing.T) {
|
||||
{
|
||||
Name: "hosts.toml",
|
||||
Mode: 0o600,
|
||||
Contents: []byte("[host]\n [host.'https://registry-2.docker.io']\n skip_verify = true\n"),
|
||||
Contents: []byte("server = 'https://registry-2.docker.io'\nskip_verify = true\n"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -210,7 +210,7 @@ func TestGenerateHostsTLSWildcard(t *testing.T) {
|
||||
{
|
||||
Name: "hosts.toml",
|
||||
Mode: 0o600,
|
||||
Contents: []byte("[host]\n [host.'https://my-registry1']\n ca = '/etc/cri/conf.d/hosts/my-registry1/my-registry1-ca.crt'\n"),
|
||||
Contents: []byte("server = 'https://my-registry1'\nca = '/etc/cri/conf.d/hosts/my-registry1/my-registry1-ca.crt'\n"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -278,7 +278,58 @@ func TestGenerateHostsWithHarbor(t *testing.T) {
|
||||
{
|
||||
Name: "hosts.toml",
|
||||
Mode: 0o600,
|
||||
Contents: []byte("[host]\n [host.'https://harbor']\n skip_verify = true\n"),
|
||||
Contents: []byte("server = 'https://harbor'\nskip_verify = true\n"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, result)
|
||||
}
|
||||
|
||||
func TestGenerateHostsSkipFallback(t *testing.T) {
|
||||
cfg := &mockConfig{
|
||||
mirrors: map[string]*v1alpha1.RegistryMirrorConfig{
|
||||
"docker.io": {
|
||||
MirrorEndpoints: []string{"https://harbor/v2/mirrors/proxy.docker.io", "http://127.0.0.1:5001/v2/"},
|
||||
MirrorOverridePath: pointer.To(true),
|
||||
MirrorSkipFallback: pointer.To(true),
|
||||
},
|
||||
"ghcr.io": {
|
||||
MirrorEndpoints: []string{"http://127.0.0.1:5002"},
|
||||
MirrorSkipFallback: pointer.To(true),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := containerd.GenerateHosts(cfg, "/etc/cri/conf.d/hosts")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf(
|
||||
"config docker.io %q",
|
||||
string(result.Directories["docker.io"].Files[0].Contents),
|
||||
)
|
||||
t.Logf(
|
||||
"config ghcr.io %q",
|
||||
string(result.Directories["ghcr.io"].Files[0].Contents),
|
||||
)
|
||||
|
||||
assert.Equal(t, &containerd.HostsConfig{
|
||||
Directories: map[string]*containerd.HostsDirectory{
|
||||
"docker.io": {
|
||||
Files: []*containerd.HostsFile{
|
||||
{
|
||||
Name: "hosts.toml",
|
||||
Mode: 0o600,
|
||||
Contents: []byte("server = 'http://127.0.0.1:5001/v2/'\ncapabilities = ['pull', 'resolve']\noverride_path = true\n[host]\n [host.'https://harbor/v2/mirrors/proxy.docker.io']\n capabilities = ['pull', 'resolve']\n override_path = true\n"), //nolint:lll
|
||||
},
|
||||
},
|
||||
},
|
||||
"ghcr.io": {
|
||||
Files: []*containerd.HostsFile{
|
||||
{
|
||||
Name: "hosts.toml",
|
||||
Mode: 0o600,
|
||||
Contents: []byte("server = 'http://127.0.0.1:5002'\ncapabilities = ['pull', 'resolve']\n"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
"github.com/containerd/containerd/v2/core/remotes/docker"
|
||||
"github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/siderolabs/gen/xslices"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/httpdefaults"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/config"
|
||||
@@ -34,15 +35,15 @@ func RegistryHosts(reg config.Registries) docker.RegistryHosts {
|
||||
return func(host string) ([]docker.RegistryHost, error) {
|
||||
var registries []docker.RegistryHost
|
||||
|
||||
endpoints, overridePath, err := RegistryEndpoints(reg, host)
|
||||
endpoints, err := RegistryEndpoints(reg, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
u, err := url.Parse(endpoint)
|
||||
u, err := url.Parse(endpoint.Endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing endpoint %q for host %q: %w", endpoint, host, err)
|
||||
return nil, fmt.Errorf("error parsing endpoint %q for host %q: %w", endpoint.Endpoint, host, err)
|
||||
}
|
||||
|
||||
transport := newTransport()
|
||||
@@ -62,13 +63,13 @@ func RegistryHosts(reg config.Registries) docker.RegistryHosts {
|
||||
}
|
||||
|
||||
if u.Path == "" {
|
||||
if !overridePath {
|
||||
if !endpoint.OverridePath {
|
||||
u.Path = "/v2"
|
||||
}
|
||||
} else {
|
||||
u.Path = path.Clean(u.Path)
|
||||
|
||||
if !strings.HasSuffix(u.Path, "/v2") && !overridePath {
|
||||
if !strings.HasSuffix(u.Path, "/v2") && !endpoint.OverridePath {
|
||||
u.Path += "/v2"
|
||||
}
|
||||
}
|
||||
@@ -97,25 +98,56 @@ func RegistryHosts(reg config.Registries) docker.RegistryHosts {
|
||||
}
|
||||
}
|
||||
|
||||
// EndpointEntry represents a registry endpoint.
|
||||
type EndpointEntry struct {
|
||||
Endpoint string
|
||||
OverridePath bool
|
||||
}
|
||||
|
||||
// RegistryEndpointEntriesFromConfig returns registry endpoints per host.
|
||||
func RegistryEndpointEntriesFromConfig(host string, reg config.RegistryMirrorConfig) ([]EndpointEntry, error) {
|
||||
entries := xslices.Map(reg.Endpoints(), func(endpoint string) EndpointEntry {
|
||||
return EndpointEntry{Endpoint: endpoint, OverridePath: reg.OverridePath()}
|
||||
})
|
||||
|
||||
if reg.SkipFallback() {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
defaultHost, err := docker.DefaultHost(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting default host for %q: %w", host, err)
|
||||
}
|
||||
|
||||
entries = append(entries, EndpointEntry{Endpoint: "https://" + defaultHost, OverridePath: false})
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// RegistryEndpoints returns registry endpoints per host using reg.
|
||||
func RegistryEndpoints(reg config.Registries, host string) (endpoints []string, overridePath bool, err error) {
|
||||
func RegistryEndpoints(reg config.Registries, host string) (endpoints []EndpointEntry, err error) {
|
||||
// direct hit by host
|
||||
if hostConfig, ok := reg.Mirrors()[host]; ok {
|
||||
return hostConfig.Endpoints(), hostConfig.OverridePath(), nil
|
||||
return RegistryEndpointEntriesFromConfig(host, hostConfig)
|
||||
}
|
||||
|
||||
// '*'
|
||||
if catchAllConfig, ok := reg.Mirrors()["*"]; ok {
|
||||
return catchAllConfig.Endpoints(), catchAllConfig.OverridePath(), nil
|
||||
return RegistryEndpointEntriesFromConfig(host, catchAllConfig)
|
||||
}
|
||||
|
||||
// still no endpoints, use default
|
||||
defaultHost, err := docker.DefaultHost(host)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error getting default host for %q: %w", host, err)
|
||||
return nil, fmt.Errorf("error getting default host for %q: %w", host, err)
|
||||
}
|
||||
|
||||
return []string{"https://" + defaultHost}, false, nil
|
||||
return []EndpointEntry{
|
||||
{
|
||||
Endpoint: "https://" + defaultHost,
|
||||
OverridePath: false,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PrepareAuth returns authentication info in the format expected by containerd.
|
||||
|
||||
@@ -55,8 +55,7 @@ func (suite *ResolverSuite) TestRegistryEndpoints() {
|
||||
type request struct {
|
||||
host string
|
||||
|
||||
expectedEndpoints []string
|
||||
expectedOverridePath bool
|
||||
expectedEndpoints []image.EndpointEntry
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
@@ -71,20 +70,29 @@ func (suite *ResolverSuite) TestRegistryEndpoints() {
|
||||
requests: []request{
|
||||
{
|
||||
host: "docker.io",
|
||||
expectedEndpoints: []string{"https://registry-1.docker.io"},
|
||||
},
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
host: "quay.io",
|
||||
expectedEndpoints: []string{"https://quay.io"},
|
||||
Endpoint: "https://registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with mirror",
|
||||
host: "quay.io",
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "https://quay.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with mirror and no fallback",
|
||||
config: &mockConfig{
|
||||
mirrors: map[string]*v1alpha1.RegistryMirrorConfig{
|
||||
"docker.io": {
|
||||
MirrorEndpoints: []string{"http://127.0.0.1:5000", "https://some.host"},
|
||||
MirrorSkipFallback: pointer.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -92,21 +100,101 @@ func (suite *ResolverSuite) TestRegistryEndpoints() {
|
||||
requests: []request{
|
||||
{
|
||||
host: "docker.io",
|
||||
expectedEndpoints: []string{"http://127.0.0.1:5000", "https://some.host"},
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "http://127.0.0.1:5000",
|
||||
},
|
||||
{
|
||||
host: "quay.io",
|
||||
expectedEndpoints: []string{"https://quay.io"},
|
||||
Endpoint: "https://some.host",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with catch-all",
|
||||
host: "quay.io",
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "https://quay.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with mirror and fallback",
|
||||
config: &mockConfig{
|
||||
mirrors: map[string]*v1alpha1.RegistryMirrorConfig{
|
||||
"ghcr.io": {
|
||||
MirrorEndpoints: []string{"http://127.0.0.1:5000", "https://some.host"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
requests: []request{
|
||||
{
|
||||
host: "ghcr.io",
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "http://127.0.0.1:5000",
|
||||
},
|
||||
{
|
||||
Endpoint: "https://some.host",
|
||||
},
|
||||
{
|
||||
Endpoint: "https://ghcr.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
host: "docker.io",
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "https://registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with catch-all and no fallback",
|
||||
config: &mockConfig{
|
||||
mirrors: map[string]*v1alpha1.RegistryMirrorConfig{
|
||||
"docker.io": {
|
||||
MirrorEndpoints: []string{"http://127.0.0.1:5000", "https://some.host"},
|
||||
MirrorSkipFallback: pointer.To(true),
|
||||
},
|
||||
"*": {
|
||||
MirrorEndpoints: []string{"http://127.0.0.1:5001"},
|
||||
MirrorSkipFallback: pointer.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
requests: []request{
|
||||
{
|
||||
host: "docker.io",
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "http://127.0.0.1:5000",
|
||||
},
|
||||
{
|
||||
Endpoint: "https://some.host",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
host: "quay.io",
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "http://127.0.0.1:5001",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with catch-all and fallback",
|
||||
config: &mockConfig{
|
||||
mirrors: map[string]*v1alpha1.RegistryMirrorConfig{
|
||||
"*": {
|
||||
MirrorEndpoints: []string{"http://127.0.0.1:5001"},
|
||||
},
|
||||
@@ -116,11 +204,25 @@ func (suite *ResolverSuite) TestRegistryEndpoints() {
|
||||
requests: []request{
|
||||
{
|
||||
host: "docker.io",
|
||||
expectedEndpoints: []string{"http://127.0.0.1:5000", "https://some.host"},
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "http://127.0.0.1:5001",
|
||||
},
|
||||
{
|
||||
Endpoint: "https://registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
host: "quay.io",
|
||||
expectedEndpoints: []string{"http://127.0.0.1:5001"},
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "http://127.0.0.1:5001",
|
||||
},
|
||||
{
|
||||
Endpoint: "https://quay.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -131,6 +233,7 @@ func (suite *ResolverSuite) TestRegistryEndpoints() {
|
||||
"docker.io": {
|
||||
MirrorEndpoints: []string{"https://harbor/v2/registry.docker.io"},
|
||||
MirrorOverridePath: pointer.To(true),
|
||||
MirrorSkipFallback: pointer.To(true),
|
||||
},
|
||||
"ghcr.io": {
|
||||
MirrorEndpoints: []string{"https://harbor/v2/registry.ghcr.io"},
|
||||
@@ -142,17 +245,32 @@ func (suite *ResolverSuite) TestRegistryEndpoints() {
|
||||
requests: []request{
|
||||
{
|
||||
host: "docker.io",
|
||||
expectedEndpoints: []string{"https://harbor/v2/registry.docker.io"},
|
||||
expectedOverridePath: true,
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "https://harbor/v2/registry.docker.io",
|
||||
OverridePath: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
host: "ghcr.io",
|
||||
expectedEndpoints: []string{"https://harbor/v2/registry.ghcr.io"},
|
||||
expectedOverridePath: true,
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "https://harbor/v2/registry.ghcr.io",
|
||||
OverridePath: true,
|
||||
},
|
||||
{
|
||||
Endpoint: "https://ghcr.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
host: "quay.io",
|
||||
expectedEndpoints: []string{"https://quay.io"},
|
||||
expectedEndpoints: []image.EndpointEntry{
|
||||
{
|
||||
Endpoint: "https://quay.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -160,11 +278,10 @@ func (suite *ResolverSuite) TestRegistryEndpoints() {
|
||||
suite.Run(tt.name, func() {
|
||||
for _, req := range tt.requests {
|
||||
suite.Run(req.host, func() {
|
||||
endpoints, overridePath, err := image.RegistryEndpoints(tt.config, req.host)
|
||||
endpoints, err := image.RegistryEndpoints(tt.config, req.host)
|
||||
|
||||
suite.Assert().NoError(err)
|
||||
suite.Assert().Equal(req.expectedEndpoints, endpoints)
|
||||
suite.Assert().Equal(req.expectedOverridePath, overridePath)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -224,10 +341,12 @@ func (suite *ResolverSuite) TestRegistryHosts() {
|
||||
mirrors: map[string]*v1alpha1.RegistryMirrorConfig{
|
||||
"docker.io": {
|
||||
MirrorEndpoints: []string{"http://127.0.0.1:5000/docker.io", "https://some.host"},
|
||||
MirrorSkipFallback: pointer.To(true),
|
||||
},
|
||||
"ghcr.io": {
|
||||
MirrorEndpoints: []string{"https://harbor/v2/registry.ghcr.io"},
|
||||
MirrorOverridePath: pointer.To(true),
|
||||
MirrorSkipFallback: pointer.To(true),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -255,6 +374,7 @@ func (suite *ResolverSuite) TestRegistryHosts() {
|
||||
mirrors: map[string]*v1alpha1.RegistryMirrorConfig{
|
||||
"docker.io": {
|
||||
MirrorEndpoints: []string{"https://some.host:123"},
|
||||
MirrorSkipFallback: pointer.To(true),
|
||||
},
|
||||
},
|
||||
config: map[string]*v1alpha1.RegistryConfig{
|
||||
|
||||
@@ -366,6 +366,7 @@ type Registries interface {
|
||||
type RegistryMirrorConfig interface {
|
||||
Endpoints() []string
|
||||
OverridePath() bool
|
||||
SkipFallback() bool
|
||||
}
|
||||
|
||||
// RegistryConfig specifies auth & TLS config per registry.
|
||||
|
||||
@@ -3239,6 +3239,13 @@
|
||||
"description": "Use the exact path specified for the endpoint (don’t append /v2/).\nThis setting is often required for setting up multiple mirrors\non a single instance of a registry.\n",
|
||||
"markdownDescription": "Use the exact path specified for the endpoint (don't append /v2/).\nThis setting is often required for setting up multiple mirrors\non a single instance of a registry.",
|
||||
"x-intellij-html-description": "\u003cp\u003eUse the exact path specified for the endpoint (don\u0026rsquo;t append /v2/).\nThis setting is often required for setting up multiple mirrors\non a single instance of a registry.\u003c/p\u003e\n"
|
||||
},
|
||||
"skipFallback": {
|
||||
"type": "boolean",
|
||||
"title": "skipFallback",
|
||||
"description": "Skip fallback to the upstream endpoint, for example the mirror configuration\nfor docker.io will not fallback to registry-1.docker.io.\n",
|
||||
"markdownDescription": "Skip fallback to the upstream endpoint, for example the mirror configuration\nfor `docker.io` will not fallback to `registry-1.docker.io`.",
|
||||
"x-intellij-html-description": "\u003cp\u003eSkip fallback to the upstream endpoint, for example the mirror configuration\nfor \u003ccode\u003edocker.io\u003c/code\u003e will not fallback to \u003ccode\u003eregistry-1.docker.io\u003c/code\u003e.\u003c/p\u003e\n"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1464,6 +1464,11 @@ func (r *RegistryMirrorConfig) OverridePath() bool {
|
||||
return pointer.SafeDeref(r.MirrorOverridePath)
|
||||
}
|
||||
|
||||
// SkipFallback implements the Registries interface.
|
||||
func (r *RegistryMirrorConfig) SkipFallback() bool {
|
||||
return pointer.SafeDeref(r.MirrorSkipFallback)
|
||||
}
|
||||
|
||||
// Content implements the config.Provider interface.
|
||||
func (f *MachineFile) Content() string {
|
||||
return f.FileContent
|
||||
|
||||
@@ -2057,6 +2057,10 @@ type RegistryMirrorConfig struct {
|
||||
// This setting is often required for setting up multiple mirrors
|
||||
// on a single instance of a registry.
|
||||
MirrorOverridePath *bool `yaml:"overridePath,omitempty"`
|
||||
// description: |
|
||||
// Skip fallback to the upstream endpoint, for example the mirror configuration
|
||||
// for `docker.io` will not fallback to `registry-1.docker.io`.
|
||||
MirrorSkipFallback *bool `yaml:"skipFallback,omitempty"`
|
||||
}
|
||||
|
||||
// RegistryConfig specifies auth & TLS config per registry.
|
||||
|
||||
@@ -3224,6 +3224,13 @@ func (RegistryMirrorConfig) Doc() *encoder.Doc {
|
||||
Description: "Use the exact path specified for the endpoint (don't append /v2/).\nThis setting is often required for setting up multiple mirrors\non a single instance of a registry.",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "Use the exact path specified for the endpoint (don't append /v2/)." /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
},
|
||||
{
|
||||
Name: "skipFallback",
|
||||
Type: "bool",
|
||||
Note: "",
|
||||
Description: "Skip fallback to the upstream endpoint, for example the mirror configuration\nfor `docker.io` will not fallback to `registry-1.docker.io`.",
|
||||
Comments: [3]string{"" /* encoder.HeadComment */, "Skip fallback to the upstream endpoint, for example the mirror configuration" /* encoder.LineComment */, "" /* encoder.FootComment */},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2152,6 +2152,7 @@ machine:
|
||||
|-------|------|-------------|----------|
|
||||
|`endpoints` |[]string |<details><summary>List of endpoints (URLs) for registry mirrors to use.</summary>Endpoint configures HTTP/HTTPS access mode, host name,<br />port and path (if path is not set, it defaults to `/v2`).</details> | |
|
||||
|`overridePath` |bool |<details><summary>Use the exact path specified for the endpoint (don't append /v2/).</summary>This setting is often required for setting up multiple mirrors<br />on a single instance of a registry.</details> | |
|
||||
|`skipFallback` |bool |<details><summary>Skip fallback to the upstream endpoint, for example the mirror configuration</summary>for `docker.io` will not fallback to `registry-1.docker.io`.</details> | |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3239,6 +3239,13 @@
|
||||
"description": "Use the exact path specified for the endpoint (don’t append /v2/).\nThis setting is often required for setting up multiple mirrors\non a single instance of a registry.\n",
|
||||
"markdownDescription": "Use the exact path specified for the endpoint (don't append /v2/).\nThis setting is often required for setting up multiple mirrors\non a single instance of a registry.",
|
||||
"x-intellij-html-description": "\u003cp\u003eUse the exact path specified for the endpoint (don\u0026rsquo;t append /v2/).\nThis setting is often required for setting up multiple mirrors\non a single instance of a registry.\u003c/p\u003e\n"
|
||||
},
|
||||
"skipFallback": {
|
||||
"type": "boolean",
|
||||
"title": "skipFallback",
|
||||
"description": "Skip fallback to the upstream endpoint, for example the mirror configuration\nfor docker.io will not fallback to registry-1.docker.io.\n",
|
||||
"markdownDescription": "Skip fallback to the upstream endpoint, for example the mirror configuration\nfor `docker.io` will not fallback to `registry-1.docker.io`.",
|
||||
"x-intellij-html-description": "\u003cp\u003eSkip fallback to the upstream endpoint, for example the mirror configuration\nfor \u003ccode\u003edocker.io\u003c/code\u003e will not fallback to \u003ccode\u003eregistry-1.docker.io\u003c/code\u003e.\u003c/p\u003e\n"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
Reference in New Issue
Block a user