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 <andrey.smirnov@siderolabs.com>
(cherry picked from commit c2948cef232f6a175312636369b444124cb995db)
This commit is contained in:
Andrey Smirnov 2026-04-23 19:12:13 +04:00
parent 92ca9e16f9
commit cd317d5330
No known key found for this signature in database
GPG Key ID: 322C6F63F594CE7C
10 changed files with 114 additions and 21 deletions

View File

@ -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")

View File

@ -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)
}

View File

@ -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")
}
}

View File

@ -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) {

View File

@ -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.

View File

@ -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)")

View File

@ -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

View File

@ -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),

View File

@ -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
}

View File

@ -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 "<driver1>:<size1>" (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 "<driver1>:<size1>" (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)