diff --git a/cmd/osctl/cmd/cluster.go b/cmd/osctl/cmd/cluster.go index 3a8674862..7274310ac 100644 --- a/cmd/osctl/cmd/cluster.go +++ b/cmd/osctl/cmd/cluster.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "sort" + "strings" "text/tabwriter" "time" @@ -37,6 +38,7 @@ var ( clusterName string nodeImage string nodeInstallImage string + registryMirrors []string nodeVmlinuxPath string nodeInitramfsPath string bootloaderEmulation bool @@ -196,6 +198,15 @@ func create(ctx context.Context) (err error) { generate.WithInstallImage(nodeInstallImage), } + for _, registryMirror := range registryMirrors { + components := strings.SplitN(registryMirror, "=", 2) + if len(components) != 2 { + return fmt.Errorf("invalid registry mirror spec: %q", registryMirror) + } + + genOptions = append(genOptions, generate.WithRegistryMirror(components[0], components[1])) + } + genOptions = append(genOptions, provisioner.GenOptions(request.Network)...) defaultInternalLB, defaultExternalLB := provisioner.GetLoadBalancers(request.Network) @@ -418,6 +429,7 @@ func init() { clusterUpCmd.Flags().StringVar(&nodeVmlinuxPath, "vmlinux-path", helpers.ArtifactPath(constants.KernelUncompressedAsset), "the uncompressed kernel image to use") clusterUpCmd.Flags().StringVar(&nodeInitramfsPath, "initrd-path", helpers.ArtifactPath(constants.InitramfsAsset), "the uncompressed kernel image to use") clusterUpCmd.Flags().BoolVar(&bootloaderEmulation, "with-bootloader-emulation", false, "enable bootloader emulation to load kernel and initramfs from disk image") + clusterUpCmd.Flags().StringSliceVar(®istryMirrors, "registry-mirror", []string{}, "list of registry mirrors to use in format: =") clusterUpCmd.Flags().IntVar(&networkMTU, "mtu", 1500, "MTU of the docker bridge network") clusterUpCmd.Flags().StringVar(&networkCIDR, "cidr", "10.5.0.0/24", "CIDR of the docker bridge network") clusterUpCmd.Flags().StringSliceVar(&nameservers, "nameservers", []string{"8.8.8.8", "1.1.1.1"}, "list of nameservers to use (VM only)") diff --git a/cmd/osctl/cmd/config.go b/cmd/osctl/cmd/config.go index 4c4f7f869..cc55d6f7b 100644 --- a/cmd/osctl/cmd/config.go +++ b/cmd/osctl/cmd/config.go @@ -201,17 +201,28 @@ func genV1Alpha1Config(args []string) error { return fmt.Errorf("failed to create output dir: %w", err) } + var genOptions []generate.GenOption //nolint: prealloc + + for _, registryMirror := range registryMirrors { + components := strings.SplitN(registryMirror, "=", 2) + if len(components) != 2 { + return fmt.Errorf("invalid registry mirror spec: %q", registryMirror) + } + + genOptions = append(genOptions, generate.WithRegistryMirror(components[0], components[1])) + } + configBundle, err := config.NewConfigBundle( config.WithInputOptions( &config.InputOptions{ ClusterName: args[0], Endpoint: args[1], KubeVersion: kubernetesVersion, - GenOptions: []generate.GenOption{ + GenOptions: append(genOptions, generate.WithInstallDisk(installDisk), generate.WithInstallImage(installImage), generate.WithAdditionalSubjectAltNames(additionalSANs), - }, + ), }, ), ) @@ -280,6 +291,7 @@ func init() { configGenerateCmd.Flags().StringVar(&configVersion, "version", "v1alpha1", "the desired machine config version to generate") configGenerateCmd.Flags().StringVar(&kubernetesVersion, "kubernetes-version", constants.DefaultKubernetesVersion, "desired kubernetes version to run") configGenerateCmd.Flags().StringVarP(&outputDir, "output-dir", "o", "", "destination to output generated files") + configGenerateCmd.Flags().StringSliceVar(®istryMirrors, "registry-mirror", []string{}, "list of registry mirrors to use in format: =") helpers.Should(configAddCmd.MarkFlagRequired("ca")) helpers.Should(configAddCmd.MarkFlagRequired("crt")) helpers.Should(configAddCmd.MarkFlagRequired("key")) diff --git a/docs/osctl/osctl_cluster_create.md b/docs/osctl/osctl_cluster_create.md index e2fd93773..0ae7f7eb1 100644 --- a/docs/osctl/osctl_cluster_create.md +++ b/docs/osctl/osctl_cluster_create.md @@ -32,6 +32,7 @@ osctl cluster create [flags] --memory int the limit on memory usage in MB (each container) (default 1024) --mtu int MTU of the docker bridge network (default 1500) --nameservers strings list of nameservers to use (VM only) (default [8.8.8.8,1.1.1.1]) + --registry-mirror strings list of registry mirrors to use in format: = --vmlinux-path string the uncompressed kernel image to use (default "_out/vmlinux") --wait wait for the cluster to be ready before returning --wait-timeout duration timeout to wait for the cluster to be ready (default 20m0s) diff --git a/docs/osctl/osctl_config_generate.md b/docs/osctl/osctl_config_generate.md index c7d8d8676..1aee37fe1 100644 --- a/docs/osctl/osctl_config_generate.md +++ b/docs/osctl/osctl_config_generate.md @@ -20,6 +20,7 @@ osctl config generate https:// [fla --install-image string the image used to perform an installation (default "docker.io/autonomy/installer:latest") --kubernetes-version string desired kubernetes version to run (default "1.17.1") -o, --output-dir string destination to output generated files + --registry-mirror strings list of registry mirrors to use in format: = --version string the desired machine config version to generate (default "v1alpha1") ``` diff --git a/docs/website/content/v0.3/en/configuration/v1alpha1.md b/docs/website/content/v0.3/en/configuration/v1alpha1.md index 3bb68631e..d7529bd5c 100644 --- a/docs/website/content/v0.3/en/configuration/v1alpha1.md +++ b/docs/website/content/v0.3/en/configuration/v1alpha1.md @@ -331,6 +331,49 @@ sysctls: ``` +#### registries + +Used to configure the machine's container image registry mirrors. + +Automatically generates matching CRI configuration for registry mirrors. + +Section `mirrors` allows to redirect requests for images to non-default registry, +which might be local registry or caching mirror. + +Section `config` provides a way to authenticate to the registry with TLS client +identity, provide registry CA, or authentication information. +Authentication information has same meaning with the corresponding field in `.docker/config.json`. + +See also matching configuration for [CRI containerd plugin](https://github.com/containerd/cri/blob/master/docs/registry.md). + +Type: `RegistriesConfig` + +Examples: + +```yaml +registries: + mirrors: + docker.io: + endpoints: + - https://registry-1.docker.io + '*': + endpoints: + - http://some.host:123/ + config: + "some.host:123": + tls: + CA: ... # base64-encoded CA certificate in PEM format + clientIdentity: + cert: ... # base64-encoded client certificate in PEM format + key: ... # base64-encoded client key in PEM format + auth: + username: ... + password: ... + auth: ... + identityToken: ... + +``` + --- ### ClusterConfig @@ -739,6 +782,32 @@ Type: `array` --- +### RegistriesConfig + +#### mirrors + +Specifies mirror configuration for each registry. +This setting allows to use local pull-through caching registires, +air-gapped installations, etc. + +Registry name is the first segment of image identifier, with 'docker.io' +being default one. +Name '*' catches any registry names not specified explicitly. + +Type: `map` + +#### config + +Specifies TLS & auth configuration for HTTPS image registries. +Mutual TLS can be enabled with 'clientIdentity' option. + +TLS configuration can be skipped if registry has trusted +server certificate. + +Type: `map` + +--- + ### PodCheckpointer #### image diff --git a/go.mod b/go.mod index 251061316..5738cc2b3 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ replace ( require ( code.cloudfoundry.org/bytefmt v0.0.0-20180906201452-2aa6f33b730c + github.com/BurntSushi/toml v0.3.1 github.com/Microsoft/hcsshim v0.8.7 // indirect github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e github.com/beevik/ntp v0.2.0 diff --git a/internal/app/machined/internal/api/reg/reg.go b/internal/app/machined/internal/api/reg/reg.go index becf08353..a1a4fcd63 100644 --- a/internal/app/machined/internal/api/reg/reg.go +++ b/internal/app/machined/internal/api/reg/reg.go @@ -42,6 +42,7 @@ import ( "github.com/talos-systems/talos/pkg/chunker" filechunker "github.com/talos-systems/talos/pkg/chunker/file" "github.com/talos-systems/talos/pkg/chunker/stream" + machinecfg "github.com/talos-systems/talos/pkg/config/machine" "github.com/talos-systems/talos/pkg/constants" "github.com/talos-systems/talos/pkg/version" ) @@ -105,7 +106,7 @@ func (r *Registrator) Shutdown(ctx context.Context, in *empty.Empty) (reply *mac // Upgrade initiates an upgrade. func (r *Registrator) Upgrade(ctx context.Context, in *machineapi.UpgradeRequest) (data *machineapi.UpgradeResponse, err error) { - if err = pullAndValidateInstallerImage(ctx, in.GetImage()); err != nil { + if err = pullAndValidateInstallerImage(ctx, r.config.Machine().Registries(), in.GetImage()); err != nil { return nil, err } @@ -574,7 +575,7 @@ func (r *Registrator) Read(in *machineapi.ReadRequest, srv machineapi.MachineSer } } -func pullAndValidateInstallerImage(ctx context.Context, ref string) error { +func pullAndValidateInstallerImage(ctx context.Context, config machinecfg.Registries, ref string) error { // Pull down specified installer image early so we can bail if it doesn't exist in the upstream registry containerdctx := namespaces.WithNamespace(ctx, constants.SystemContainerdNamespace) @@ -583,7 +584,7 @@ func pullAndValidateInstallerImage(ctx context.Context, ref string) error { return err } - img, err := image.Pull(containerdctx, client, ref) + img, err := image.Pull(containerdctx, config, client, ref) if err != nil { return err } diff --git a/internal/app/machined/internal/phase/config/extra_files.go b/internal/app/machined/internal/phase/config/extra_files.go index 2ae8cadf0..0f7a0c1c3 100644 --- a/internal/app/machined/internal/phase/config/extra_files.go +++ b/internal/app/machined/internal/phase/config/extra_files.go @@ -35,7 +35,12 @@ func (task *ExtraFiles) TaskFunc(mode runtime.Mode) phase.TaskFunc { func (task *ExtraFiles) runtime(r runtime.Runtime) (err error) { var result *multierror.Error - for _, f := range r.Config().Machine().Files() { + files, err := r.Config().Machine().Files() + if err != nil { + return fmt.Errorf("error generating extra files: %w", err) + } + + for _, f := range files { content := f.Content switch f.Op { diff --git a/internal/app/machined/pkg/system/services/etcd.go b/internal/app/machined/pkg/system/services/etcd.go index 268152c9b..1288ab274 100644 --- a/internal/app/machined/pkg/system/services/etcd.go +++ b/internal/app/machined/pkg/system/services/etcd.go @@ -69,7 +69,7 @@ func (e *Etcd) PreFunc(ctx context.Context, config runtime.Configurator) (err er // Pull the image and unpack it. containerdctx := namespaces.WithNamespace(ctx, constants.SystemContainerdNamespace) - if _, err = image.Pull(containerdctx, client, config.Cluster().Etcd().Image()); err != nil { + if _, err = image.Pull(containerdctx, config.Machine().Registries(), client, config.Cluster().Etcd().Image()); err != nil { return fmt.Errorf("failed to pull image %q: %w", config.Cluster().Etcd().Image(), err) } diff --git a/internal/app/machined/pkg/system/services/kubelet.go b/internal/app/machined/pkg/system/services/kubelet.go index 38926455c..2f6dc9d39 100644 --- a/internal/app/machined/pkg/system/services/kubelet.go +++ b/internal/app/machined/pkg/system/services/kubelet.go @@ -114,7 +114,7 @@ func (k *Kubelet) PreFunc(ctx context.Context, config runtime.Configurator) erro // Pull the image and unpack it. containerdctx := namespaces.WithNamespace(ctx, "k8s.io") - _, err = image.Pull(containerdctx, client, config.Machine().Kubelet().Image()) + _, err = image.Pull(containerdctx, config.Machine().Registries(), client, config.Machine().Kubelet().Image()) if err != nil { return err } diff --git a/internal/pkg/containers/cri/containerd/config.go b/internal/pkg/containers/cri/containerd/config.go new file mode 100644 index 000000000..1d30a0a10 --- /dev/null +++ b/internal/pkg/containers/cri/containerd/config.go @@ -0,0 +1,153 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package containerd + +import ( + "bytes" + "fmt" + "path/filepath" + + "github.com/BurntSushi/toml" + + "github.com/talos-systems/talos/pkg/config/machine" + "github.com/talos-systems/talos/pkg/constants" +) + +// config structures to generate TOML containerd CRI plugin config +type mirror struct { + Endpoints []string `toml:"endpoint"` +} + +type authConfig struct { + Username string `toml:"username"` + Password string `toml:"password"` + Auth string `toml:"auth"` + IdentityToken string `toml:"identitytoken"` +} + +type tlsConfig struct { + InsecureSkipVerify bool `toml:"insecure_skip_verify"` + CAFile string `toml:"ca_file"` + CertFile string `toml:"cert_file"` + KeyFile string `toml:"key_file"` +} + +type registryConfig struct { + Auth *authConfig `toml:"auth"` + TLS *tlsConfig `toml:"tls"` +} + +type registry struct { + Mirrors map[string]mirror `toml:"mirrors"` + Configs map[string]registryConfig `toml:"configs"` +} + +type criConfig struct { + Registry registry `toml:"registry"` +} + +type pluginsConfig struct { + CRI criConfig `toml:"cri"` +} + +type containerdConfig struct { + Plugins pluginsConfig `toml:"plugins"` +} + +// GenerateRegistriesConfig for containerd CRI plugin (TOML format). +// +//nolint: gocyclo +func GenerateRegistriesConfig(input machine.Registries) ([]machine.File, error) { + caPath := filepath.Join(filepath.Dir(constants.CRIContainerdConfig), "ca") + clientPath := filepath.Join(filepath.Dir(constants.CRIContainerdConfig), "client") + + var config containerdConfig + config.Plugins.CRI.Registry.Mirrors = make(map[string]mirror) + config.Plugins.CRI.Registry.Configs = make(map[string]registryConfig) + + for mirrorName, mirrorConfig := range input.Mirrors() { + config.Plugins.CRI.Registry.Mirrors[mirrorName] = mirror{Endpoints: mirrorConfig.Endpoints} + } + + var extraFiles []machine.File + + for registryHost, hostConfig := range input.Config() { + cfg := registryConfig{} + + if hostConfig.Auth != nil { + cfg.Auth = &authConfig{ + Username: hostConfig.Auth.Username, + Password: hostConfig.Auth.Password, + Auth: hostConfig.Auth.Auth, + IdentityToken: hostConfig.Auth.IdentityToken, + } + } + + if hostConfig.TLS != nil { + cfg.TLS = &tlsConfig{ + InsecureSkipVerify: hostConfig.TLS.InsecureSkipVerify, + } + + if hostConfig.TLS.CA != nil { + path := filepath.Join(caPath, fmt.Sprintf("%s.crt", registryHost)) + + extraFiles = append(extraFiles, machine.File{ + Content: string(hostConfig.TLS.CA), + Permissions: 0600, + Path: path, + Op: "create", + }) + + cfg.TLS.CAFile = path + } + + if hostConfig.TLS.ClientIdentity.Crt != nil { + path := filepath.Join(clientPath, fmt.Sprintf("%s.crt", registryHost)) + + extraFiles = append(extraFiles, machine.File{ + Content: string(hostConfig.TLS.ClientIdentity.Crt), + Permissions: 0600, + Path: path, + Op: "create", + }) + + cfg.TLS.CertFile = path + } + + if hostConfig.TLS.ClientIdentity.Key != nil { + path := filepath.Join(clientPath, fmt.Sprintf("%s.key", registryHost)) + + extraFiles = append(extraFiles, machine.File{ + Content: string(hostConfig.TLS.ClientIdentity.Key), + Permissions: 0600, + Path: path, + Op: "create", + }) + + cfg.TLS.KeyFile = path + } + } + + if cfg.Auth != nil || cfg.TLS != nil { + config.Plugins.CRI.Registry.Configs[registryHost] = cfg + } + } + + var buf bytes.Buffer + + if err := toml.NewEncoder(&buf).Encode(&config); err != nil { + return nil, err + } + + // CRI plugin doesn't support merging configs for plugins across files, + // so we have to append CRI plugin to the main config, as it already contains + // configuration pieces for CRI plugin + return append(extraFiles, machine.File{ + Content: buf.String(), + Permissions: 0644, + Path: constants.CRIContainerdConfig, + Op: "append", + }), nil +} diff --git a/internal/pkg/containers/cri/containerd/config_test.go b/internal/pkg/containers/cri/containerd/config_test.go new file mode 100644 index 000000000..69b4aafb1 --- /dev/null +++ b/internal/pkg/containers/cri/containerd/config_test.go @@ -0,0 +1,117 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package containerd_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/talos-systems/talos/internal/pkg/containers/cri/containerd" + "github.com/talos-systems/talos/pkg/config/machine" + "github.com/talos-systems/talos/pkg/constants" + "github.com/talos-systems/talos/pkg/crypto/x509" +) + +type mockConfig struct { + mirrors map[string]machine.RegistryMirrorConfig + config map[string]machine.RegistryConfig +} + +func (c *mockConfig) Mirrors() map[string]machine.RegistryMirrorConfig { + return c.mirrors +} + +func (c *mockConfig) Config() map[string]machine.RegistryConfig { + return c.config +} + +func (c *mockConfig) ExtraFiles() ([]machine.File, error) { + return nil, fmt.Errorf("not implemented") +} + +type ConfigSuite struct { + suite.Suite +} + +func (suite *ConfigSuite) TestGenerateRegistriesConfig() { + cfg := &mockConfig{ + mirrors: map[string]machine.RegistryMirrorConfig{ + "docker.io": { + Endpoints: []string{"https://registry-1.docker.io", "https://registry-2.docker.io"}, + }, + }, + config: map[string]machine.RegistryConfig{ + "some.host:123": { + Auth: &machine.RegistryAuthConfig{ + Username: "root", + Password: "secret", + Auth: "auth", + IdentityToken: "token", + }, + TLS: &machine.RegistryTLSConfig{ + InsecureSkipVerify: true, + CA: []byte("cacert"), + ClientIdentity: &x509.PEMEncodedCertificateAndKey{ + Crt: []byte("clientcert"), + Key: []byte("clientkey"), + }, + }, + }, + }, + } + + files, err := containerd.GenerateRegistriesConfig(cfg) + suite.Require().NoError(err) + suite.Assert().Equal([]machine.File{ + { + Content: `cacert`, + Permissions: 0600, + Path: "/etc/cri/ca/some.host:123.crt", + Op: "create", + }, + { + Content: `clientcert`, + Permissions: 0600, + Path: "/etc/cri/client/some.host:123.crt", + Op: "create", + }, + { + Content: `clientkey`, + Permissions: 0600, + Path: "/etc/cri/client/some.host:123.key", + Op: "create", + }, + { + Content: `[plugins] + [plugins.cri] + [plugins.cri.registry] + [plugins.cri.registry.mirrors] + [plugins.cri.registry.mirrors."docker.io"] + endpoint = ["https://registry-1.docker.io", "https://registry-2.docker.io"] + [plugins.cri.registry.configs] + [plugins.cri.registry.configs."some.host:123"] + [plugins.cri.registry.configs."some.host:123".auth] + username = "root" + password = "secret" + auth = "auth" + identitytoken = "token" + [plugins.cri.registry.configs."some.host:123".tls] + insecure_skip_verify = true + ca_file = "/etc/cri/ca/some.host:123.crt" + cert_file = "/etc/cri/client/some.host:123.crt" + key_file = "/etc/cri/client/some.host:123.key" +`, + Permissions: 0644, + Path: constants.CRIContainerdConfig, + Op: "append", + }, + }, files) +} + +func TestConfigSuite(t *testing.T) { + suite.Run(t, new(ConfigSuite)) +} diff --git a/internal/pkg/containers/cri/containerd/containerd.go b/internal/pkg/containers/cri/containerd/containerd.go new file mode 100644 index 000000000..28cd8bde2 --- /dev/null +++ b/internal/pkg/containers/cri/containerd/containerd.go @@ -0,0 +1,6 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package containerd provides support for containerd CRI plugin +package containerd diff --git a/internal/pkg/containers/image/image.go b/internal/pkg/containers/image/image.go index 5c9a8ec4d..1d05171f1 100644 --- a/internal/pkg/containers/image/image.go +++ b/internal/pkg/containers/image/image.go @@ -9,6 +9,7 @@ import ( "fmt" "time" + "github.com/talos-systems/talos/pkg/config/machine" "github.com/talos-systems/talos/pkg/retry" "github.com/containerd/containerd" @@ -16,9 +17,11 @@ import ( // Pull is a convenience function that wraps the containerd image pull func with // retry functionality. -func Pull(ctx context.Context, client *containerd.Client, ref string) (img containerd.Image, err error) { +func Pull(ctx context.Context, config machine.Registries, client *containerd.Client, ref string) (img containerd.Image, err error) { + resolver := NewResolver(config) + err = retry.Exponential(1*time.Minute, retry.WithUnits(1*time.Second)).Retry(func() error { - if img, err = client.Pull(ctx, ref, containerd.WithPullUnpack); err != nil { + if img, err = client.Pull(ctx, ref, containerd.WithPullUnpack, containerd.WithResolver(resolver)); err != nil { return retry.ExpectedError(fmt.Errorf("failed to pull image %q: %w", ref, err)) } diff --git a/internal/pkg/containers/image/resolver.go b/internal/pkg/containers/image/resolver.go new file mode 100644 index 000000000..110ccb045 --- /dev/null +++ b/internal/pkg/containers/image/resolver.go @@ -0,0 +1,167 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package image + +import ( + "encoding/base64" + "fmt" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/containerd/containerd/remotes" + "github.com/containerd/containerd/remotes/docker" + + "github.com/talos-systems/talos/pkg/config/machine" +) + +// NewResolver builds registry resolver based on Talos configuration. +func NewResolver(config machine.Registries) remotes.Resolver { + return docker.NewResolver(docker.ResolverOptions{ + Hosts: RegistryHosts(config), + }) +} + +// RegistryHosts returns host configuration per registry. +func RegistryHosts(config machine.Registries) docker.RegistryHosts { + return func(host string) ([]docker.RegistryHost, error) { + var registries []docker.RegistryHost + + endpoints, err := RegistryEndpoints(config, host) + if err != nil { + return nil, err + } + + for _, endpoint := range endpoints { + u, err := url.Parse(endpoint) + if err != nil { + return nil, fmt.Errorf("error parsing endpoint %q for host %q: %w", endpoint, host, err) + } + + transport := newTransport() + client := &http.Client{Transport: transport} + + registryConfig := config.Config()[u.Host] + + if u.Scheme != "https" && registryConfig.TLS != nil { + return nil, fmt.Errorf("TLS config specified for non-HTTPS registry: %q", u.Host) + } + + if registryConfig.TLS != nil { + transport.TLSClientConfig, err = registryConfig.TLS.GetTLSConfig() + if err != nil { + return nil, fmt.Errorf("error preparing TLS config for %q: %w", u.Host, err) + } + } + + if u.Path == "" { + u.Path = "/v2" + } + + uu := u + + registries = append(registries, docker.RegistryHost{ + Client: client, + Authorizer: docker.NewDockerAuthorizer( + docker.WithAuthClient(client), + docker.WithAuthCreds(func(host string) (string, string, error) { + return PrepareAuth(registryConfig.Auth, uu.Host, host) + })), + Host: uu.Host, + Scheme: uu.Scheme, + Path: uu.Path, + Capabilities: docker.HostCapabilityResolve | docker.HostCapabilityPull, + }) + } + + return registries, nil + } +} + +// RegistryEndpoints returns registry endpoints per host using config. +func RegistryEndpoints(config machine.Registries, host string) ([]string, error) { + var endpoints []string + + if hostConfig, ok := config.Mirrors()[host]; ok { + endpoints = hostConfig.Endpoints + } + + if endpoints == nil { + if catchAllConfig, ok := config.Mirrors()["*"]; ok { + endpoints = catchAllConfig.Endpoints + } + } + + if len(endpoints) == 0 { + // still no endpoints, use default + defaultHost, err := docker.DefaultHost(host) + if err != nil { + return nil, fmt.Errorf("error getting default host for %q: %w", host, err) + } + + endpoints = append(endpoints, "https://"+defaultHost) + } + + return endpoints, nil +} + +// PrepareAuth returns authentication info in the format expected by containerd. +func PrepareAuth(auth *machine.RegistryAuthConfig, host, expectedHost string) (string, string, error) { + if auth == nil { + return "", "", nil + } + + if expectedHost != host { + // don't pass auth to unknown hosts + return "", "", nil + } + + if auth.Username != "" { + return auth.Username, auth.Password, nil + } + + if auth.IdentityToken != "" { + return "", auth.IdentityToken, nil + } + + if auth.Auth != "" { + decLen := base64.StdEncoding.DecodedLen(len(auth.Auth)) + decoded := make([]byte, decLen) + + _, err := base64.StdEncoding.Decode(decoded, []byte(auth.Auth)) + if err != nil { + return "", "", fmt.Errorf("error parsing auth for %q: %w", host, err) + } + + fields := strings.SplitN(string(decoded), ":", 2) + if len(fields) != 2 { + return "", "", fmt.Errorf("invalid decoded auth for %q: %q", host, decoded) + } + + user, password := fields[0], fields[1] + + return user, strings.Trim(password, "\x00"), nil + } + + return "", "", fmt.Errorf("invalid auth config for %q", host) +} + +// newTransport creates HTTP transport with default settings. +func newTransport() *http.Transport { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 5 * time.Second, + } +} diff --git a/internal/pkg/containers/image/resolver_test.go b/internal/pkg/containers/image/resolver_test.go new file mode 100644 index 000000000..a869be571 --- /dev/null +++ b/internal/pkg/containers/image/resolver_test.go @@ -0,0 +1,225 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package image_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/talos-systems/talos/internal/pkg/containers/image" + "github.com/talos-systems/talos/pkg/config/machine" +) + +type mockConfig struct { + mirrors map[string]machine.RegistryMirrorConfig + config map[string]machine.RegistryConfig +} + +func (c *mockConfig) Mirrors() map[string]machine.RegistryMirrorConfig { + return c.mirrors +} + +func (c *mockConfig) Config() map[string]machine.RegistryConfig { + return c.config +} + +func (c *mockConfig) ExtraFiles() ([]machine.File, error) { + return nil, fmt.Errorf("not implemented") +} + +type ResolverSuite struct { + suite.Suite +} + +func (suite *ResolverSuite) TestRegistryEndpoints() { + // defaults + endpoints, err := image.RegistryEndpoints(&mockConfig{}, "docker.io") + suite.Assert().NoError(err) + suite.Assert().Equal([]string{"https://registry-1.docker.io"}, endpoints) + + endpoints, err = image.RegistryEndpoints(&mockConfig{}, "quay.io") + suite.Assert().NoError(err) + suite.Assert().Equal([]string{"https://quay.io"}, endpoints) + + // overrides without catch-all + cfg := &mockConfig{ + mirrors: map[string]machine.RegistryMirrorConfig{ + "docker.io": { + Endpoints: []string{"http://127.0.0.1:5000", "https://some.host"}, + }, + }, + } + + endpoints, err = image.RegistryEndpoints(cfg, "docker.io") + suite.Assert().NoError(err) + suite.Assert().Equal([]string{"http://127.0.0.1:5000", "https://some.host"}, endpoints) + + endpoints, err = image.RegistryEndpoints(cfg, "quay.io") + suite.Assert().NoError(err) + suite.Assert().Equal([]string{"https://quay.io"}, endpoints) + + // overrides with catch-all + cfg = &mockConfig{ + mirrors: map[string]machine.RegistryMirrorConfig{ + "docker.io": { + Endpoints: []string{"http://127.0.0.1:5000", "https://some.host"}, + }, + "*": { + Endpoints: []string{"http://127.0.0.1:5001"}, + }, + }, + } + + endpoints, err = image.RegistryEndpoints(cfg, "docker.io") + suite.Assert().NoError(err) + suite.Assert().Equal([]string{"http://127.0.0.1:5000", "https://some.host"}, endpoints) + + endpoints, err = image.RegistryEndpoints(cfg, "quay.io") + suite.Assert().NoError(err) + suite.Assert().Equal([]string{"http://127.0.0.1:5001"}, endpoints) +} + +func (suite *ResolverSuite) TestPrepareAuth() { + user, pass, err := image.PrepareAuth(nil, "docker.io", "docker.io") + suite.Assert().NoError(err) + suite.Assert().Equal("", user) + suite.Assert().Equal("", pass) + + user, pass, err = image.PrepareAuth(&machine.RegistryAuthConfig{ + Username: "root", + Password: "secret", + }, "docker.io", "not.docker.io") + suite.Assert().NoError(err) + suite.Assert().Equal("", user) + suite.Assert().Equal("", pass) + + user, pass, err = image.PrepareAuth(&machine.RegistryAuthConfig{ + Username: "root", + Password: "secret", + }, "docker.io", "docker.io") + suite.Assert().NoError(err) + suite.Assert().Equal("root", user) + suite.Assert().Equal("secret", pass) + + user, pass, err = image.PrepareAuth(&machine.RegistryAuthConfig{ + IdentityToken: "xyz", + }, "docker.io", "docker.io") + suite.Assert().NoError(err) + suite.Assert().Equal("", user) + suite.Assert().Equal("xyz", pass) + + user, pass, err = image.PrepareAuth(&machine.RegistryAuthConfig{ + Auth: "dXNlcjE6c2VjcmV0MQ==", + }, "docker.io", "docker.io") + suite.Assert().NoError(err) + suite.Assert().Equal("user1", user) + suite.Assert().Equal("secret1", pass) + + _, _, err = image.PrepareAuth(&machine.RegistryAuthConfig{}, "docker.io", "docker.io") + suite.Assert().EqualError(err, "invalid auth config for \"docker.io\"") +} + +func (suite *ResolverSuite) TestRegistryHosts() { + registryHosts, err := image.RegistryHosts(&mockConfig{})("docker.io") + suite.Require().NoError(err) + suite.Assert().Len(registryHosts, 1) + suite.Assert().Equal("https", registryHosts[0].Scheme) + suite.Assert().Equal("registry-1.docker.io", registryHosts[0].Host) + suite.Assert().Equal("/v2", registryHosts[0].Path) + suite.Assert().Nil(registryHosts[0].Client.Transport.(*http.Transport).TLSClientConfig) //nolint: errcheck + + cfg := &mockConfig{ + mirrors: map[string]machine.RegistryMirrorConfig{ + "docker.io": { + Endpoints: []string{"http://127.0.0.1:5000/docker.io", "https://some.host"}, + }, + }, + } + + registryHosts, err = image.RegistryHosts(cfg)("docker.io") + suite.Require().NoError(err) + suite.Assert().Len(registryHosts, 2) + suite.Assert().Equal("http", registryHosts[0].Scheme) + suite.Assert().Equal("127.0.0.1:5000", registryHosts[0].Host) + suite.Assert().Equal("/docker.io", registryHosts[0].Path) + suite.Assert().Nil(registryHosts[0].Client.Transport.(*http.Transport).TLSClientConfig) //nolint: errcheck + suite.Assert().Equal("https", registryHosts[1].Scheme) + suite.Assert().Equal("some.host", registryHosts[1].Host) + suite.Assert().Equal("/v2", registryHosts[1].Path) + suite.Assert().Nil(registryHosts[1].Client.Transport.(*http.Transport).TLSClientConfig) //nolint: errcheck + + cfg = &mockConfig{ + mirrors: map[string]machine.RegistryMirrorConfig{ + "docker.io": { + Endpoints: []string{"https://some.host:123"}, + }, + }, + config: map[string]machine.RegistryConfig{ + "some.host:123": { + TLS: &machine.RegistryTLSConfig{ + CA: []byte(caCertMock), + // ClientIdentity: &x509.PEMEncodedCertificateAndKey{}, + }, + Auth: &machine.RegistryAuthConfig{ + Username: "root", + Password: "secret", + }, + }, + }, + } + + registryHosts, err = image.RegistryHosts(cfg)("docker.io") + suite.Require().NoError(err) + suite.Assert().Len(registryHosts, 1) + suite.Assert().Equal("https", registryHosts[0].Scheme) + suite.Assert().Equal("some.host:123", registryHosts[0].Host) + suite.Assert().Equal("/v2", registryHosts[0].Path) + + tlsClientConfig := registryHosts[0].Client.Transport.(*http.Transport).TLSClientConfig //nolint: errcheck + suite.Require().NotNil(tlsClientConfig) + suite.Require().NotNil(tlsClientConfig.RootCAs) + suite.Require().Empty(tlsClientConfig.Certificates) + + suite.Require().NotNil(registryHosts[0].Authorizer) + + req, err := http.NewRequest("GET", "htts://some.host:123/v2", nil) + suite.Require().NoError(err) + + resp := &http.Response{} + resp.Request = req + resp.Header = http.Header{} + resp.Header.Add(http.CanonicalHeaderKey("WWW-Authenticate"), "Basic realm=\"Access to the staging site\", charset=\"UTF-8\"") + + suite.Require().NoError(registryHosts[0].Authorizer.AddResponses(context.Background(), []*http.Response{resp})) + suite.Require().NoError(registryHosts[0].Authorizer.Authorize(context.Background(), req)) + + suite.Assert().Equal("Basic cm9vdDpzZWNyZXQ=", req.Header.Get("Authorization")) +} + +func TestResolverSuite(t *testing.T) { + suite.Run(t, new(ResolverSuite)) +} + +const caCertMock = `-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- +` diff --git a/internal/pkg/install/install.go b/internal/pkg/install/install.go index 71cfa6411..28070ee0c 100644 --- a/internal/pkg/install/install.go +++ b/internal/pkg/install/install.go @@ -44,7 +44,7 @@ func RunInstallerContainer(r runtime.Runtime, opts ...Option) error { var img containerd.Image if options.ImagePull { - img, err = image.Pull(ctx, client, r.Config().Machine().Install().Image()) + img, err = image.Pull(ctx, r.Config().Machine().Registries(), client, r.Config().Machine().Install().Image()) if err != nil { return err } diff --git a/pkg/config/machine/machine.go b/pkg/config/machine/machine.go index ef49e7414..ddc7b22c1 100644 --- a/pkg/config/machine/machine.go +++ b/pkg/config/machine/machine.go @@ -5,6 +5,8 @@ package machine import ( + "crypto/tls" + stdx509 "crypto/x509" "fmt" "os" @@ -53,10 +55,11 @@ type Machine interface { Disks() []Disk Time() Time Env() Env - Files() []File + Files() ([]File, error) Type() Type Kubelet() Kubelet Sysctls() map[string]string + Registries() Registries } // Env represents a set of environment variables. @@ -174,3 +177,95 @@ type Kubelet interface { ExtraArgs() map[string]string ExtraMounts() []specs.Mount } + +// RegistryMirrorConfig represents mirror configuration for a registry. +type RegistryMirrorConfig struct { + // description: | + // 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`). + Endpoints []string `yaml:"endpoints"` +} + +// RegistryConfig specifies auth & TLS config per registry. +type RegistryConfig struct { + TLS *RegistryTLSConfig `yaml:"tls,omitempty"` + Auth *RegistryAuthConfig `yaml:"auth,omitempty"` +} + +// RegistryAuthConfig specifies authentication configuration for a registry. +type RegistryAuthConfig struct { + // description: | + // Optional registry authentication. + // The meaning of each field is the same with the corresponding field in .docker/config.json. + Username string `yaml:"username"` + // description: | + // Optional registry authentication. + // The meaning of each field is the same with the corresponding field in .docker/config.json. + Password string `yaml:"password"` + // description: | + // Optional registry authentication. + // The meaning of each field is the same with the corresponding field in .docker/config.json. + Auth string `yaml:"auth"` + // description: | + // Optional registry authentication. + // The meaning of each field is the same with the corresponding field in .docker/config.json. + IdentityToken string `yaml:"identityToken"` +} + +// RegistryTLSConfig specifies TLS config for HTTPS registries. +type RegistryTLSConfig struct { + // description: | + // Enable mutual TLS authentication with the registry. + // Client certificate and key should be base64-encoded. + // examples: + // - | + // clientIdentity: + // crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJIekNCMHF... + // key: LS0tLS1CRUdJTiBFRDI1NTE5IFBSSVZBVEUgS0VZLS0tLS0KTUM... + ClientIdentity *x509.PEMEncodedCertificateAndKey `yaml:"clientIdentity,omitempty"` + // description: | + // CA registry certificate to add the list of trusted certificates. + // Certificate should be base64-encoded. + CA []byte `yaml:"ca,omitempty"` + // description: | + // Skip TLS server certificate verification (not recommended). + InsecureSkipVerify bool `yaml:"insecureSkipVerify,omitempty"` +} + +// GetTLSConfig prepares TLS configuration for connection. +func (cfg *RegistryTLSConfig) GetTLSConfig() (*tls.Config, error) { + tlsConfig := &tls.Config{} + + if cfg.ClientIdentity != nil { + cert, err := tls.X509KeyPair(cfg.ClientIdentity.Crt, cfg.ClientIdentity.Key) + if err != nil { + return nil, fmt.Errorf("error parsing client identity: %w", err) + } + + tlsConfig.Certificates = []tls.Certificate{cert} + } + + if cfg.CA != nil { + tlsConfig.RootCAs = stdx509.NewCertPool() + tlsConfig.RootCAs.AppendCertsFromPEM(cfg.CA) + } + + if cfg.InsecureSkipVerify { + tlsConfig.InsecureSkipVerify = true + } + + tlsConfig.BuildNameToCertificate() + + return tlsConfig, nil +} + +// Registries defines the configuration for image fetching. +type Registries interface { + // Mirror config by registry host (first part of image reference). + Mirrors() map[string]RegistryMirrorConfig + // Registry config (auth, TLS) by hostname. + Config() map[string]RegistryConfig + // ExtraFiles generates TOML config for containerd CRI plugin. + ExtraFiles() ([]File, error) +} diff --git a/pkg/config/types/v1alpha1/generate/controlplane.go b/pkg/config/types/v1alpha1/generate/controlplane.go index c9df72db2..56caa0220 100644 --- a/pkg/config/types/v1alpha1/generate/controlplane.go +++ b/pkg/config/types/v1alpha1/generate/controlplane.go @@ -25,6 +25,9 @@ func controlPlaneUd(in *Input) (*v1alpha1.Config, error) { InstallImage: in.InstallImage, InstallBootloader: true, }, + MachineRegistries: v1alpha1.RegistriesConfig{ + RegistryMirrors: in.RegistryMirrors, + }, } controlPlaneURL, err := url.Parse(in.ControlPlaneEndpoint) diff --git a/pkg/config/types/v1alpha1/generate/generate.go b/pkg/config/types/v1alpha1/generate/generate.go index c0038f3c7..711f6caff 100644 --- a/pkg/config/types/v1alpha1/generate/generate.go +++ b/pkg/config/types/v1alpha1/generate/generate.go @@ -83,6 +83,8 @@ type Input struct { InstallImage string NetworkConfig *v1alpha1.NetworkConfig + + RegistryMirrors map[string]machine.RegistryMirrorConfig } // GetAPIServerEndpoint returns the formatted host:port of the API server endpoint @@ -320,6 +322,7 @@ func NewInput(clustername string, endpoint string, kubernetesVersion string, opt InstallDisk: options.InstallDisk, InstallImage: options.InstallImage, NetworkConfig: options.NetworkConfig, + RegistryMirrors: options.RegistryMirrors, } return input, nil diff --git a/pkg/config/types/v1alpha1/generate/init.go b/pkg/config/types/v1alpha1/generate/init.go index 89f6a125e..b7774e06f 100644 --- a/pkg/config/types/v1alpha1/generate/init.go +++ b/pkg/config/types/v1alpha1/generate/init.go @@ -25,6 +25,9 @@ func initUd(in *Input) (*v1alpha1.Config, error) { InstallImage: in.InstallImage, InstallBootloader: true, }, + MachineRegistries: v1alpha1.RegistriesConfig{ + RegistryMirrors: in.RegistryMirrors, + }, } certSANs := in.GetAPIServerSANs() diff --git a/pkg/config/types/v1alpha1/generate/join.go b/pkg/config/types/v1alpha1/generate/join.go index 56045b562..b3500f19d 100644 --- a/pkg/config/types/v1alpha1/generate/join.go +++ b/pkg/config/types/v1alpha1/generate/join.go @@ -25,6 +25,9 @@ func workerUd(in *Input) (*v1alpha1.Config, error) { InstallImage: in.InstallImage, InstallBootloader: true, }, + MachineRegistries: v1alpha1.RegistriesConfig{ + RegistryMirrors: in.RegistryMirrors, + }, } controlPlaneURL, err := url.Parse(in.ControlPlaneEndpoint) diff --git a/pkg/config/types/v1alpha1/generate/options.go b/pkg/config/types/v1alpha1/generate/options.go index c48bc28c5..d245f2ee3 100644 --- a/pkg/config/types/v1alpha1/generate/options.go +++ b/pkg/config/types/v1alpha1/generate/options.go @@ -4,7 +4,10 @@ package generate -import v1alpha1 "github.com/talos-systems/talos/pkg/config/types/v1alpha1" +import ( + "github.com/talos-systems/talos/pkg/config/machine" + v1alpha1 "github.com/talos-systems/talos/pkg/config/types/v1alpha1" +) // GenOption controls generate options specific to input generation. type GenOption func(o *GenOptions) error @@ -54,6 +57,19 @@ func WithNetworkConfig(network *v1alpha1.NetworkConfig) GenOption { } } +// WithRegistryMirror configures registry mirror endpoint(s). +func WithRegistryMirror(host string, endpoints ...string) GenOption { + return func(o *GenOptions) error { + if o.RegistryMirrors == nil { + o.RegistryMirrors = make(map[string]machine.RegistryMirrorConfig) + } + + o.RegistryMirrors[host] = machine.RegistryMirrorConfig{Endpoints: endpoints} + + return nil + } +} + // GenOptions describes generate parameters. type GenOptions struct { EndpointList []string @@ -61,6 +77,7 @@ type GenOptions struct { InstallImage string AdditionalSubjectAltNames []string NetworkConfig *v1alpha1.NetworkConfig + RegistryMirrors map[string]machine.RegistryMirrorConfig } // DefaultGenOptions returns default options. diff --git a/pkg/config/types/v1alpha1/v1alpha1_configurator.go b/pkg/config/types/v1alpha1/v1alpha1_configurator.go index 799ac13ff..b92d4b7ed 100644 --- a/pkg/config/types/v1alpha1/v1alpha1_configurator.go +++ b/pkg/config/types/v1alpha1/v1alpha1_configurator.go @@ -14,6 +14,7 @@ import ( "github.com/kubernetes-sigs/bootkube/pkg/asset" "github.com/opencontainers/runtime-spec/specs-go" + criplugin "github.com/talos-systems/talos/internal/pkg/containers/cri/containerd" "github.com/talos-systems/talos/pkg/config/cluster" "github.com/talos-systems/talos/pkg/config/machine" "github.com/talos-systems/talos/pkg/constants" @@ -103,8 +104,10 @@ func (m *MachineConfig) Env() machine.Env { } // Files implements the Configurator interface. -func (m *MachineConfig) Files() []machine.File { - return m.MachineFiles +func (m *MachineConfig) Files() ([]machine.File, error) { + files, err := m.Registries().ExtraFiles() + + return append(files, m.MachineFiles...), err } // Type implements the Configurator interface. @@ -153,6 +156,11 @@ func (m *MachineConfig) SetCertSANs(sans []string) { m.MachineCertSANs = append(m.MachineCertSANs, sans...) } +// Registries implements the Configurator interface. +func (m *MachineConfig) Registries() machine.Registries { + return &m.MachineRegistries +} + // Image implements the Configurator interface. func (k *KubeletConfig) Image() string { image := k.KubeletImage @@ -302,6 +310,21 @@ func (e *EtcdConfig) ExtraArgs() map[string]string { return e.EtcdExtraArgs } +// Mirrors implements the Registries interface. +func (r *RegistriesConfig) Mirrors() map[string]machine.RegistryMirrorConfig { + return r.RegistryMirrors +} + +// Config implements the Registries interface. +func (r *RegistriesConfig) Config() map[string]machine.RegistryConfig { + return r.RegistryConfig +} + +// ExtraFiles implements the Registries interface. +func (r *RegistriesConfig) ExtraFiles() ([]machine.File, error) { + return criplugin.GenerateRegistriesConfig(r) +} + // Token implements the Configurator interface. func (c *ClusterConfig) Token() cluster.Token { return c diff --git a/pkg/config/types/v1alpha1/v1alpha1_types.go b/pkg/config/types/v1alpha1/v1alpha1_types.go index fd0d93b87..cc123e50d 100644 --- a/pkg/config/types/v1alpha1/v1alpha1_types.go +++ b/pkg/config/types/v1alpha1/v1alpha1_types.go @@ -179,6 +179,42 @@ type MachineConfig struct { // kernel.domainname: talos.dev // net.ipv4.ip_forward: "0" MachineSysctls map[string]string `yaml:"sysctls,omitempty"` + // description: | + // Used to configure the machine's container image registry mirrors. + // + // Automatically generates matching CRI configuration for registry mirrors. + // + // Section `mirrors` allows to redirect requests for images to non-default registry, + // which might be local registry or caching mirror. + // + // Section `config` provides a way to authenticate to the registry with TLS client + // identity, provide registry CA, or authentication information. + // Authentication information has same meaning with the corresponding field in `.docker/config.json`. + // + // See also matching configuration for [CRI containerd plugin](https://github.com/containerd/cri/blob/master/docs/registry.md). + // examples: + // - | + // registries: + // mirrors: + // docker.io: + // endpoints: + // - https://registry-1.docker.io + // '*': + // endpoints: + // - http://some.host:123/ + // config: + // "some.host:123": + // tls: + // CA: ... # base64-encoded CA certificate in PEM format + // clientIdentity: + // cert: ... # base64-encoded client certificate in PEM format + // key: ... # base64-encoded client key in PEM format + // auth: + // username: ... + // password: ... + // auth: ... + // identityToken: ... + MachineRegistries RegistriesConfig `yaml:"registries,omitempty"` } // ClusterConfig reperesents the cluster-wide config values @@ -428,6 +464,26 @@ type TimeConfig struct { TimeServers []string `yaml:"servers,omitempty"` } +// RegistriesConfig represents the image pull options. +type RegistriesConfig struct { + // description: | + // Specifies mirror configuration for each registry. + // This setting allows to use local pull-through caching registires, + // air-gapped installations, etc. + // + // Registry name is the first segment of image identifier, with 'docker.io' + // being default one. + // Name '*' catches any registry names not specified explicitly. + RegistryMirrors map[string]machine.RegistryMirrorConfig `yaml:"mirrors,omitempty"` + // description: | + // Specifies TLS & auth configuration for HTTPS image registries. + // Mutual TLS can be enabled with 'clientIdentity' option. + // + // TLS configuration can be skipped if registry has trusted + // server certificate. + RegistryConfig map[string]machine.RegistryConfig `yaml:"config,omitempty"` +} + // PodCheckpointer represents the pod-checkpointer config values type PodCheckpointer struct { // description: |