Repository: spegel-org/spegel
Pull request #373 (2024-02-29 13:14:53)

Implement support to disable registry filtering in Containerd
Some users may want to mirror all images without specifying a registry. This change modifies the filtering regex to allow for an empty filter registries list. Images will still be filtered however to contain a registry in the reference.

This change would resolve a k3s specific use case for now, but should be supported as an option in Spegel also.
https://github.com/k3s-io/k3s/issues/9590

vflaux: With no registry configured, no mirror is configured in containerd config.
Shouldn't you add a default configuration to mirror all registries to spegel in this case ? There is a special hostname "_default" for this: https://github.com/containerd/containerd/blob/main/docs/hosts.md#registry-host-namespace.
sfackler: @phillebaba Is there anything blocking this from merging?
phillebaba: @sfackler not really, I will rebase and just check my work one more time before merging. Just forgot about this while doing other stuff.
sfackler: Thanks!
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/spegel-org/spegel/pull/373?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spegel-org) Report
All modified and coverable lines are covered by tests :white_check_mark:

| [Files](https://app.codecov.io/gh/spegel-org/spegel/pull/373?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spegel-org) | Coverage Δ | |
|---|---|---|
| [pkg/oci/containerd.go](https://app.codecov.io/gh/spegel-org/spegel/pull/373?src=pr&el=tree&filepath=pkg%2Foci%2Fcontainerd.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spegel-org#diff-cGtnL29jaS9jb250YWluZXJkLmdv) | `59.86% <100.00%> (+0.27%)` | :arrow_up: |


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e1646a..a94244d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - [#747](https://github.com/spegel-org/spegel/pull/747) Update Go to 1.23.6.
 - [#750](https://github.com/spegel-org/spegel/pull/750) Rename append mirrors to prepend existing.
+- [#373](https://github.com/spegel-org/spegel/pull/373) Apply mirror configuration on all registires by default.
 
 ### Deprecated
 
diff --git a/charts/spegel/README.md b/charts/spegel/README.md
index 1054431..9bc5696 100644
--- a/charts/spegel/README.md
+++ b/charts/spegel/README.md
@@ -52,10 +52,10 @@ Read the [getting started](https://spegel.dev/docs/getting-started/) guide to de
 | spegel.containerdRegistryConfigPath | string | `"/etc/containerd/certs.d"` | Path to Containerd mirror configuration. |
 | spegel.containerdSock | string | `"/run/containerd/containerd.sock"` | Path to Containerd socket. |
 | spegel.logLevel | string | `"INFO"` | Minimum log level to output. Value should be DEBUG, INFO, WARN, or ERROR. |
-| spegel.mirrorResolveRetries | int | `3` | Max ammount of mirrors to attempt. |
+| spegel.mirrorResolveRetries | int | `3` | Max amount of mirrors to attempt. |
 | spegel.mirrorResolveTimeout | string | `"20ms"` | Max duration spent finding a mirror. |
+| spegel.mirroredRegistries | list | `[]` | Registries for which mirror configuration will be created. Empty means all registires will be mirrored. |
 | spegel.prependExisting | bool | `false` | When true existing mirror configuration will be kept and Spegel will prepend it's configuration. |
-| spegel.registries | list | `["https://cgr.dev","https://docker.io","https://ghcr.io","https://quay.io","https://mcr.microsoft.com","https://public.ecr.aws","https://gcr.io","https://registry.k8s.io","https://k8s.gcr.io","https://lscr.io"]` | Registries for which mirror configuration will be created. |
 | spegel.resolveLatestTag | bool | `true` | When true latest tags will be resolved to digests. |
 | spegel.resolveTags | bool | `true` | When true Spegel will resolve tags to digests. |
 | tolerations | list | `[{"key":"CriticalAddonsOnly","operator":"Exists"},{"effect":"NoExecute","operator":"Exists"},{"effect":"NoSchedule","operator":"Exists"}]` | Tolerations for pod assignment. |
diff --git a/charts/spegel/templates/daemonset.yaml b/charts/spegel/templates/daemonset.yaml
index be81134..a048099 100644
--- a/charts/spegel/templates/daemonset.yaml
+++ b/charts/spegel/templates/daemonset.yaml
@@ -43,13 +43,13 @@ spec:
           - configuration
           - --log-level={{ .Values.spegel.logLevel }}
           - --containerd-registry-config-path={{ .Values.spegel.containerdRegistryConfigPath }}
-          {{- with .Values.spegel.registries }}
-          - --registries
+          {{- with .Values.spegel.mirroredRegistries }}
+          - --mirrored-registries
           {{- range . }}
           - {{ . | quote }}
           {{- end }}
           {{- end }}
-          - --mirror-registries
+          - --mirror-targets
           - http://$(NODE_IP):{{ .Values.service.registry.hostPort }}
           - http://$(NODE_IP):{{ .Values.service.registry.nodePort }}
           {{- with .Values.spegel.additionalMirrorRegistries }}
@@ -83,7 +83,7 @@ spec:
           - --router-addr=:{{ .Values.service.router.port }}
           - --metrics-addr=:{{ .Values.service.metrics.port }}
           {{- with .Values.spegel.registries }}
-          - --registries
+          - --mirrored-registries
           {{- range . }}
           - {{ . | quote }}
           {{- end }}
diff --git a/charts/spegel/values.yaml b/charts/spegel/values.yaml
index 5392ee7..f0e721c 100644
--- a/charts/spegel/values.yaml
+++ b/charts/spegel/values.yaml
@@ -136,21 +136,13 @@ priorityClassName: system-node-critical
 spegel:
   # -- Minimum log level to output. Value should be DEBUG, INFO, WARN, or ERROR.
   logLevel: "INFO"
-  # -- Registries for which mirror configuration will be created.
-  registries:
-    - https://cgr.dev
-    - https://docker.io
-    - https://ghcr.io
-    - https://quay.io
-    - https://mcr.microsoft.com
-    - https://public.ecr.aws
-    - https://gcr.io
-    - https://registry.k8s.io
-    - https://k8s.gcr.io
-    - https://lscr.io
+  # -- Registries for which mirror configuration will be created. Empty means all registires will be mirrored.
+  mirroredRegistries: []
+    # - https://docker.io
+    # - https://ghcr.io
   # -- Additional target mirror registries other than Spegel.
   additionalMirrorRegistries: []
-  # -- Max ammount of mirrors to attempt.
+  # -- Max amount of mirrors to attempt.
   mirrorResolveRetries: 3
   # -- Max duration spent finding a mirror.
   mirrorResolveTimeout: "20ms"
diff --git a/main.go b/main.go
index 109e56d..e549c8f 100644
--- a/main.go
+++ b/main.go
@@ -31,8 +31,8 @@ import (
 
 type ConfigurationCmd struct {
 	ContainerdRegistryConfigPath string    `arg:"--containerd-registry-config-path,env:CONTAINERD_REGISTRY_CONFIG_PATH" default:"/etc/containerd/certs.d" help:"Directory where mirror configuration is written."`
-	Registries                   []url.URL `arg:"--registries,required,env:REGISTRIES" help:"registries that are configured to be mirrored."`
-	MirrorRegistries             []url.URL `arg:"--mirror-registries,env:MIRROR_REGISTRIES,required" help:"registries that are configured to act as mirrors."`
+	MirroredRegistries           []url.URL `arg:"--mirrored-registries,env:MIRRORED_REGISTRIES" help:"Registries that are configured to be mirrored, if slice is empty all registires are mirrored."`
+	MirrorTargets                []url.URL `arg:"--mirror-targets,env:MIRROR_TARGETS,required" help:"registries that are configured to act as mirrors."`
 	ResolveTags                  bool      `arg:"--resolve-tags,env:RESOLVE_TAGS" default:"true" help:"When true Spegel will resolve tags to digests."`
 	PrependExisting              bool      `arg:"--prepend-existing,env:PREPEND_EXISTING" default:"false" help:"When true existing mirror configuration will be kept and Spegel will prepend it's configuration."`
 }
@@ -57,7 +57,7 @@ type RegistryCmd struct {
 	ContainerdContentPath        string        `arg:"--containerd-content-path,env:CONTAINERD_CONTENT_PATH" default:"/var/lib/containerd/io.containerd.content.v1.content" help:"Path to Containerd content store"`
 	RouterAddr                   string        `arg:"--router-addr,env:ROUTER_ADDR,required" help:"address to serve router."`
 	RegistryAddr                 string        `arg:"--registry-addr,env:REGISTRY_ADDR,required" help:"address to server image registry."`
-	Registries                   []url.URL     `arg:"--registries,env:REGISTRIES,required" help:"registries that are configured to be mirrored."`
+	MirroredRegistries           []url.URL     `arg:"--mirrored-registries,env:MIRRORED_REGISTRIES" help:"Registries that are configured to be mirrored, if slice is empty all registires are mirrored."`
 	MirrorResolveTimeout         time.Duration `arg:"--mirror-resolve-timeout,env:MIRROR_RESOLVE_TIMEOUT" default:"20ms" help:"Max duration spent finding a mirror."`
 	MirrorResolveRetries         int           `arg:"--mirror-resolve-retries,env:MIRROR_RESOLVE_RETRIES" default:"3" help:"Max amount of mirrors to attempt."`
 	ResolveLatestTag             bool          `arg:"--resolve-latest-tag,env:RESOLVE_LATEST_TAG" default:"true" help:"When true latest tags will be resolved to digests."`
@@ -105,7 +105,7 @@ func run(ctx context.Context, args *Arguments) error {
 
 func configurationCommand(ctx context.Context, args *ConfigurationCmd) error {
 	fs := afero.NewOsFs()
-	err := oci.AddMirrorConfiguration(ctx, fs, args.ContainerdRegistryConfigPath, args.Registries, args.MirrorRegistries, args.ResolveTags, args.PrependExisting)
+	err := oci.AddMirrorConfiguration(ctx, fs, args.ContainerdRegistryConfigPath, args.MirroredRegistries, args.MirrorTargets, args.ResolveTags, args.PrependExisting)
 	if err != nil {
 		return err
 	}
@@ -117,7 +117,7 @@ func registryCommand(ctx context.Context, args *RegistryCmd) (err error) {
 	g, ctx := errgroup.WithContext(ctx)
 
 	// OCI Client
-	ociClient, err := oci.NewContainerd(args.ContainerdSock, args.ContainerdNamespace, args.ContainerdRegistryConfigPath, args.Registries, oci.WithContentPath(args.ContainerdContentPath))
+	ociClient, err := oci.NewContainerd(args.ContainerdSock, args.ContainerdNamespace, args.ContainerdRegistryConfigPath, args.MirroredRegistries, oci.WithContentPath(args.ContainerdContentPath))
 	if err != nil {
 		return err
 	}
diff --git a/pkg/oci/containerd.go b/pkg/oci/containerd.go
index a05d340..6dba64f 100644
--- a/pkg/oci/containerd.go
+++ b/pkg/oci/containerd.go
@@ -55,8 +55,8 @@ func WithContentPath(path string) Option {
 	}
 }
 
-func NewContainerd(sock, namespace, registryConfigPath string, registries []url.URL, opts ...Option) (*Containerd, error) {
-	listFilter, eventFilter := createFilters(registries)
+func NewContainerd(sock, namespace, registryConfigPath string, mirroredRegistries []url.URL, opts ...Option) (*Containerd, error) {
+	listFilter, eventFilter := createFilters(mirroredRegistries)
 	c := &Containerd{
 		clientGetter: func() (*client.Client, error) {
 			return client.New(sock, client.WithDefaultNamespace(namespace))
@@ -332,12 +332,17 @@ func getEventImage(e typeurl.Any) (string, EventType, error) {
 	}
 }
 
-func createFilters(registries []url.URL) (string, string) {
+func createFilters(mirroredRegistries []url.URL) (string, string) {
 	registryHosts := []string{}
-	for _, registry := range registries {
+	for _, registry := range mirroredRegistries {
 		registryHosts = append(registryHosts, strings.ReplaceAll(registry.Host, `.`, `\\.`))
 	}
 	listFilter := fmt.Sprintf(`name~="^(%s)/"`, strings.Join(registryHosts, "|"))
+	if len(registryHosts) == 0 {
+		// Filter images that do not have a registry in it's reference,
+		// as we cant mirror images without registries.
+		listFilter = `name~="^.+/"`
+	}
 	eventFilter := fmt.Sprintf(`topic~="/images/create|/images/update|/images/delete",event.%s`, listFilter)
 	return listFilter, eventFilter
 }
@@ -345,9 +350,9 @@ func createFilters(registries []url.URL) (string, string) {
 // Refer to containerd registry configuration documentation for more information about required configuration.
 // https://github.com/containerd/containerd/blob/main/docs/cri/config.md#registry-configuration
 // https://github.com/containerd/containerd/blob/main/docs/hosts.md#registry-configuration---examples
-func AddMirrorConfiguration(ctx context.Context, fs afero.Fs, configPath string, registryURLs, mirrorURLs []url.URL, resolveTags, prependExisting bool) error {
+func AddMirrorConfiguration(ctx context.Context, fs afero.Fs, configPath string, mirroredRegistries, mirrorTargets []url.URL, resolveTags, prependExisting bool) error {
 	log := logr.FromContextOrDiscard(ctx)
-	err := validateRegistries(registryURLs)
+	err := validateRegistries(mirroredRegistries)
 	if err != nil {
 		return err
 	}
@@ -364,27 +369,35 @@ func AddMirrorConfiguration(ctx context.Context, fs afero.Fs, configPath string,
 		return err
 	}
 
-	// Write mirror configuration
 	capabilities := []string{"pull"}
 	if resolveTags {
 		capabilities = append(capabilities, "resolve")
 	}
-	for _, registryURL := range registryURLs {
-		templatedHosts, err := templateHosts(registryURL, mirrorURLs, capabilities)
+	if len(mirroredRegistries) == 0 {
+		mirroredRegistries = append(mirroredRegistries, url.URL{})
+	}
+
+	// Write mirror configuration
+	for _, mirroredRegistry := range mirroredRegistries {
+		templatedHosts, err := templateHosts(mirroredRegistry, mirrorTargets, capabilities)
 		if err != nil {
 			return err
 		}
 		if prependExisting {
-			existingHosts, err := existingHosts(fs, configPath, registryURL)
+			existingHosts, err := existingHosts(fs, configPath, mirroredRegistry)
 			if err != nil {
 				return err
 			}
 			if existingHosts != "" {
 				templatedHosts = templatedHosts + "\n\n" + existingHosts
 			}
-			log.Info("prepending to existing Containerd mirror configuration", "registry", registryURL.String())
+			log.Info("prepending to existing Containerd mirror configuration", "registry", mirroredRegistry.String())
+		}
+		hostComp := mirroredRegistry.Host
+		if hostComp == "" {
+			hostComp = "_default"
 		}
-		fp := path.Join(configPath, registryURL.Host, "hosts.toml")
+		fp := path.Join(configPath, hostComp, "hosts.toml")
 		err = fs.MkdirAll(path.Dir(fp), 0o755)
 		if err != nil {
 			return err
@@ -393,7 +406,7 @@ func AddMirrorConfiguration(ctx context.Context, fs afero.Fs, configPath string,
 		if err != nil {
 			return err
 		}
-		log.Info("added Containerd mirror configuration", "registry", registryURL.String(), "path", fp)
+		log.Info("added Containerd mirror configuration", "registry", mirroredRegistry.String(), "path", fp)
 	}
 	return nil
 }
@@ -464,24 +477,23 @@ func clearConfig(fs afero.Fs, configPath string) error {
 	return nil
 }
 
-func templateHosts(registryURL url.URL, mirrorURLs []url.URL, capabilities []string) (string, error) {
-	server := registryURL.String()
-	if registryURL.String() == "https://docker.io" {
+func templateHosts(mirroredRegistry url.URL, mirrorTargets []url.URL, capabilities []string) (string, error) {
+	server := mirroredRegistry.String()
+	if mirroredRegistry.String() == "https://docker.io" {
 		server = "https://registry-1.docker.io"
 	}
-	capabilitiesStr := strings.Join(capabilities, "', '")
-	capabilitiesStr = fmt.Sprintf("['%s']", capabilitiesStr)
+
 	hc := struct {
-		Server       string
-		Capabilities string
-		MirrorURLs   []url.URL
+		Server        string
+		Capabilities  string
+		MirrorTargets []url.URL
 	}{
-		Server:       server,
-		Capabilities: capabilitiesStr,
-		MirrorURLs:   mirrorURLs,
+		Server:        server,
+		Capabilities:  fmt.Sprintf("['%s']", strings.Join(capabilities, "', '")),
+		MirrorTargets: mirrorTargets,
 	}
-	tmpl, err := template.New("").Parse(`server = '{{ .Server }}'
-{{ range .MirrorURLs }}
+	tmpl, err := template.New("").Parse(`{{- with .Server }}server = '{{ . }}'{{ end }}
+{{ range .MirrorTargets }}
 [host.'{{ .String }}']
 capabilities = {{ $.Capabilities }}
 {{ end }}`)
@@ -500,8 +512,8 @@ type hostFile struct {
 	Hosts map[string]interface{} `toml:"host"`
 }
 
-func existingHosts(fs afero.Fs, configPath string, registryURL url.URL) (string, error) {
-	fp := path.Join(configPath, backupDir, registryURL.Host, "hosts.toml")
+func existingHosts(fs afero.Fs, configPath string, mirroredRegistry url.URL) (string, error) {
+	fp := path.Join(configPath, backupDir, mirroredRegistry.Host, "hosts.toml")
 	b, err := afero.ReadFile(fs, fp)
 	if errors.Is(err, afero.ErrFileNotFound) {
 		return "", nil


Test patch:
diff --git a/pkg/oci/containerd_test.go b/pkg/oci/containerd_test.go
index 3b21baf..47b2b75 100644
--- a/pkg/oci/containerd_test.go
+++ b/pkg/oci/containerd_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	iofs "io/fs"
+	"maps"
 	"net/url"
 	"path/filepath"
 	"testing"
@@ -179,16 +180,16 @@ func TestCreateFilter(t *testing.T) {
 		registries          []string
 	}{
 		{
-			name:                "only registries",
+			name:                "with registry filtering",
 			registries:          []string{"https://docker.io", "https://gcr.io"},
 			expectedListFilter:  `name~="^(docker\\.io|gcr\\.io)/"`,
 			expectedEventFilter: `topic~="/images/create|/images/update|/images/delete",event.name~="^(docker\\.io|gcr\\.io)/"`,
 		},
 		{
-			name:                "additional image filtes",
-			registries:          []string{"https://docker.io", "https://gcr.io"},
-			expectedListFilter:  `name~="^(docker\\.io|gcr\\.io)/"`,
-			expectedEventFilter: `topic~="/images/create|/images/update|/images/delete",event.name~="^(docker\\.io|gcr\\.io)/"`,
+			name:                "without registry filtering",
+			registries:          []string{},
+			expectedListFilter:  `name~="^.+/"`,
+			expectedEventFilter: `topic~="/images/create|/images/update|/images/delete",event.name~="^.+/"`,
 		},
 	}
 	for _, tt := range tests {
@@ -286,7 +287,7 @@ func TestMirrorConfiguration(t *testing.T) {
 		prependExisting     bool
 	}{
 		{
-			name:            "multiple mirros",
+			name:            "multiple mirrors",
 			resolveTags:     true,
 			registries:      stringListToUrlList(t, []string{"http://foo.bar:5000"}),
 			mirrors:         stringListToUrlList(t, []string{"http://127.0.0.1:5000", "http://127.0.0.2:5000", "http://127.0.0.1:5001"}),
@@ -301,6 +302,17 @@ capabilities = ['pull', 'resolve']
 capabilities = ['pull', 'resolve']
 
 [host.'http://127.0.0.1:5001']
+capabilities = ['pull', 'resolve']`,
+			},
+		},
+		{
+			name:            "_default registry mirrors",
+			resolveTags:     true,
+			registries:      stringListToUrlList(t, []string{}),
+			mirrors:         stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
+			prependExisting: false,
+			expectedFiles: map[string]string{
+				"/etc/containerd/certs.d/_default/hosts.toml": `[host.'http://127.0.0.1:5000']
 capabilities = ['pull', 'resolve']`,
 			},
 		},
@@ -530,18 +542,21 @@ capabilities = ['pull', 'resolve']`,
 			ok, err := afero.DirExists(fs, "/etc/containerd/certs.d/_backup")
 			require.NoError(t, err)
 			require.True(t, ok)
+			seenExpectedFiles := maps.Clone(tt.expectedFiles)
 			err = afero.Walk(fs, registryConfigPath, func(path string, fi iofs.FileInfo, _ error) error {
 				if fi.IsDir() {
 					return nil
 				}
 				expectedContent, ok := tt.expectedFiles[path]
 				require.True(t, ok, path)
+				delete(seenExpectedFiles, path)
 				b, err := afero.ReadFile(fs, path)
 				require.NoError(t, err)
 				require.Equal(t, expectedContent, string(b))
 				return nil
 			})
 			require.NoError(t, err)
+			require.Empty(t, seenExpectedFiles)
 		})
 	}
 }