diff --git a/README.md b/README.md index 1339d26..2bf9d2f 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,10 @@ kind: PatchPlan metadata: name: talos-upgrade-v1.11.6 spec: - targetVersion: "ghcr.io/siderolabs/installer:v1.11.6" + # Target Talos image specification + target: + version: v1.11.6 + source: ghcr # ghcr or factory # Patch workers first, then control plane patchWorkers: true @@ -114,6 +117,50 @@ Apply the PatchPlan: kubectl apply -f patchplan.yaml ``` +**Example using Talos Factory images:** + +The `target` specification uses individual fields to construct the factory image URL. The operator builds the full URL in the format: +``` +factory.talos.dev/{installer}-installer[-secureboot]/{schematicID}:{version} +``` + +Field breakdown: +```yaml +target: + version: v1.11.6 # The Talos version tag + source: factory # Use factory.talos.dev (vs ghcr) + installer: nocloud # The installer type (aws, azure, nocloud, etc.) + schematicID: 95d432d6bb... # The factory schematic hash + secureBoot: true # Adds -secureboot suffix to installer +``` + +Full example: + +```yaml +apiVersion: kangalpatch.ozalp.dk/v1alpha1 +kind: PatchPlan +metadata: + name: talos-upgrade-factory +spec: + target: + version: v1.12.1 + source: factory + installer: aws + schematicID: 376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba + secureBoot: true + + patchWorkers: true + patchControlPlane: true + maxConcurrency: 2 + + talosConfig: + endpoints: + - 10.0.0.10:50000 + secretRef: + name: talos-credentials + namespace: kangal-patch +``` + ### 3. Monitor Progress Watch the upgrade progress: @@ -160,7 +207,12 @@ kubectl patch patchplan simple-upgrade --type merge -p '{"spec":{"paused":false} | Field | Type | Description | Default | |-------|------|-------------|---------| -| `targetVersion` | string | Target Talos version | Required | +| `target` | object | Target Talos image specification | Required | +| `target.version` | string | Talos version (e.g., v1.12.1) | Required | +| `target.source` | string | Image source: "ghcr" or "factory" | `ghcr` | +| `target.installer` | string | Installer type (e.g., "aws", "nocloud"). Required when source=factory | - | +| `target.schematicID` | string | Talos factory schematic ID. Required when source=factory | - | +| `target.secureBoot` | bool | Enable secure boot. Only applicable when source=factory | `false` | | `nodeSelector` | map | Label selector for nodes | `{}` | | `maxConcurrency` | int | Max nodes to patch concurrently | `1` | | `maxFailures` | int | Max allowed failures before stopping | `0` | diff --git a/api/v1alpha1/patchjob_types.go b/api/v1alpha1/patchjob_types.go index e445f19..0a7d56e 100644 --- a/api/v1alpha1/patchjob_types.go +++ b/api/v1alpha1/patchjob_types.go @@ -33,9 +33,9 @@ type PatchJobSpec struct { // +kubebuilder:validation:Required NodeName string `json:"nodeName"` - // TargetVersion is the Talos version to upgrade to + // Target defines the target Talos image specification // +kubebuilder:validation:Required - TargetVersion string `json:"targetVersion"` + Target TargetSpec `json:"target"` // PatchPlanRef references the parent PatchPlan // +optional diff --git a/api/v1alpha1/patchplan_types.go b/api/v1alpha1/patchplan_types.go index 0c2b8a0..6c25e0a 100644 --- a/api/v1alpha1/patchplan_types.go +++ b/api/v1alpha1/patchplan_types.go @@ -20,11 +20,39 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// TargetSpec defines the target Talos image specification +type TargetSpec struct { + // Version is the Talos version (e.g., v1.12.1) + // +kubebuilder:validation:Required + Version string `json:"version"` + + // Source specifies the image source: "factory" or "ghcr" + // +kubebuilder:validation:Enum=factory;ghcr + // +kubebuilder:default=ghcr + Source string `json:"source,omitempty"` + + // Installer specifies the installer type (e.g., "aws", "azure", "nocloud") + // Required when source=factory + // +optional + Installer string `json:"installer,omitempty"` + + // SchematicID is the Talos factory schematic ID + // Required when source=factory + // +optional + SchematicID string `json:"schematicID,omitempty"` + + // SecureBoot enables secure boot for the installer image + // Only applicable when source=factory + // +kubebuilder:default=false + // +optional + SecureBoot bool `json:"secureBoot,omitempty"` +} + // PatchPlanSpec defines the desired state of PatchPlan type PatchPlanSpec struct { - // TargetVersion is the Talos version to upgrade to + // Target defines the target Talos image specification // +kubebuilder:validation:Required - TargetVersion string `json:"targetVersion"` + Target TargetSpec `json:"target"` // NodeSelector selects which nodes to patch (label selector) // +optional @@ -155,7 +183,7 @@ type PatchPlanStatus struct { // +kubebuilder:validation:Enum=Pending;InProgress;Paused;Completed;Failed Phase PatchPhase `json:"phase,omitempty"` - // TargetVersion is the display version extracted from spec.targetVersion + // TargetVersion is the display version extracted from spec.target // +optional TargetVersion string `json:"targetVersion,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 435e159..3198b19 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -134,6 +134,7 @@ func (in *PatchJobList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PatchJobSpec) DeepCopyInto(out *PatchJobSpec) { *out = *in + out.Target = in.Target } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchJobSpec. @@ -238,6 +239,7 @@ func (in *PatchPlanList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PatchPlanSpec) DeepCopyInto(out *PatchPlanSpec) { *out = *in + out.Target = in.Target if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector *out = make(map[string]string, len(*in)) @@ -339,3 +341,18 @@ func (in *TalosConfig) DeepCopy() *TalosConfig { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TargetSpec) DeepCopyInto(out *TargetSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSpec. +func (in *TargetSpec) DeepCopy() *TargetSpec { + if in == nil { + return nil + } + out := new(TargetSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/kangalpatch.ozalp.dk_patchjobs.yaml b/config/crd/bases/kangalpatch.ozalp.dk_patchjobs.yaml index 0b902d1..6adba0a 100644 --- a/config/crd/bases/kangalpatch.ozalp.dk_patchjobs.yaml +++ b/config/crd/bases/kangalpatch.ozalp.dk_patchjobs.yaml @@ -65,12 +65,42 @@ spec: patchPlanRef: description: PatchPlanRef references the parent PatchPlan type: string - targetVersion: - description: TargetVersion is the Talos version to upgrade to - type: string + target: + description: Target defines the target Talos image specification + properties: + installer: + description: |- + Installer specifies the installer type (e.g., "aws", "azure", "nocloud") + Required when source=factory + type: string + schematicID: + description: |- + SchematicID is the Talos factory schematic ID + Required when source=factory + type: string + secureBoot: + default: false + description: |- + SecureBoot enables secure boot for the installer image + Only applicable when source=factory + type: boolean + source: + default: ghcr + description: 'Source specifies the image source: "factory" or + "ghcr"' + enum: + - factory + - ghcr + type: string + version: + description: Version is the Talos version (e.g., v1.12.1) + type: string + required: + - version + type: object required: - nodeName - - targetVersion + - target type: object status: description: PatchJobStatus defines the observed state of PatchJob diff --git a/config/crd/bases/kangalpatch.ozalp.dk_patchplans.yaml b/config/crd/bases/kangalpatch.ozalp.dk_patchplans.yaml index 58294b2..10e51bb 100644 --- a/config/crd/bases/kangalpatch.ozalp.dk_patchplans.yaml +++ b/config/crd/bases/kangalpatch.ozalp.dk_patchplans.yaml @@ -190,11 +190,41 @@ spec: - name type: object type: object - targetVersion: - description: TargetVersion is the Talos version to upgrade to - type: string + target: + description: Target defines the target Talos image specification + properties: + installer: + description: |- + Installer specifies the installer type (e.g., "aws", "azure", "nocloud") + Required when source=factory + type: string + schematicID: + description: |- + SchematicID is the Talos factory schematic ID + Required when source=factory + type: string + secureBoot: + default: false + description: |- + SecureBoot enables secure boot for the installer image + Only applicable when source=factory + type: boolean + source: + default: ghcr + description: 'Source specifies the image source: "factory" or + "ghcr"' + enum: + - factory + - ghcr + type: string + version: + description: Version is the Talos version (e.g., v1.12.1) + type: string + required: + - version + type: object required: - - targetVersion + - target type: object status: description: PatchPlanStatus defines the observed state of PatchPlan @@ -295,7 +325,7 @@ spec: format: date-time type: string targetVersion: - description: TargetVersion is the display version extracted from spec.targetVersion + description: TargetVersion is the display version extracted from spec.target type: string totalNodes: description: TotalNodes is the total number of nodes selected for diff --git a/config/samples/controlplane-only.yaml b/config/samples/controlplane-only.yaml index 334e52c..81dcfb0 100644 --- a/config/samples/controlplane-only.yaml +++ b/config/samples/controlplane-only.yaml @@ -4,7 +4,12 @@ metadata: name: controlplane-only spec: # Target version - targetVersion: "factory.talos.dev/nocloud-installer-secureboot/95d432d6bb450a67e801a6ae77c96a67e38820b62ba4159ae7e997e1695207f7:v1.11.6" + target: + version: v1.11.6 + source: factory + installer: nocloud + schematicID: 95d432d6bb450a67e801a6ae77c96a67e38820b62ba4159ae7e997e1695207f7 + secureBoot: true # Only patch control plane nodes patchControlPlane: true diff --git a/config/samples/maintenance-window.yaml b/config/samples/maintenance-window.yaml index 54487d3..6b349ec 100644 --- a/config/samples/maintenance-window.yaml +++ b/config/samples/maintenance-window.yaml @@ -4,7 +4,12 @@ metadata: name: simple-upgrade-maintenance spec: # Target Talos version to upgrade to - targetVersion: "factory.talos.dev/nocloud-installer-secureboot/95d432d6bb450a67e801a6ae77c96a67e38820b62ba4159ae7e997e1695207f7:v1.11.6" + target: + version: v1.11.6 + source: factory + installer: nocloud + schematicID: 95d432d6bb450a67e801a6ae77c96a67e38820b62ba4159ae7e997e1695207f7 + secureBoot: true # Node selection nodeSelector: diff --git a/config/samples/simple-upgrade.yaml b/config/samples/simple-upgrade.yaml index a778029..d7f7d7f 100644 --- a/config/samples/simple-upgrade.yaml +++ b/config/samples/simple-upgrade.yaml @@ -4,7 +4,12 @@ metadata: name: simple-upgrade spec: # Target Talos version to upgrade to - targetVersion: "factory.talos.dev/nocloud-installer-secureboot/95d432d6bb450a67e801a6ae77c96a67e38820b62ba4159ae7e997e1695207f7:v1.11.6" + target: + version: v1.11.6 + source: factory + installer: nocloud + schematicID: 95d432d6bb450a67e801a6ae77c96a67e38820b62ba4159ae7e997e1695207f7 + secureBoot: true # Node selection nodeSelector: diff --git a/controllers/patchjob_controller.go b/controllers/patchjob_controller.go index 0243015..92937f6 100644 --- a/controllers/patchjob_controller.go +++ b/controllers/patchjob_controller.go @@ -126,7 +126,7 @@ func (r *PatchJobReconciler) initJob(ctx context.Context, patchJob *patchv1alpha return r.failJob(ctx, original, patchJob, "failed to get current version", err) } - targetVersion := patchutil.ExtractVersion(patchJob.Spec.TargetVersion) + targetVersion := patchJob.Spec.Target.Version // Check if already at target version if currentVersion == targetVersion { @@ -347,8 +347,14 @@ func (r *PatchJobReconciler) startUpgrade(ctx context.Context, patchJob *patchv1 } defer talosClient.Close() + // Build the installer image URL + installerImage, err := patchutil.BuildInstallerImage(patchJob.Spec.Target) + if err != nil { + return r.failJob(ctx, original, patchJob, "failed to build installer image", err) + } + // Initiate upgrade - if err := talosClient.Upgrade(ctx, patchJob.Spec.NodeName, patchJob.Spec.TargetVersion); err != nil { + if err := talosClient.Upgrade(ctx, patchJob.Spec.NodeName, installerImage); err != nil { return r.failJob(ctx, original, patchJob, "failed to start upgrade", err) } diff --git a/controllers/patchplan_controller.go b/controllers/patchplan_controller.go index e1123e8..1458eed 100644 --- a/controllers/patchplan_controller.go +++ b/controllers/patchplan_controller.go @@ -109,7 +109,7 @@ func (r *PatchPlanReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( // Update status counts and total nodes original := patchPlan.DeepCopy() patchPlan.Status.TotalNodes = len(targetNodes) - patchPlan.Status.TargetVersion = patchutil.ExtractVersion(patchPlan.Spec.TargetVersion) + patchPlan.Status.TargetVersion = patchPlan.Spec.Target.Version patchPlan.Status.CompletedNodes = jobSummary.Completed patchPlan.Status.FailedNodes = jobSummary.Failed @@ -400,9 +400,9 @@ func (r *PatchPlanReconciler) createPatchJob(ctx context.Context, patchPlan *pat }, }, Spec: patchv1alpha1.PatchJobSpec{ - NodeName: node.Name, - TargetVersion: patchPlan.Spec.TargetVersion, - PatchPlanRef: patchPlan.Name, + NodeName: node.Name, + Target: patchPlan.Spec.Target, + PatchPlanRef: patchPlan.Name, }, } diff --git a/internal/patchutil/version.go b/internal/patchutil/version.go index 21b60d2..a6c487b 100644 --- a/internal/patchutil/version.go +++ b/internal/patchutil/version.go @@ -17,17 +17,38 @@ limitations under the License. package patchutil import ( - "strings" + "fmt" + + patchv1alpha1 "github.com/uozalp/kangal-patch/api/v1alpha1" ) -// ExtractVersion extracts the version tag from a Talos image reference. -func ExtractVersion(imageRef string) string { - // Split by colon to get the tag part - parts := strings.Split(imageRef, ":") - if len(parts) < 2 { - return "" +// BuildInstallerImage constructs the full Talos installer image reference from a TargetSpec. +func BuildInstallerImage(t patchv1alpha1.TargetSpec) (string, error) { + if t.Source == "ghcr" || t.Source == "" { + return fmt.Sprintf( + "ghcr.io/siderolabs/installer:%s", + t.Version, + ), nil + } + + if t.SchematicID == "" { + return "", fmt.Errorf("schematicID is required when source=factory") + } + + if t.Installer == "" { + return "", fmt.Errorf("installer is required when source=factory") + } + + suffix := "" + if t.SecureBoot { + suffix = "-secureboot" } - // Return the last part (version tag) - return parts[len(parts)-1] + return fmt.Sprintf( + "factory.talos.dev/%s-installer%s/%s:%s", + t.Installer, + suffix, + t.SchematicID, + t.Version, + ), nil }