From cd317d53306d09074d2cc222219e520c18f8057d Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Thu, 23 Apr 2026 19:12:13 +0400 Subject: [PATCH] feat: support auth for Image Factory in cluster create Allows to authenticate to Image Factory (if Image Factory is configured for auth), applies for HTTP downloads (e.g. ISO), and injects registry auth into Talos as well. Signed-off-by: Andrey Smirnov (cherry picked from commit c2948cef232f6a175312636369b444124cb995db) --- .../internal/makers/common_test.go | 17 +++++++--- .../internal/makers/docker_test.go | 2 +- .../configmaker/internal/makers/qemu.go | 20 +++++++++-- .../configmaker/internal/makers/qemu_test.go | 34 +++++++++++++++++-- .../mgmt/cluster/create/clusterops/options.go | 17 +++++++++- .../cmd/mgmt/cluster/create/cmd_dev.go | 2 +- .../cmd/mgmt/cluster/create/cmd_qemu.go | 12 ++++--- .../cmd/mgmt/cluster/create/create.go | 4 +++ .../cmd/mgmt/cluster/create/create_qemu.go | 17 ++++++++++ website/content/v1.13/reference/cli.md | 10 +++--- 10 files changed, 114 insertions(+), 21 deletions(-) diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go index 99e632fc9..30f5fadb0 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go @@ -15,6 +15,7 @@ import ( "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers" "github.com/siderolabs/talos/pkg/machinery/config" "github.com/siderolabs/talos/pkg/machinery/config/bundle" + "github.com/siderolabs/talos/pkg/machinery/config/configpatcher" "github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/siderolabs/talos/pkg/machinery/config/generate" "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets" @@ -144,11 +145,11 @@ func TestCommonMaker_MachineConfig(t *testing.T) { cOps := clusterops.GetCommon() m := getInitializedTestMaker(t, cOps) - assertConfigDefaultness(t, cOps, m) + assertConfigDefaultness(t, cOps, m, nil) } // assertConfigDefaultness makes sure the maker-generated machine configs are not different from default talos machine configs. -func assertConfigDefaultness[ExtraOps any](t *testing.T, cOps clusterops.Common, m makers.Maker[ExtraOps], desiredExtraGenOps ...generate.Option) { +func assertConfigDefaultness[ExtraOps any](t *testing.T, cOps clusterops.Common, m makers.Maker[ExtraOps], desiredExtraGenOps []generate.Option, extraPatches ...configpatcher.Patch) { var versionContract *config.VersionContract secretsBundle, err := secrets.NewBundle(secrets.NewClock(), versionContract) @@ -171,14 +172,22 @@ func assertConfigDefaultness[ExtraOps any](t *testing.T, cOps clusterops.Common, require.NoError(t, err) for _, node := range clusterCfgs.ClusterRequest.Nodes { - assertMachineConfig(t, in, node) + assertMachineConfig(t, in, node, extraPatches...) } } -func assertMachineConfig(t *testing.T, in *generate.Input, node provision.NodeRequest) { +func assertMachineConfig(t *testing.T, in *generate.Input, node provision.NodeRequest, extraPatches ...configpatcher.Patch) { cfgExpected, err := in.Config(node.Type) require.NoError(t, err) + if len(extraPatches) > 0 { + patched, err := configpatcher.Apply(configpatcher.WithConfig(cfgExpected), extraPatches) + require.NoError(t, err) + + cfgExpected, err = patched.Config() + require.NoError(t, err) + } + cfgGot := node.Config cfgGot = cfgGot.RedactSecrets("secret") diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/docker_test.go b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/docker_test.go index d0d74a42e..ec969494f 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/docker_test.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/docker_test.go @@ -29,5 +29,5 @@ func TestDockerMaker_MachineConfig(t *testing.T) { generate.WithAdditionalSubjectAltNames([]string{"talos-api-endpoint.test"}), } - assertConfigDefaultness(t, cOps, *m.Maker, desiredExtraGenOps...) + assertConfigDefaultness(t, cOps, *m.Maker, desiredExtraGenOps) } diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/qemu.go b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/qemu.go index 7342046b4..0a412d63e 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/qemu.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/qemu.go @@ -30,6 +30,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/generate" "github.com/siderolabs/talos/pkg/machinery/config/machine" "github.com/siderolabs/talos/pkg/machinery/config/types/block" + "github.com/siderolabs/talos/pkg/machinery/config/types/cri" networkcfg "github.com/siderolabs/talos/pkg/machinery/config/types/network" "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" "github.com/siderolabs/talos/pkg/machinery/constants" @@ -170,6 +171,21 @@ func (m *Qemu) AddExtraGenOps() error { m.GenOps = slices.Concat(m.GenOps, []generate.Option{generate.WithAdditionalSubjectAltNames(m.Endpoints)}) } + for host, auth := range m.EOps.DownloadHTTPAuth { + registryAuthConfig := cri.NewRegistryAuthConfigV1Alpha1(host) + registryAuthConfig.RegistryUsername = auth.Username + registryAuthConfig.RegistryPassword = auth.Password + + ctr, err := container.New(registryAuthConfig) + if err != nil { + return err + } + + m.ConfigBundleOps = append(m.ConfigBundleOps, + bundle.WithPatch([]configpatcher.Patch{configpatcher.NewStrategicMergePatch(ctr)}), + ) + } + return nil } @@ -269,7 +285,7 @@ func (m *Qemu) ModifyClusterRequest() error { m.ClusterRequest.Network.NoMasqueradeCIDRs = noMasqueradeCIDRs m.ClusterRequest.Network.DHCPSkipHostname = m.EOps.DHCPSkipHostname m.ClusterRequest.Network.NetworkChaos = m.EOps.NetworkChaos - m.ClusterRequest.Network.Jitter = m.EOps.Jjitter + m.ClusterRequest.Network.Jitter = m.EOps.Jitter m.ClusterRequest.Network.Latency = m.EOps.Latency m.ClusterRequest.Network.PacketLoss = m.EOps.PacketLoss m.ClusterRequest.Network.PacketReorder = m.EOps.PacketReorder @@ -294,7 +310,7 @@ func (m *Qemu) ModifyClusterRequest() error { func (m *Qemu) validateNetworkChaosParams() error { if !m.EOps.NetworkChaos { - if m.EOps.Jjitter != 0 || m.EOps.Latency != 0 || m.EOps.PacketLoss != 0 || m.EOps.PacketReorder != 0 || m.EOps.PacketCorrupt != 0 || m.EOps.Bandwidth != 0 { + if m.EOps.Jitter != 0 || m.EOps.Latency != 0 || m.EOps.PacketLoss != 0 || m.EOps.PacketReorder != 0 || m.EOps.PacketCorrupt != 0 || m.EOps.Bandwidth != 0 { return errors.New("network chaos flags can only be used with network-chaos option enabled") } } diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/qemu_test.go b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/qemu_test.go index 7520a85f4..156c3bb05 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/qemu_test.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/qemu_test.go @@ -13,7 +13,9 @@ import ( "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/clusterops" "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers" "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/flags" - "github.com/siderolabs/talos/pkg/machinery/config/generate" + "github.com/siderolabs/talos/pkg/machinery/config/configpatcher" + "github.com/siderolabs/talos/pkg/machinery/config/container" + "github.com/siderolabs/talos/pkg/machinery/config/types/cri" "github.com/siderolabs/talos/pkg/provision" ) @@ -28,9 +30,35 @@ func TestQemuMaker_MachineConfig(t *testing.T) { }) require.NoError(t, err) - desiredExtraGenOps := []generate.Option{} + assertConfigDefaultness(t, cOps, *m.Maker, nil) +} - assertConfigDefaultness(t, cOps, *m.Maker, desiredExtraGenOps...) +func TestQemuMaker_RegistryAuth(t *testing.T) { + cOps := clusterops.GetCommon() + qOps := clusterops.GetQemu() + + qOps.DownloadHTTPAuth = map[string]clusterops.HTTPAuth{ + "example.com": { + Username: "username", + Password: "password", + }, + } + + m, err := makers.NewQemu(makers.MakerOptions[clusterops.Qemu]{ + ExtraOps: qOps, + CommonOps: cOps, + Provisioner: testProvisioner{}, // use test provisioner to simplify the test case. + }) + require.NoError(t, err) + + registryAuthConfig := cri.NewRegistryAuthConfigV1Alpha1("example.com") + registryAuthConfig.RegistryUsername = "username" + registryAuthConfig.RegistryPassword = "password" + + ctr, err := container.New(registryAuthConfig) + require.NoError(t, err) + + assertConfigDefaultness(t, cOps, *m.Maker, nil, configpatcher.NewStrategicMergePatch(ctr)) } func TestQemuMaker_Disks(t *testing.T) { diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/options.go b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/options.go index 6c7afd054..341b8abba 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/options.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/options.go @@ -129,7 +129,7 @@ type Qemu struct { ExtraBootKernelArgs string DHCPSkipHostname bool NetworkChaos bool - Jjitter time.Duration + Jitter time.Duration Latency time.Duration PacketLoss float64 PacketReorder float64 @@ -146,6 +146,21 @@ type Qemu struct { ImageCacheTLSCertFile string ImageCacheTLSKeyFile string ImageCachePort uint16 + + // DownloadHTTPAuth is a map of endpoint hosts to basic auth credentials used for + // HTTP boot asset downloads and for injecting CRI registry auth into generated + // Talos config. + // + // The key is url.URL.Host (that is, "host[:port]", for example + // "example.com" or "registry.example.com:5000"), and the value is the HTTPAuth + // containing the username and password for that endpoint. + DownloadHTTPAuth map[string]HTTPAuth +} + +// HTTPAuth represents basic authentication credentials for downloading boot assets. +type HTTPAuth struct { + Username string + Password string } // GetCommon returns the default common options. diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/cmd_dev.go b/cmd/talosctl/cmd/mgmt/cluster/create/cmd_dev.go index 5b489a7ff..f9dffdfc9 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/cmd_dev.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/cmd_dev.go @@ -242,7 +242,7 @@ func getCreateCmd(cmdName string, hidden bool) *cobra.Command { qemu.StringVar(&qOps.ExtraBootKernelArgs, extraBootKernelArgsFlag, qOps.ExtraBootKernelArgs, "add extra kernel args to the initial boot from vmlinuz and initramfs") qemu.BoolVar(&qOps.DHCPSkipHostname, dhcpSkipHostnameFlag, qOps.DHCPSkipHostname, "skip announcing hostname via DHCP") qemu.BoolVar(&qOps.NetworkChaos, networkChaosFlag, qOps.NetworkChaos, "enable to use network chaos parameters") - qemu.DurationVar(&qOps.Jjitter, jitterFlag, qOps.Jjitter, "specify jitter on the bridge interface") + qemu.DurationVar(&qOps.Jitter, jitterFlag, qOps.Jitter, "specify jitter on the bridge interface") qemu.DurationVar(&qOps.Latency, latencyFlag, qOps.Latency, "specify latency on the bridge interface") qemu.Float64Var(&qOps.PacketLoss, packetLossFlag, qOps.PacketLoss, "specify percent of packet loss on the bridge interface. e.g. 50% = 0.50 (default: 0.0)") diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/cmd_qemu.go b/cmd/talosctl/cmd/mgmt/cluster/create/cmd_qemu.go index 1c2a71a44..519e15941 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/cmd_qemu.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/cmd_qemu.go @@ -19,9 +19,10 @@ import ( ) type presetOptions struct { - schematicID string - imageFactoryURL string - presets []string + schematicID string + imageFactoryURL string + imageFactoryAuth string + presets []string } func init() { @@ -40,8 +41,9 @@ func init() { qemu := pflag.NewFlagSet("qemu", pflag.PanicOnError) addDisksFlag(qemu, &qOps.Disks) - qemu.StringVar(&presetOptions.schematicID, "schematic-id", "", "image factory schematic id (defaults to an empty schematic)") - qemu.StringVar(&presetOptions.imageFactoryURL, "image-factory-url", constants.ImageFactoryURL, "image factory url") + qemu.StringVar(&presetOptions.schematicID, "schematic-id", "", "Image Factory schematic id (defaults to an empty schematic)") + qemu.StringVar(&presetOptions.imageFactoryURL, "image-factory-url", constants.ImageFactoryURL, "Image Factory url") + qemu.StringVar(&presetOptions.imageFactoryAuth, "image-factory-auth", "", "username:password for authenticating with the Image Factory") qemu.StringSliceVar(&presetOptions.presets, "presets", []string{preset.ISO{}.Name()}, "list of presets to apply") return qemu diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/create.go b/cmd/talosctl/cmd/mgmt/cluster/create/create.go index 32745d770..77e51eafa 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/create.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/create.go @@ -108,6 +108,10 @@ func downloadBootAssets(ctx context.Context, qOps *clusterops.Qemu) error { u.RawQuery = q.Encode() } + if auth, ok := qOps.DownloadHTTPAuth[u.Host]; ok && u.User == nil { + u.User = url.UserPassword(auth.Username, auth.Password) + } + _, err = client.Get(ctx, &getter.Request{ Src: u.String(), Dst: filepath.Join(cacheDir, destPath), diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/create_qemu.go b/cmd/talosctl/cmd/mgmt/cluster/create/create_qemu.go index 41d84b3d7..acb8d44c7 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/create_qemu.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/create_qemu.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "slices" + "strings" "github.com/siderolabs/talos/cmd/talosctl/cmd/constants" clustercmd "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster" @@ -67,6 +68,22 @@ func createQemuCluster( return err } + if presetOptions.imageFactoryAuth != "" { + username, password, ok := strings.Cut(presetOptions.imageFactoryAuth, ":") + if !ok { + return fmt.Errorf("invalid Image Factory auth format: expected username:password") + } + + if qOps.DownloadHTTPAuth == nil { + qOps.DownloadHTTPAuth = make(map[string]clusterops.HTTPAuth) + } + + qOps.DownloadHTTPAuth[factoryURL.Host] = clusterops.HTTPAuth{ + Username: username, + Password: password, + } + } + if err := downloadBootAssets(ctx, &qOps); err != nil { return err } diff --git a/website/content/v1.13/reference/cli.md b/website/content/v1.13/reference/cli.md index ce30ab374..686a8390c 100644 --- a/website/content/v1.13/reference/cli.md +++ b/website/content/v1.13/reference/cli.md @@ -309,13 +309,14 @@ talosctl cluster create qemu [flags] --cpus-workers string the share of CPUs as fraction for each worker/VM (default "2.0") --disks disks list of disks to create in format ":" (disks after the first one are added only to worker machines) (default virtio:10GiB,virtio:6GiB) -h, --help help for qemu - --image-factory-url string image factory url (default "https://factory.talos.dev/") + --image-factory-auth string username:password for authenticating with the Image Factory + --image-factory-url string Image Factory url (default "https://factory.talos.dev/") --kubernetes-version string desired kubernetes version to run (default "1.36.0") --memory-controlplanes string(mb,gb) the limit on memory usage for each control plane/VM (default 2.0GiB) --memory-workers string(mb,gb) the limit on memory usage for each worker/VM (default 2.0GiB) --omni-api-endpoint string the Omni API endpoint (must include a scheme, a hostname and a join token, e.g. 'https://siderolink.omni.example?jointoken=foobar') --presets strings list of presets to apply (default [iso]) - --schematic-id string image factory schematic id (defaults to an empty schematic) + --schematic-id string Image Factory schematic id (defaults to an empty schematic) --talos-version string the desired talos version (default "latest") --talosconfig-destination string The location to save the generated Talos configuration file to. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. --workers int the number of workers to create (default 1) @@ -523,13 +524,14 @@ talosctl cluster create qemu [flags] --cpus-workers string the share of CPUs as fraction for each worker/VM (default "2.0") --disks disks list of disks to create in format ":" (disks after the first one are added only to worker machines) (default virtio:10GiB,virtio:6GiB) -h, --help help for qemu - --image-factory-url string image factory url (default "https://factory.talos.dev/") + --image-factory-auth string username:password for authenticating with the Image Factory + --image-factory-url string Image Factory url (default "https://factory.talos.dev/") --kubernetes-version string desired kubernetes version to run (default "1.36.0") --memory-controlplanes string(mb,gb) the limit on memory usage for each control plane/VM (default 2.0GiB) --memory-workers string(mb,gb) the limit on memory usage for each worker/VM (default 2.0GiB) --omni-api-endpoint string the Omni API endpoint (must include a scheme, a hostname and a join token, e.g. 'https://siderolink.omni.example?jointoken=foobar') --presets strings list of presets to apply (default [iso]) - --schematic-id string image factory schematic id (defaults to an empty schematic) + --schematic-id string Image Factory schematic id (defaults to an empty schematic) --talos-version string the desired talos version (default "latest") --talosconfig-destination string The location to save the generated Talos configuration file to. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. --workers int the number of workers to create (default 1)