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)