From 30f8b5a9f76441d933341b91d753ae141a9f4d10 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Thu, 14 Nov 2024 18:35:01 +0400 Subject: [PATCH] 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 --- hack/release.toml | 12 ++ .../pkg/containers/cri/containerd/hosts.go | 184 ++++++++++++------ .../containers/cri/containerd/hosts_test.go | 59 +++++- internal/pkg/containers/image/resolver.go | 52 ++++- .../pkg/containers/image/resolver_test.go | 178 ++++++++++++++--- pkg/machinery/config/config/machine.go | 1 + .../config/schemas/config.schema.json | 7 + .../types/v1alpha1/v1alpha1_provider.go | 5 + .../config/types/v1alpha1/v1alpha1_types.go | 4 + .../types/v1alpha1/v1alpha1_types_doc.go | 7 + .../configuration/v1alpha1/config.md | 1 + .../content/v1.9/schemas/config.schema.json | 7 + 12 files changed, 419 insertions(+), 98 deletions(-) diff --git a/hack/release.toml b/hack/release.toml index 5c95cb70a8..43a2a937b4 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -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] diff --git a/internal/pkg/containers/cri/containerd/hosts.go b/internal/pkg/containers/cri/containerd/hosts.go index 7ff48cce1f..7735a493e2 100644 --- a/internal/pkg/containers/cri/containerd/hosts.go +++ b/internal/pkg/containers/cri/containerd/hosts.go @@ -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{ - Capabilities: []string{"pull", "resolve"}, // TODO: we should make it configurable eventually - OverridePath: endpoints.OverridePath(), + 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) - - var tomlBuf bytes.Buffer + configureEndpoint(u.Host, directoryName, &hostEntry.HostToml, directory) - if err := toml.NewEncoder(&tomlBuf).SetIndentTables(true).Encode(hostsToml); err != nil { - return nil, err - } + hostsConfig.HostEntries = append(hostsConfig.HostEntries, hostEntry) + } - 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):] - } - } + if endpoints.SkipFallback() { + hostsConfig.DisableFallback() + } - buf.Write(tomlBytes) + cfgOut, err := hostsConfig.RenderTOML() + if err != nil { + return nil, err } 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 { - Server string `toml:"server,omitempty"` - HostConfigs map[string]*HostToml `toml:"host"` +// 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"` + 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`. diff --git a/internal/pkg/containers/cri/containerd/hosts_test.go b/internal/pkg/containers/cri/containerd/hosts_test.go index cf80f9f4ae..0f54daa683 100644 --- a/internal/pkg/containers/cri/containerd/hosts_test.go +++ b/internal/pkg/containers/cri/containerd/hosts_test.go @@ -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"), }, }, }, diff --git a/internal/pkg/containers/image/resolver.go b/internal/pkg/containers/image/resolver.go index bebdd4e5b2..a27598aa56 100644 --- a/internal/pkg/containers/image/resolver.go +++ b/internal/pkg/containers/image/resolver.go @@ -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. diff --git a/internal/pkg/containers/image/resolver_test.go b/internal/pkg/containers/image/resolver_test.go index 9079b540aa..b0ce1449ef 100644 --- a/internal/pkg/containers/image/resolver_test.go +++ b/internal/pkg/containers/image/resolver_test.go @@ -55,8 +55,7 @@ func (suite *ResolverSuite) TestRegistryEndpoints() { type request struct { host string - expectedEndpoints []string - expectedOverridePath bool + expectedEndpoints []image.EndpointEntry } for _, tt := range []struct { @@ -70,20 +69,61 @@ func (suite *ResolverSuite) TestRegistryEndpoints() { config: &mockConfig{}, requests: []request{ { - host: "docker.io", - expectedEndpoints: []string{"https://registry-1.docker.io"}, + host: "docker.io", + expectedEndpoints: []image.EndpointEntry{ + { + Endpoint: "https://registry-1.docker.io", + }, + }, }, { - host: "quay.io", - expectedEndpoints: []string{"https://quay.io"}, + host: "quay.io", + expectedEndpoints: []image.EndpointEntry{ + { + Endpoint: "https://quay.io", + }, + }, }, }, }, { - name: "config with mirror", + 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), + }, + }, + }, + + 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: "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"}, }, }, @@ -91,22 +131,70 @@ func (suite *ResolverSuite) TestRegistryEndpoints() { requests: []request{ { - host: "docker.io", - expectedEndpoints: []string{"http://127.0.0.1:5000", "https://some.host"}, + host: "ghcr.io", + expectedEndpoints: []image.EndpointEntry{ + { + Endpoint: "http://127.0.0.1:5000", + }, + { + Endpoint: "https://some.host", + }, + { + Endpoint: "https://ghcr.io", + }, + }, }, { - host: "quay.io", - expectedEndpoints: []string{"https://quay.io"}, + host: "docker.io", + expectedEndpoints: []image.EndpointEntry{ + { + Endpoint: "https://registry-1.docker.io", + }, + }, }, }, }, { - name: "config with catch-all", + 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"}, + 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"}, }, @@ -115,12 +203,26 @@ func (suite *ResolverSuite) TestRegistryEndpoints() { requests: []request{ { - host: "docker.io", - expectedEndpoints: []string{"http://127.0.0.1:5000", "https://some.host"}, + host: "docker.io", + 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"}, + host: "quay.io", + 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"}, @@ -141,18 +244,33 @@ func (suite *ResolverSuite) TestRegistryEndpoints() { requests: []request{ { - host: "docker.io", - expectedEndpoints: []string{"https://harbor/v2/registry.docker.io"}, - expectedOverridePath: true, + host: "docker.io", + 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, + host: "ghcr.io", + expectedEndpoints: []image.EndpointEntry{ + { + Endpoint: "https://harbor/v2/registry.ghcr.io", + OverridePath: true, + }, + { + Endpoint: "https://ghcr.io", + }, + }, }, { - host: "quay.io", - expectedEndpoints: []string{"https://quay.io"}, + host: "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) }) } }) @@ -223,11 +340,13 @@ func (suite *ResolverSuite) TestRegistryHosts() { cfg := &mockConfig{ mirrors: map[string]*v1alpha1.RegistryMirrorConfig{ "docker.io": { - MirrorEndpoints: []string{"http://127.0.0.1:5000/docker.io", "https://some.host"}, + 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), }, }, } @@ -254,7 +373,8 @@ func (suite *ResolverSuite) TestRegistryHosts() { cfg = &mockConfig{ mirrors: map[string]*v1alpha1.RegistryMirrorConfig{ "docker.io": { - MirrorEndpoints: []string{"https://some.host:123"}, + MirrorEndpoints: []string{"https://some.host:123"}, + MirrorSkipFallback: pointer.To(true), }, }, config: map[string]*v1alpha1.RegistryConfig{ diff --git a/pkg/machinery/config/config/machine.go b/pkg/machinery/config/config/machine.go index 7207de959f..6ce8e4171b 100644 --- a/pkg/machinery/config/config/machine.go +++ b/pkg/machinery/config/config/machine.go @@ -366,6 +366,7 @@ type Registries interface { type RegistryMirrorConfig interface { Endpoints() []string OverridePath() bool + SkipFallback() bool } // RegistryConfig specifies auth & TLS config per registry. diff --git a/pkg/machinery/config/schemas/config.schema.json b/pkg/machinery/config/schemas/config.schema.json index 97c0c0a11e..58a96acefc 100644 --- a/pkg/machinery/config/schemas/config.schema.json +++ b/pkg/machinery/config/schemas/config.schema.json @@ -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, diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go index 860340d8f8..7b48839c6c 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go @@ -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 diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go index 9d07311d09..981719c25e 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go @@ -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. diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go index 42e1385e00..fae7e1d605 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go @@ -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 */}, + }, }, } diff --git a/website/content/v1.9/reference/configuration/v1alpha1/config.md b/website/content/v1.9/reference/configuration/v1alpha1/config.md index c54a8e278c..eb4e1bc2c7 100644 --- a/website/content/v1.9/reference/configuration/v1alpha1/config.md +++ b/website/content/v1.9/reference/configuration/v1alpha1/config.md @@ -2152,6 +2152,7 @@ machine: |-------|------|-------------|----------| |`endpoints` |[]string |
List of endpoints (URLs) for registry mirrors to use.Endpoint configures HTTP/HTTPS access mode, host name,
port and path (if path is not set, it defaults to `/v2`).
| | |`overridePath` |bool |
Use the exact path specified for the endpoint (don't append /v2/).This setting is often required for setting up multiple mirrors
on a single instance of a registry.
| | +|`skipFallback` |bool |
Skip fallback to the upstream endpoint, for example the mirror configurationfor `docker.io` will not fallback to `registry-1.docker.io`.
| | diff --git a/website/content/v1.9/schemas/config.schema.json b/website/content/v1.9/schemas/config.schema.json index 97c0c0a11e..58a96acefc 100644 --- a/website/content/v1.9/schemas/config.schema.json +++ b/website/content/v1.9/schemas/config.schema.json @@ -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,