pkg/containerd/hosts.go (126 lines of code) (raw):
// Initial Copyright (c) 2023 Xenit AB and 2024 The Spegel Authors.
// Portions Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package containerd
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path"
"github.com/pelletier/go-toml/v2"
"github.com/rs/zerolog"
"github.com/spf13/afero"
)
const (
backupDir = "_backup"
)
type hostFile struct {
Server string `toml:"server"`
HostConfigs map[string]hostConfig `toml:"host"`
}
type hostConfig struct {
Capabilities []string `toml:"capabilities"`
SkipVerify bool `toml:"skip_verify"`
}
// AddHostsConfiguration adds mirror configuration to containerd for the specified URLs.
// Refer to containerd registry configuration documentation for mor 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 AddHostsConfiguration(ctx context.Context, fs afero.Fs, configPath string, registryURLs, mirrorURLs []url.URL, resolveTags bool) error {
log := zerolog.Ctx(ctx).With().Str("component", "containerd-mirror").Logger()
if err := validate(registryURLs); err != nil {
return err
}
// Create config path dir if it does not exist
ok, err := afero.DirExists(fs, configPath)
if err != nil {
return err
}
if !ok {
err := fs.MkdirAll(configPath, 0755)
if err != nil {
return err
}
}
// Backup files and directories in config path
backupDirPath := path.Join(configPath, backupDir)
if _, err := fs.Stat(backupDirPath); os.IsNotExist(err) {
files, err := afero.ReadDir(fs, configPath)
if err != nil {
return err
}
if len(files) > 0 {
err = fs.MkdirAll(backupDirPath, 0755)
if err != nil {
return err
}
for _, fi := range files {
oldPath := path.Join(configPath, fi.Name())
newPath := path.Join(backupDirPath, fi.Name())
err := fs.Rename(oldPath, newPath)
if err != nil {
return err
}
log.Info().Str("path", oldPath).Str("target", newPath).Msg("backing up Containerd host configuration")
}
}
}
// Remove all content from config path to start from a clean slate
files, err := afero.ReadDir(fs, configPath)
if err != nil {
return err
}
for _, fi := range files {
if fi.Name() == backupDir {
continue
}
filePath := path.Join(configPath, fi.Name())
err := fs.RemoveAll(filePath)
if err != nil {
return err
}
}
// Write mirror configuration
capabilities := []string{"pull"}
if resolveTags {
capabilities = append(capabilities, "resolve")
}
for _, registryURL := range registryURLs {
// Need a special case for Docker Hub as docker.io is just an alias.
server := registryURL.String()
if registryURL.String() == "https://docker.io" {
server = "https://registry-1.docker.io"
}
hostConfigs := map[string]hostConfig{}
for _, u := range mirrorURLs {
hostConfigs[u.String()] = hostConfig{Capabilities: capabilities, SkipVerify: true} // nolint: gosec. TODO avtakkar: configure TLS.
}
cfg := hostFile{
Server: server,
HostConfigs: hostConfigs,
}
b, err := toml.Marshal(&cfg)
if err != nil {
return err
}
fp := path.Join(configPath, registryURL.Host, "hosts.toml")
err = fs.MkdirAll(path.Dir(fp), 0755)
if err != nil {
return err
}
err = afero.WriteFile(fs, fp, b, 0644)
if err != nil {
return err
}
log.Info().Str("host", registryURL.String()).Str("path", fp).Msg("added containerd mirror configuration")
}
return nil
}
// validate validates registry URLs.
func validate(urls []url.URL) error {
errs := []error{}
for _, u := range urls {
if u.Scheme != "http" && u.Scheme != "https" {
errs = append(errs, fmt.Errorf("invalid registry url, scheme must be http or https, got: %s", u.String()))
}
if u.Path != "" {
errs = append(errs, fmt.Errorf("invalid registry url, path has to be empty, got: %s", u.String()))
}
if len(u.Query()) != 0 {
errs = append(errs, fmt.Errorf("invalid registry url, query has to be empty, got: %s", u.String()))
}
if u.User != nil {
errs = append(errs, fmt.Errorf("invalid registry url, user has to be empty, got: %s", u.String()))
}
}
return errors.Join(errs...)
}