From 08e3870367b1e521413d6d4e8493ea6d41a46d76 Mon Sep 17 00:00:00 2001 From: Jeroen Bruinink Date: Fri, 20 Feb 2026 17:21:55 +0100 Subject: [PATCH] feat(sks): add Karpenter credential rotation and active nodepool template lookup --- CHANGELOG.md | 5 + .../sks/sks_active_nodepool_templates.go | 114 ++++++++++++++++++ .../sks/sks_rotate_karpenter_credentials.go | 73 +++++++++++ 3 files changed, 192 insertions(+) create mode 100644 cmd/compute/sks/sks_active_nodepool_templates.go create mode 100644 cmd/compute/sks/sks_rotate_karpenter_credentials.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a2bfe641..80a964ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Features + +- sks: add `rotate-karpenter-credentials` command +- sks: add `active-nodepool-templates` command + ### Bug fixes - Fix panic when config commands are used without a default account set #798 diff --git a/cmd/compute/sks/sks_active_nodepool_templates.go b/cmd/compute/sks/sks_active_nodepool_templates.go new file mode 100644 index 00000000..634e18ac --- /dev/null +++ b/cmd/compute/sks/sks_active_nodepool_templates.go @@ -0,0 +1,114 @@ +package sks + +import ( + "errors" + "fmt" + "strings" + + "github.com/spf13/cobra" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" + v3 "github.com/exoscale/egoscale/v3" +) + +type sksActiveNodepoolTemplateOutput struct { + KubeVersion string `json:"kube_version"` + Variant string `json:"variant"` + TemplateID v3.UUID `json:"template_id"` + Template string `json:"template"` +} + +type sksActiveNodepoolTemplatesOutput []sksActiveNodepoolTemplateOutput + +func (o *sksActiveNodepoolTemplatesOutput) ToJSON() { output.JSON(o) } +func (o *sksActiveNodepoolTemplatesOutput) ToText() { output.Text(o) } +func (o *sksActiveNodepoolTemplatesOutput) ToTable() { output.Table(o) } + +type sksActiveNodepoolTemplatesCmd struct { + exocmd.CliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"active-nodepool-templates"` + + KubeVersion string `cli-arg:"#" cli-usage:"KUBERNETES-VERSION"` + Variant string `cli-flag:"variant" cli-usage:"nodepool template variant to resolve (standard|nvidia)"` + Zone v3.ZoneName `cli-short:"z" cli-usage:"zone to query in"` +} + +func (c *sksActiveNodepoolTemplatesCmd) CmdAliases() []string { + return []string{"active-nodepool-template"} +} + +func (c *sksActiveNodepoolTemplatesCmd) CmdShort() string { + return "Find active SKS nodepool templates for a Kubernetes version" +} + +func (c *sksActiveNodepoolTemplatesCmd) CmdLong() string { + return fmt.Sprintf(`This command finds active SKS nodepool templates for a given Kubernetes version. + +By default, both "standard" and "nvidia" variants are queried. + +Supported output template annotations: %s`, + strings.Join(output.TemplateAnnotations(&sksActiveNodepoolTemplateOutput{}), ", ")) +} + +func (c *sksActiveNodepoolTemplatesCmd) CmdPreRun(cmd *cobra.Command, args []string) error { + exocmd.CmdSetZoneFlagFromDefault(cmd) + return exocmd.CliCommandDefaultPreRun(c, cmd, args) +} + +func (c *sksActiveNodepoolTemplatesCmd) CmdRun(_ *cobra.Command, _ []string) error { + ctx := exocmd.GContext + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, c.Zone) + if err != nil { + return err + } + + var variants []v3.GetActiveNodepoolTemplateVariant + switch c.Variant { + case "": + variants = []v3.GetActiveNodepoolTemplateVariant{ + v3.GetActiveNodepoolTemplateVariantStandard, + v3.GetActiveNodepoolTemplateVariantNvidia, + } + case string(v3.GetActiveNodepoolTemplateVariantStandard): + variants = []v3.GetActiveNodepoolTemplateVariant{v3.GetActiveNodepoolTemplateVariantStandard} + case string(v3.GetActiveNodepoolTemplateVariantNvidia): + variants = []v3.GetActiveNodepoolTemplateVariant{v3.GetActiveNodepoolTemplateVariantNvidia} + default: + return errors.New(`invalid variant, must be one of: "standard", "nvidia"`) + } + + out := make(sksActiveNodepoolTemplatesOutput, 0, len(variants)) + for _, variant := range variants { + activeTemplate, err := client.GetActiveNodepoolTemplate(ctx, c.KubeVersion, variant) + if err != nil { + return fmt.Errorf("error retrieving active %q nodepool template: %w", variant, err) + } + + if activeTemplate.ActiveTemplate == "" { + return fmt.Errorf("no active template returned for variant %q", variant) + } + + template, err := client.GetTemplate(ctx, activeTemplate.ActiveTemplate) + if err != nil { + return fmt.Errorf("error retrieving template details for %q: %w", activeTemplate.ActiveTemplate, err) + } + + out = append(out, sksActiveNodepoolTemplateOutput{ + KubeVersion: c.KubeVersion, + Variant: string(variant), + TemplateID: template.ID, + Template: template.Name, + }) + } + + return c.OutputFunc(&out, nil) +} + +func init() { + cobra.CheckErr(exocmd.RegisterCLICommand(sksCmd, &sksActiveNodepoolTemplatesCmd{ + CliCommandSettings: exocmd.DefaultCLICmdSettings(), + })) +} diff --git a/cmd/compute/sks/sks_rotate_karpenter_credentials.go b/cmd/compute/sks/sks_rotate_karpenter_credentials.go new file mode 100644 index 00000000..cb056b47 --- /dev/null +++ b/cmd/compute/sks/sks_rotate_karpenter_credentials.go @@ -0,0 +1,73 @@ +package sks + +import ( + "fmt" + + "github.com/spf13/cobra" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/utils" + v3 "github.com/exoscale/egoscale/v3" +) + +type sksRotateKarpenterCredentialsCmd struct { + exocmd.CliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"rotate-karpenter-credentials"` + + Cluster string `cli-arg:"#" cli-usage:"CLUSTER-NAME|ID"` + Zone v3.ZoneName `cli-flag:"zone" cli-short:"z" cli-usage:"SKS cluster zone"` +} + +func (c *sksRotateKarpenterCredentialsCmd) CmdAliases() []string { return nil } + +func (c *sksRotateKarpenterCredentialsCmd) CmdShort() string { + return "Rotate the Exoscale Karpenter IAM credentials for an SKS cluster" +} + +func (c *sksRotateKarpenterCredentialsCmd) CmdLong() string { + return `This command rotates the Exoscale IAM credentials managed by the SKS control +plane for the Kubernetes Karpenter addon. +` +} + +func (c *sksRotateKarpenterCredentialsCmd) CmdPreRun(cmd *cobra.Command, args []string) error { + exocmd.CmdSetZoneFlagFromDefault(cmd) + return exocmd.CliCommandDefaultPreRun(c, cmd, args) +} + +func (c *sksRotateKarpenterCredentialsCmd) CmdRun(_ *cobra.Command, _ []string) error { + ctx := exocmd.GContext + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, c.Zone) + if err != nil { + return err + } + + resp, err := client.ListSKSClusters(ctx) + if err != nil { + return err + } + + cluster, err := resp.FindSKSCluster(c.Cluster) + if err != nil { + return err + } + + op, err := client.RotateSKSKarpenterCredentials(ctx, cluster.ID) + if err != nil { + return err + } + + utils.DecorateAsyncOperation(fmt.Sprintf("Rotating SKS cluster %q Exoscale Karpenter credentials...", c.Cluster), func() { + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) + }) + + return err +} + +func init() { + cobra.CheckErr(exocmd.RegisterCLICommand(sksCmd, &sksRotateKarpenterCredentialsCmd{ + CliCommandSettings: exocmd.DefaultCLICmdSettings(), + })) +}