Skip to content

Commit

Permalink
Implement support to disable registry filtering
Browse files Browse the repository at this point in the history
Signed-off-by: Philip Laine <[email protected]>
  • Loading branch information
phillebaba committed Feb 25, 2025
1 parent af3d364 commit d67cda3
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 54 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions charts/spegel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
8 changes: 4 additions & 4 deletions charts/spegel/templates/daemonset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 }}
Expand Down
18 changes: 5 additions & 13 deletions charts/spegel/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 5 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,required" 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."`
}
Expand All @@ -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,required" 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."`
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
53 changes: 29 additions & 24 deletions pkg/oci/containerd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -332,22 +332,27 @@ 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
}

// 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
}
Expand All @@ -369,22 +374,22 @@ func AddMirrorConfiguration(ctx context.Context, fs afero.Fs, configPath string,
if resolveTags {
capabilities = append(capabilities, "resolve")
}
for _, registryURL := range registryURLs {
templatedHosts, err := templateHosts(registryURL, mirrorURLs, capabilities)
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())
}
fp := path.Join(configPath, registryURL.Host, "hosts.toml")
fp := path.Join(configPath, mirroredRegistry.Host, "hosts.toml")
err = fs.MkdirAll(path.Dir(fp), 0o755)
if err != nil {
return err
Expand All @@ -393,7 +398,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
}
Expand Down Expand Up @@ -464,24 +469,24 @@ 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: capabilitiesStr,
MirrorTargets: mirrorTargets,
}
tmpl, err := template.New("").Parse(`server = '{{ .Server }}'
{{ range .MirrorURLs }}
{{ range .MirrorTargets }}
[host.'{{ .String }}']
capabilities = {{ $.Capabilities }}
{{ end }}`)
Expand All @@ -500,8 +505,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
Expand Down
27 changes: 21 additions & 6 deletions pkg/oci/containerd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
iofs "io/fs"
"maps"
"net/url"
"path/filepath"
"testing"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"}),
Expand All @@ -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']`,
},
},
Expand Down Expand Up @@ -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)
})
}
}
Expand Down

0 comments on commit d67cda3

Please sign in to comment.