From 080efcbda2c4334f9d8c70804a5a37f0cdb2df2d Mon Sep 17 00:00:00 2001 From: Mateusz Urbanek Date: Mon, 29 Dec 2025 10:48:37 +0100 Subject: [PATCH] feat: add k8s-version parameter to k8s-bundle Allow overriding K8s version in the command. Signed-off-by: Mateusz Urbanek --- cmd/talosctl/cmd/talos/image.go | 74 ++++++--- cmd/talosctl/pkg/talos/helpers/flags.go | 69 +++++++- hack/release.toml | 6 + .../pkg/controllers/k8s/control_plane.go | 2 +- pkg/images/list.go | 157 +++++++++++++----- website/content/v1.13/reference/cli.md | 6 +- 6 files changed, 236 insertions(+), 78 deletions(-) diff --git a/cmd/talosctl/cmd/talos/image.go b/cmd/talosctl/cmd/talos/image.go index 05d06f190..6486fa2bb 100644 --- a/cmd/talosctl/cmd/talos/image.go +++ b/cmd/talosctl/cmd/talos/image.go @@ -138,36 +138,56 @@ var imagePullCmd = &cobra.Command{ }, } -// imageDefaultCmd represents the image k8s-bundle command. -var imageDefaultCmd = &cobra.Command{ +var imageK8sBundleCmdFlags = struct { + k8sVersion pflag.Value + flannelVersion pflag.Value + corednsVersion pflag.Value + etcdVersion pflag.Value +}{ + k8sVersion: helpers.Semver(constants.DefaultKubernetesVersion), + flannelVersion: helpers.Semver(constants.FlannelVersion), + corednsVersion: helpers.Semver(constants.DefaultCoreDNSVersion), + etcdVersion: helpers.Semver(constants.DefaultEtcdVersion), +} + +// imageK8sBundleCmd represents the image k8s-bundle command. +var imageK8sBundleCmd = &cobra.Command{ Use: "k8s-bundle", Aliases: []string{"default"}, Short: "List the default Kubernetes images used by Talos", Long: ``, RunE: func(cmd *cobra.Command, args []string) error { - images := images.List(container.NewV1Alpha1(&v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineKubelet: &v1alpha1.KubeletConfig{}, + images := images.ListWithOptions(container.NewV1Alpha1( + &v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{ + MachineKubelet: &v1alpha1.KubeletConfig{}, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + EtcdConfig: &v1alpha1.EtcdConfig{}, + APIServerConfig: &v1alpha1.APIServerConfig{}, + ControllerManagerConfig: &v1alpha1.ControllerManagerConfig{}, + SchedulerConfig: &v1alpha1.SchedulerConfig{}, + CoreDNSConfig: &v1alpha1.CoreDNS{}, + ProxyConfig: &v1alpha1.ProxyConfig{}, + }, + }), + images.VersionsListOptions{ + KubernetesVersion: imageK8sBundleCmdFlags.k8sVersion.String(), + EtcdVersion: imageK8sBundleCmdFlags.etcdVersion.String(), + FlannelVersion: imageK8sBundleCmdFlags.flannelVersion.String(), + CoreDNSVersion: imageK8sBundleCmdFlags.corednsVersion.String(), }, - ClusterConfig: &v1alpha1.ClusterConfig{ - EtcdConfig: &v1alpha1.EtcdConfig{}, - APIServerConfig: &v1alpha1.APIServerConfig{}, - ControllerManagerConfig: &v1alpha1.ControllerManagerConfig{}, - SchedulerConfig: &v1alpha1.SchedulerConfig{}, - CoreDNSConfig: &v1alpha1.CoreDNS{}, - ProxyConfig: &v1alpha1.ProxyConfig{}, - }, - })) + ) fmt.Printf("%s\n", images.Flannel) fmt.Printf("%s\n", images.CoreDNS) fmt.Printf("%s\n", images.Etcd) + fmt.Printf("%s\n", images.Pause) fmt.Printf("%s\n", images.KubeAPIServer) fmt.Printf("%s\n", images.KubeControllerManager) fmt.Printf("%s\n", images.KubeScheduler) fmt.Printf("%s\n", images.KubeProxy) fmt.Printf("%s\n", images.Kubelet) - fmt.Printf("%s\n", images.Pause) return nil }, @@ -317,15 +337,15 @@ var imageIntegrationCmd = &cobra.Command{ })) imageNames := []string{ - imgs.Flannel, - imgs.CoreDNS, - imgs.Etcd, - imgs.KubeAPIServer, - imgs.KubeControllerManager, - imgs.KubeScheduler, - imgs.KubeProxy, - imgs.Kubelet, - imgs.Pause, + imgs.Flannel.String(), + imgs.CoreDNS.String(), + imgs.Etcd.String(), + imgs.KubeAPIServer.String(), + imgs.KubeControllerManager.String(), + imgs.KubeScheduler.String(), + imgs.KubeProxy.String(), + imgs.Kubelet.String(), + imgs.Pause.String(), "registry.k8s.io/conformance:v" + constants.DefaultKubernetesVersion, "docker.io/library/alpine:latest", "ghcr.io/siderolabs/talosctl:latest", @@ -651,7 +671,11 @@ func init() { imageTalosBundleCmd.PersistentFlags().BoolVar(&imageTalosBundleCmdFlags.overlays, "overlays", true, "Include images that belong to Talos overlays") imageTalosBundleCmd.PersistentFlags().BoolVar(&imageTalosBundleCmdFlags.extensions, "extensions", true, "Include images that belong to Talos extensions") - imageCmd.AddCommand(imageDefaultCmd) + imageCmd.AddCommand(imageK8sBundleCmd) + imageK8sBundleCmd.PersistentFlags().Var(imageK8sBundleCmdFlags.k8sVersion, "k8s-version", "Kubernetes semantic version") + imageK8sBundleCmd.PersistentFlags().Var(imageK8sBundleCmdFlags.etcdVersion, "etcd-version", "ETCD semantic version") + imageK8sBundleCmd.PersistentFlags().Var(imageK8sBundleCmdFlags.flannelVersion, "flannel-version", "Flannel CNI semantic version") + imageK8sBundleCmd.PersistentFlags().Var(imageK8sBundleCmdFlags.corednsVersion, "coredns-version", "CoreDNS semantic version") imageCmd.AddCommand(imageCacheCreateCmd) imageCacheCreateCmd.PersistentFlags().StringVar(&imageCacheCreateCmdFlags.imageCachePath, "image-cache-path", "", "directory to save the image cache in OCI format") diff --git a/cmd/talosctl/pkg/talos/helpers/flags.go b/cmd/talosctl/pkg/talos/helpers/flags.go index 636b7e0c5..036e66bd1 100644 --- a/cmd/talosctl/pkg/talos/helpers/flags.go +++ b/cmd/talosctl/pkg/talos/helpers/flags.go @@ -8,32 +8,32 @@ import ( "fmt" "slices" + "github.com/blang/semver/v4" "github.com/spf13/pflag" ) -// choiceValue implements the [pflag.Value] interface. type choiceValue struct { value string validate func(string) error } -// Set sets the value of the choice. -func (f *choiceValue) Set(s string) error { - err := f.validate(s) +// Set implements pflag.Value interface. +func (v *choiceValue) Set(s string) error { + err := v.validate(s) if err != nil { return err } - f.value = s + v.value = s return nil } -// Type returns the type of the choice, which must be "string" for [pflag.FlagSet.GetString]. -func (f *choiceValue) Type() string { return "string" } +// Type implements pflag.Value interface. +func (v *choiceValue) Type() string { return "string" } -// String returns the current value of the choice. -func (f *choiceValue) String() string { return f.value } +// String implements pflag.Value interface. +func (v *choiceValue) String() string { return v.value } // StringChoice returns a [choiceValue] that validates the value against a set // of choices. Only the last value will be used if multiple values are set. @@ -51,3 +51,54 @@ func StringChoice(defaultValue string, otherChoices ...string) pflag.Value { }, } } + +type semverValue struct { + value semver.Version + validators []SemverValidateFunc +} + +// SemverValidateFunc allows setting restrictions on the version. +type SemverValidateFunc func(v semver.Version) error + +// Set implements pflag.Value interface. +func (v *semverValue) Set(s string) error { + vers, err := semver.ParseTolerant(s) + if err != nil { + return err + } + + for _, validator := range v.validators { + if err := validator(vers); err != nil { + return err + } + } + + v.value = vers + + return nil +} + +// Type implements pflag.Value interface. +func (v *semverValue) Type() string { return "semver" } + +// String implements pflag.Value interface. +func (v *semverValue) String() string { return "v" + v.value.String() } + +// Semver returns a pflag.Value that parses and stores a semantic version. +// +// Parsing is performed using semver.ParseTolerant. After parsing, any provided +// SemverValidateFunc validators are applied in order and may reject the version. +// +// The returned value is initialized with defaultValue, which is used until Set +// is called successfully. +func Semver(defaultValue string, validators ...SemverValidateFunc) pflag.Value { + v, err := semver.ParseTolerant(defaultValue) + if err != nil { + panic(err) + } + + return &semverValue{ + value: v, + validators: validators, + } +} diff --git a/hack/release.toml b/hack/release.toml index c99bd45c1..a2eea5b8d 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -101,6 +101,12 @@ The switch and inventory backfill is automatic and no action is needed from the description = """\ The `talosctl images talos-bundle` command now accepts optional `--ovelays` and `--extensions` flags. If those are set to `false`, the command will not attempt to reach out to the container registry to fetch the latest versions and digests of the overlays and extensions. +""" + + [notes.images_k8s_bundle] + title = "Talosctl images k8s-bundle subcommand accepts version parameter" + description = """\ +The `talosctl images k8s-bundle` command now accepts an optional version overrides arguments. """ [make_deps] diff --git a/internal/app/machined/pkg/controllers/k8s/control_plane.go b/internal/app/machined/pkg/controllers/k8s/control_plane.go index 8ba85c445..ff2b29cc6 100644 --- a/internal/app/machined/pkg/controllers/k8s/control_plane.go +++ b/internal/app/machined/pkg/controllers/k8s/control_plane.go @@ -339,7 +339,7 @@ func NewControlPlaneBootstrapManifestsController() *ControlPlaneBootstrapManifes DNSServiceIPv6: dnsServiceIPv6, FlannelEnabled: cfgProvider.Cluster().Network().CNI().Name() == constants.FlannelCNI, - FlannelImage: images.Flannel, + FlannelImage: images.Flannel.String(), FlannelExtraArgs: cfgProvider.Cluster().Network().CNI().Flannel().ExtraArgs(), FlannelKubeServiceHost: flannelKubeServiceHost, FlannelKubeServicePort: flannelKubeServicePort, diff --git a/pkg/images/list.go b/pkg/images/list.go index fb1245233..f2b2359f7 100644 --- a/pkg/images/list.go +++ b/pkg/images/list.go @@ -7,26 +7,25 @@ package images import ( "fmt" + "github.com/google/go-containerregistry/pkg/name" + "github.com/siderolabs/talos/pkg/machinery/config" "github.com/siderolabs/talos/pkg/machinery/constants" ) // Versions holds all the images (and their versions) that are used in Talos. type Versions struct { - Etcd string - Flannel string - CoreDNS string + Etcd name.Tag + Flannel name.Tag + CoreDNS name.Tag - Kubelet string - KubeAPIServer string - KubeControllerManager string - KubeProxy string - KubeScheduler string + Kubelet name.Tag + KubeAPIServer name.Tag + KubeControllerManager name.Tag + KubeProxy name.Tag + KubeScheduler name.Tag - Installer string - Talos string - - Pause string + Pause name.Tag } // DefaultSandboxImage is defined as a constant in cri package of containerd, and it's not exported. @@ -38,47 +37,121 @@ const DefaultSandboxImage = "registry.k8s.io/pause:3.10.1" func List(config config.Config) Versions { var images Versions - images.Etcd = config.Cluster().Etcd().Image() - images.CoreDNS = config.Cluster().CoreDNS().Image() - images.Flannel = fmt.Sprintf("ghcr.io/siderolabs/flannel:%s", constants.FlannelVersion) // mirrored from docker.io/flannelcni/flannel - images.Kubelet = config.Machine().Kubelet().Image() - images.KubeAPIServer = config.Cluster().APIServer().Image() - images.KubeControllerManager = config.Cluster().ControllerManager().Image() - images.KubeProxy = config.Cluster().Proxy().Image() - images.KubeScheduler = config.Cluster().Scheduler().Image() + images.Etcd = mustParseTag(config.Cluster().Etcd().Image()) + images.CoreDNS = mustParseTag(config.Cluster().CoreDNS().Image()) + images.Flannel = mustParseTag(fmt.Sprintf("ghcr.io/siderolabs/flannel:%s", constants.FlannelVersion)) // mirrored from docker.io/flannelcni/flannel + images.Kubelet = mustParseTag(config.Machine().Kubelet().Image()) + images.KubeAPIServer = mustParseTag(config.Cluster().APIServer().Image()) + images.KubeControllerManager = mustParseTag(config.Cluster().ControllerManager().Image()) + images.KubeProxy = mustParseTag(config.Cluster().Proxy().Image()) + images.KubeScheduler = mustParseTag(config.Cluster().Scheduler().Image()) - images.Installer = DefaultInstallerImage - images.Talos = DefaultTalosImage - - images.Pause = DefaultSandboxImage + images.Pause = mustParseTag(DefaultSandboxImage) return images } -// SourceBundle holds the core images (and their versions) that are used to build Talos. -type SourceBundle struct { - Installer string - InstallerBase string - Imager string - Talos string - TalosctlAll string +// VersionsListOptions allows overriding the default component versions +// displayed to the user. +// +// Any non-empty field value replaces the corresponding default version +// when presenting available or selected versions. Fields left empty +// will fall back to their built-in defaults. +type VersionsListOptions struct { + // KubernetesVersion overrides the default Kubernetes version. + KubernetesVersion string - Overlays string - Extensions string + // CoreDNSVersion overrides the default CoreDNS version. + CoreDNSVersion string + + // EtcdVersion overrides the default etcd version. + EtcdVersion string + + // FlannelVersion overrides the default Flannel version. + FlannelVersion string + + // PauseVersion overrides the default pause container image version. + PauseVersion string +} + +// ListWithOptions returns image versions with overrides. +func ListWithOptions(config config.Config, opts VersionsListOptions) Versions { + images := List(config) + + if opts.CoreDNSVersion != "" { + images.CoreDNS = images.CoreDNS.Tag(opts.CoreDNSVersion) + } + + if opts.EtcdVersion != "" { + images.Etcd = images.Etcd.Tag(opts.EtcdVersion) + } + + if opts.FlannelVersion != "" { + images.Flannel = images.Flannel.Tag(opts.FlannelVersion) + } + + if opts.PauseVersion != "" { + images.Pause = images.Pause.Tag(opts.PauseVersion) + } + + if opts.KubernetesVersion != "" { + images.Kubelet = images.Kubelet.Tag(opts.KubernetesVersion) + images.KubeAPIServer = images.KubeAPIServer.Tag(opts.KubernetesVersion) + images.KubeControllerManager = images.KubeControllerManager.Tag(opts.KubernetesVersion) + images.KubeProxy = images.KubeProxy.Tag(opts.KubernetesVersion) + images.KubeScheduler = images.KubeScheduler.Tag(opts.KubernetesVersion) + } + + return images +} + +func mustParseTag(s string) name.Tag { + r, err := name.ParseReference(s) + if err != nil { + panic(err) + } + + t, ok := r.(name.Tag) + if !ok { + panic(fmt.Sprintf("%T is not name.Tag: %#+v", r, r)) + } + + return t +} + +func mustParseReferenceWithTag(ref, tag string) name.Tag { + r, err := name.ParseReference(ref) + if err != nil { + panic(err) + } + + return r.Context().Tag(tag) +} + +// TalosBundle holds the core images (and their versions) that are used to build Talos. +type TalosBundle struct { + Installer name.Tag + InstallerBase name.Tag + Imager name.Tag + Talos name.Tag + TalosctlAll name.Tag + + Overlays name.Tag + Extensions name.Tag } // ListSourcesFor returns source bundle for specific version. -func ListSourcesFor(tag string) SourceBundle { - var bundle SourceBundle +func ListSourcesFor(tag string) TalosBundle { + var bundle TalosBundle - bundle.Installer = DefaultInstallerImageRepository + ":" + tag - bundle.InstallerBase = DefaultInstallerBaseImageRepository + ":" + tag - bundle.Imager = DefaultImagerImageRepository + ":" + tag - bundle.Talos = DefaultTalosImageRepository + ":" + tag - bundle.TalosctlAll = DefaultTalosctlAllImageRepository + ":" + tag + bundle.Installer = mustParseReferenceWithTag(DefaultInstallerImageRepository, tag) + bundle.InstallerBase = mustParseReferenceWithTag(DefaultInstallerBaseImageRepository, tag) + bundle.Imager = mustParseReferenceWithTag(DefaultImagerImageRepository, tag) + bundle.Talos = mustParseReferenceWithTag(DefaultTalosImageRepository, tag) + bundle.TalosctlAll = mustParseReferenceWithTag(DefaultTalosctlAllImageRepository, tag) - bundle.Overlays = DefaultOverlaysManifestRepository + ":" + tag - bundle.Extensions = DefaultExtensionsManifestRepository + ":" + tag + bundle.Overlays = mustParseReferenceWithTag(DefaultOverlaysManifestRepository, tag) + bundle.Extensions = mustParseReferenceWithTag(DefaultExtensionsManifestRepository, tag) return bundle } diff --git a/website/content/v1.13/reference/cli.md b/website/content/v1.13/reference/cli.md index 69ee35b50..2b4589d9b 100644 --- a/website/content/v1.13/reference/cli.md +++ b/website/content/v1.13/reference/cli.md @@ -2215,7 +2215,11 @@ talosctl image k8s-bundle [flags] ### Options ``` - -h, --help help for k8s-bundle + --coredns-version semver CoreDNS semantic version (default v1.13.2) + --etcd-version semver ETCD semantic version (default v3.6.7) + --flannel-version semver Flannel CNI semantic version (default v0.27.4) + -h, --help help for k8s-bundle + --k8s-version semver Kubernetes semantic version (default v1.35.0) ``` ### Options inherited from parent commands