package oci
import (
"fmt"
iofs "io/fs"
"maps"
"net/url"
"os"
"path/filepath"
"testing"
eventtypes "github.com/containerd/containerd/api/events"
"github.com/containerd/typeurl/v2"
"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
)
func TestNewContainerd(t *testing.T) {
t.Parallel()
c, err := NewContainerd("socket", "namespace", "foo", nil)
require.NoError(t, err)
require.Empty(t, c.contentPath)
require.Nil(t, c.client)
require.Equal(t, "foo", c.registryConfigPath)
c, err = NewContainerd("socket", "namespace", "foo", nil, WithContentPath("local"))
require.NoError(t, err)
require.Equal(t, "local", c.contentPath)
}
func TestCanVerifyContainerdConfiguration(t *testing.T) {
t.Parallel()
tests := []struct {
version string
expected bool
}{
{
version: "v2.0.2",
expected: false,
},
{
version: "2.1.4",
expected: false,
},
{
version: "v1.7.27",
expected: true,
},
{
version: "1.6.0",
expected: true,
},
}
for _, tt := range tests {
for _, suffix := range []string{"", "~ds1"} {
version := tt.version + suffix
t.Run(version, func(t *testing.T) {
t.Parallel()
ok, err := canVerifyContainerdConfiguration(tt.version)
require.NoError(t, err)
require.Equal(t, tt.expected, ok)
})
}
}
}
func TestVerifyStatusResponse(t *testing.T) {
t.Parallel()
tests := []struct {
name string
configPath string
requiredConfigPath string
expectedErrMsg string
discardUnpackedLayers bool
}{
{
name: "empty config path",
configPath: "",
requiredConfigPath: "/etc/containerd/certs.d",
expectedErrMsg: "Containerd registry config path needs to be set for mirror configuration to take effect",
},
{
name: "single config path",
configPath: "/etc/containerd/certs.d",
requiredConfigPath: "/etc/containerd/certs.d",
},
{
name: "missing single config path",
configPath: "/etc/containerd/certs.d",
requiredConfigPath: "/var/lib/containerd/certs.d",
expectedErrMsg: "Containerd registry config path is /etc/containerd/certs.d but needs to contain path /var/lib/containerd/certs.d for mirror configuration to take effect",
},
{
name: "multiple config paths",
configPath: "/etc/containerd/certs.d:/etc/docker/certs.d",
requiredConfigPath: "/etc/containerd/certs.d",
},
{
name: "missing multiple config paths",
configPath: "/etc/containerd/certs.d:/etc/docker/certs.d",
requiredConfigPath: "/var/lib/containerd/certs.d",
expectedErrMsg: "Containerd registry config path is /etc/containerd/certs.d:/etc/docker/certs.d but needs to contain path /var/lib/containerd/certs.d for mirror configuration to take effect",
},
{
name: "discard unpacked layers enabled",
discardUnpackedLayers: true,
expectedErrMsg: "Containerd discard unpacked layers cannot be enabled",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp := &runtimeapi.StatusResponse{
Info: map[string]string{
"config": fmt.Sprintf(`{"registry": {"configPath": %q}, "containerd": {"discardUnpackedLayers": %v}}`, tt.configPath, tt.discardUnpackedLayers),
},
}
err := verifyStatusResponse(resp, tt.requiredConfigPath)
if tt.expectedErrMsg != "" {
require.EqualError(t, err, tt.expectedErrMsg)
return
}
require.NoError(t, err)
})
}
}
func TestVerifyStatusResponseMissingRequired(t *testing.T) {
t.Parallel()
tests := []struct {
name string
config string
expectedErrMsg string
}{
{
name: "missing discard upacked layers false",
config: `{"registry": {"configPath": "foo"}, "containerd": {"runtimes":{"discardUnpackedLayers": false}}}`,
expectedErrMsg: "field containerd.discardUnpackedLayers missing from config",
},
{
name: "missing discard upacked layers true",
config: `{"registry": {"configPath": "foo"}, "containerd": {"runtimes":{"discardUnpackedLayers": true}}}`,
expectedErrMsg: "field containerd.discardUnpackedLayers missing from config",
},
{
name: "missing containerd field",
config: `{"registry": {"configPath": "foo"}}`,
expectedErrMsg: "field containerd.discardUnpackedLayers missing from config",
},
{
name: "missing registry field",
config: `{"containerd": {"discardUnpackedLayers": false}}`,
expectedErrMsg: "field registry.configPath missing from config",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp := &runtimeapi.StatusResponse{
Info: map[string]string{
"config": tt.config,
},
}
err := verifyStatusResponse(resp, "foo")
require.EqualError(t, err, tt.expectedErrMsg)
})
}
}
func TestBackupConfig(t *testing.T) {
t.Parallel()
log := logr.Discard()
configPath := t.TempDir()
err := backupConfig(log, configPath)
require.NoError(t, err)
ok, err := dirExists(filepath.Join(configPath, "_backup"))
require.NoError(t, err)
require.True(t, ok)
files, err := os.ReadDir(filepath.Join(configPath, "_backup"))
require.NoError(t, err)
require.Empty(t, files)
configPath = t.TempDir()
err = os.WriteFile(filepath.Join(configPath, "test.txt"), []byte("Hello World"), 0o644)
require.NoError(t, err)
err = backupConfig(log, configPath)
require.NoError(t, err)
ok, err = dirExists(filepath.Join(configPath, "_backup"))
require.NoError(t, err)
require.True(t, ok)
files, err = os.ReadDir(filepath.Join(configPath, "_backup"))
require.NoError(t, err)
require.Len(t, files, 1)
}
func TestCreateFilter(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expectedListFilter string
expectedEventFilter string
registries []string
}{
{
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: "without registry filtering",
registries: []string{},
expectedListFilter: `name~="^.+/"`,
expectedEventFilter: `topic~="/images/create|/images/update|/images/delete",event.name~="^.+/"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
listFilter, eventFilter := createFilters(stringListToUrlList(t, tt.registries))
require.Equal(t, tt.expectedListFilter, listFilter)
require.Equal(t, tt.expectedEventFilter, eventFilter)
})
}
}
func TestGetEventImage(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data any
expectedErr string
expectedName string
expectedEventType EventType
}{
{
name: "type url is nil",
data: nil,
expectedErr: "any cannot be nil",
},
{
name: "unknown event",
data: &eventtypes.ContainerCreate{},
expectedErr: "unsupported event type",
},
{
name: "create event",
data: &eventtypes.ImageCreate{
Name: "create",
},
expectedName: "create",
expectedEventType: CreateEvent,
},
{
name: "update event",
data: &eventtypes.ImageUpdate{
Name: "update",
},
expectedName: "update",
expectedEventType: UpdateEvent,
},
{
name: "delete event",
data: &eventtypes.ImageDelete{
Name: "delete",
},
expectedName: "delete",
expectedEventType: DeleteEvent,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var e typeurl.Any
var err error
if tt.data != nil {
e, err = typeurl.MarshalAny(tt.data)
require.NoError(t, err)
}
name, event, err := getEventImage(e)
if tt.expectedErr != "" {
require.EqualError(t, err, tt.expectedErr)
return
}
require.NoError(t, err)
require.Equal(t, tt.expectedName, name)
require.Equal(t, tt.expectedEventType, event)
})
}
}
func TestMirrorConfiguration(t *testing.T) {
t.Parallel()
tests := []struct {
existingFiles map[string]string
expectedFiles map[string]string
name string
username string
password string
registries []url.URL
mirrors []url.URL
resolveTags bool
createConfigPathDir bool
prependExisting bool
}{
{
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"}),
prependExisting: false,
expectedFiles: map[string]string{
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
[host.'http://127.0.0.2:5000']
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{
"_default/hosts.toml": `[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
},
},
{
name: "resolve tags disabled",
resolveTags: false,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
prependExisting: false,
expectedFiles: map[string]string{
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://127.0.0.1:5000']
capabilities = ['pull']`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull']`,
},
},
{
name: "config path directory does not exist",
resolveTags: true,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: false,
prependExisting: false,
expectedFiles: map[string]string{
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
},
},
{
name: "config path directory does exist",
resolveTags: true,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
prependExisting: false,
expectedFiles: map[string]string{
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
},
},
{
name: "config path directory contains configuration",
resolveTags: true,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
prependExisting: false,
existingFiles: map[string]string{
"docker.io/hosts.toml": "hello = 'world'",
"ghcr.io/hosts.toml": "foo = 'bar'",
},
expectedFiles: map[string]string{
"_backup/docker.io/hosts.toml": "hello = 'world'",
"_backup/ghcr.io/hosts.toml": "foo = 'bar'",
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
},
},
{
name: "config path directory contains backup",
resolveTags: true,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
prependExisting: false,
existingFiles: map[string]string{
"_backup/docker.io/hosts.toml": "hello = 'world'",
"_backup/ghcr.io/hosts.toml": "foo = 'bar'",
"test.txt": "test",
"foo": "bar",
},
expectedFiles: map[string]string{
"_backup/docker.io/hosts.toml": "hello = 'world'",
"_backup/ghcr.io/hosts.toml": "foo = 'bar'",
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
},
},
{
name: "prepend to existing configuration",
resolveTags: true,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
prependExisting: true,
existingFiles: map[string]string{
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://example.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
capabilities = ['pull', 'resolve']
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
},
expectedFiles: map[string]string{
"_backup/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://example.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
capabilities = ['pull', 'resolve']
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
[host.'http://example.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
},
},
{
name: "prepend existing disabled",
resolveTags: true,
registries: stringListToUrlList(t, []string{"https://docker.io", "http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000"}),
createConfigPathDir: true,
prependExisting: false,
existingFiles: map[string]string{
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://example.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
capabilities = ['pull', 'resolve']
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
},
expectedFiles: map[string]string{
"_backup/docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://example.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
[host.'http://example.com:30021']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']
capabilities = ['pull', 'resolve']
[host.'http://bar.com:30020']
capabilities = ['pull', 'resolve']
client = ['/etc/certs/xxx/client.cert', '/etc/certs/xxx/client.key']`,
"docker.io/hosts.toml": `server = 'https://registry-1.docker.io'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']`,
},
},
{
name: "with basic authentication",
resolveTags: true,
registries: stringListToUrlList(t, []string{"http://foo.bar:5000"}),
mirrors: stringListToUrlList(t, []string{"http://127.0.0.1:5000", "http://127.0.0.1:5001"}),
prependExisting: false,
username: "hello",
password: "world",
expectedFiles: map[string]string{
"foo.bar:5000/hosts.toml": `server = 'http://foo.bar:5000'
[host.'http://127.0.0.1:5000']
capabilities = ['pull', 'resolve']
[host.'http://127.0.0.1:5000'.header]
Authorization = 'Basic aGVsbG86d29ybGQ='
[host.'http://127.0.0.1:5001']
capabilities = ['pull', 'resolve']
[host.'http://127.0.0.1:5001'.header]
Authorization = 'Basic aGVsbG86d29ybGQ='`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
registryConfigPath := filepath.Join(t.TempDir(), "etc", "containerd", "certs.d")
if tt.createConfigPathDir {
err := os.MkdirAll(registryConfigPath, 0o755)
require.NoError(t, err)
}
for k, v := range tt.existingFiles {
path := filepath.Join(registryConfigPath, k)
err := os.MkdirAll(filepath.Dir(path), 0o755)
require.NoError(t, err)
err = os.WriteFile(path, []byte(v), 0o644)
require.NoError(t, err)
}
err := AddMirrorConfiguration(t.Context(), registryConfigPath, tt.registries, tt.mirrors, tt.resolveTags, tt.prependExisting, tt.username, tt.password)
require.NoError(t, err)
ok, err := dirExists(filepath.Join(registryConfigPath, "_backup"))
require.NoError(t, err)
require.True(t, ok)
seenExpectedFiles := maps.Clone(tt.expectedFiles)
err = filepath.Walk(registryConfigPath, func(path string, fi iofs.FileInfo, _ error) error {
if fi.IsDir() {
return nil
}
relPath, err := filepath.Rel(registryConfigPath, path)
require.NoError(t, err)
expectedContent, ok := tt.expectedFiles[relPath]
require.True(t, ok)
delete(seenExpectedFiles, relPath)
b, err := os.ReadFile(path)
require.NoError(t, err)
require.Equal(t, expectedContent, string(b))
return nil
})
require.NoError(t, err)
require.Empty(t, seenExpectedFiles)
})
}
}
func TestMirrorConfigurationInvalidMirrorURL(t *testing.T) {
t.Parallel()
configPath := filepath.Join(t.TempDir(), "etc", "containerd", "certs.d")
mirrors := stringListToUrlList(t, []string{"http://127.0.0.1:5000"})
registries := stringListToUrlList(t, []string{"ftp://docker.io"})
err := AddMirrorConfiguration(t.Context(), configPath, registries, mirrors, true, false, "", "")
require.EqualError(t, err, "invalid registry url scheme must be http or https: ftp://docker.io")
registries = stringListToUrlList(t, []string{"https://docker.io/foo/bar"})
err = AddMirrorConfiguration(t.Context(), configPath, registries, mirrors, true, false, "", "")
require.EqualError(t, err, "invalid registry url path has to be empty: https://docker.io/foo/bar")
registries = stringListToUrlList(t, []string{"https://docker.io?foo=bar"})
err = AddMirrorConfiguration(t.Context(), configPath, registries, mirrors, true, false, "", "")
require.EqualError(t, err, "invalid registry url query has to be empty: https://docker.io?foo=bar")
registries = stringListToUrlList(t, []string{"https://foo@docker.io"})
err = AddMirrorConfiguration(t.Context(), configPath, registries, mirrors, true, false, "", "")
require.EqualError(t, err, "invalid registry url user has to be empty: https://foo@docker.io")
}
func TestExistingHosts(t *testing.T) {
t.Parallel()
configPath := t.TempDir()
u, err := url.Parse("https://ghcr.io")
require.NoError(t, err)
eh, err := existingHosts(configPath, *u)
require.NoError(t, err)
require.Empty(t, eh)
tomlHosts := `server = "https://registry-1.docker.io"
[host."https://mirror.registry"]
capabilities = ["pull"]
ca = "/etc/certs/mirror.pem"
skip_verify = false
[host."https://mirror.registry".header]
x-custom-2 = ["value1", "value2"]
[host]
[host."https://mirror-bak.registry/us"]
capabilities = ["pull"]
skip_verify = true
[host."http://mirror.registry"]
capabilities = ["pull"]
[host."https://test-3.registry"]
client = ["/etc/certs/client-1.pem", "/etc/certs/client-2.pem"]
[host."https://test-2.registry".header]
x-custom-2 = ["foo"]
[host."https://test-1.registry"]
capabilities = ["pull", "resolve", "push"]
ca = ["/etc/certs/test-1-ca.pem", "/etc/certs/special.pem"]
client = [["/etc/certs/client.cert", "/etc/certs/client.key"],["/etc/certs/client.pem", ""]]
[host."https://test-2.registry"]
client = "/etc/certs/client.pem"
[host."https://non-compliant-mirror.registry/v2/upstream"]
capabilities = ["pull"]
override_path = true`
err = os.MkdirAll(filepath.Join(configPath, backupDir, u.Host), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(configPath, backupDir, u.Host, "hosts.toml"), []byte(tomlHosts), 0o644)
require.NoError(t, err)
eh, err = existingHosts(configPath, *u)
require.NoError(t, err)
expected := `[host.'https://mirror.registry']
ca = '/etc/certs/mirror.pem'
capabilities = ['pull']
skip_verify = false
[host.'https://mirror.registry'.header]
x-custom-2 = ['value1', 'value2']
[host.'https://mirror-bak.registry/us']
capabilities = ['pull']
skip_verify = true
[host.'http://mirror.registry']
capabilities = ['pull']
[host.'https://test-3.registry']
client = ['/etc/certs/client-1.pem', '/etc/certs/client-2.pem']
[host.'https://test-1.registry']
ca = ['/etc/certs/test-1-ca.pem', '/etc/certs/special.pem']
capabilities = ['pull', 'resolve', 'push']
client = [['/etc/certs/client.cert', '/etc/certs/client.key'], ['/etc/certs/client.pem', '']]
[host.'https://test-2.registry']
client = '/etc/certs/client.pem'
[host.'https://test-2.registry'.header]
x-custom-2 = ['foo']
[host.'https://non-compliant-mirror.registry/v2/upstream']
capabilities = ['pull']
override_path = true`
require.Equal(t, expected, eh)
}
func TestCleanupMirrorConfiguration(t *testing.T) {
t.Parallel()
configPath := filepath.Join(t.TempDir(), "certs.d")
err := os.MkdirAll(filepath.Join(configPath, "_backup"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(configPath, backupDir, "data.txt"), []byte("hello world"), 0o644)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(configPath, "foo.bin"), []byte("hello world"), 0o644)
require.NoError(t, err)
err = os.MkdirAll(filepath.Join(configPath, "docker.io"), 0o755)
require.NoError(t, err)
for range 2 {
err = CleanupMirrorConfiguration(t.Context(), configPath)
require.NoError(t, err)
files, err := os.ReadDir(configPath)
require.NoError(t, err)
require.Len(t, files, 1)
require.Equal(t, "data.txt", files[0].Name())
}
}
func stringListToUrlList(t *testing.T, list []string) []url.URL {
t.Helper()
urls := []url.URL{}
for _, item := range list {
u, err := url.Parse(item)
require.NoError(t, err)
urls = append(urls, *u)
}
return urls
}