Skip to content

fix(backup): refactor backups .status.underlyingResources#2319

Merged
androndo merged 1 commit intomainfrom
fix/backups-abstract-app-specific-status
Apr 1, 2026
Merged

fix(backup): refactor backups .status.underlyingResources#2319
androndo merged 1 commit intomainfrom
fix/backups-abstract-app-specific-status

Conversation

@androndo
Copy link
Copy Markdown
Contributor

@androndo androndo commented Apr 1, 2026

What this PR does

Release note

- refactor backups status underlying resources structure

now it looks like:

    underlyingResources:
      kind: VMInstance
      apiVersion: apps.cozystack.io/v1alpha1
      dataVolumes:
        - dataVolumeName: vm-disk-ubuntu-source
          applicationName: ubuntu-source
      ip: "10.244.2.98"
      mac: "9e:92:c9:0c:10:9b"

Summary by CodeRabbit

  • Refactor
    • Backup status now stores underlying resources as a generic, opaque JSON object instead of a fixed typed structure.
    • CRD schema and API behavior now preserve unknown fields, allowing arbitrary application-specific metadata to be retained in that payload.
    • Backup/restore flows were updated to consistently read, forward and derive VM-related metadata from the opaque payload end-to-end.

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Apr 1, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

Replaced the typed UnderlyingResources struct with an opaque *runtime.RawExtension in Backup.status.underlyingResources. Removed autogenerated deepcopy methods for the removed struct. Controller logic was updated to marshal/unmarshal VM-specific resource blobs and propagate the opaque payload through backup and restore paths.

Changes

Cohort / File(s) Summary
API Types
api/backups/v1alpha1/backup_types.go
Removed exported UnderlyingResources struct; changed BackupStatus.UnderlyingResources from *UnderlyingResources to *runtime.RawExtension. Added kubebuilder markers to preserve unknown fields and validate as a generic JSON object; updated field docs to describe a self-typed payload including TypeMeta.
Autogenerated Deepcopy
api/backups/v1alpha1/zz_generated.deepcopy.go
Removed deepcopy methods for UnderlyingResources; updated BackupStatus.DeepCopyInto to allocate and deepcopy a runtime.RawExtension instead of the removed typed struct.
Controller Logic
internal/backupcontroller/velerostrategy_controller.go
Introduced opaque VM-specific representation (vmInstanceResources) and helpers marshalUnderlyingResources / getVMInstanceResources. Switched collection, Velero annotation storage, restore resolution, and resource-modifier creation to use *runtime.RawExtension; updated function signatures and call sites accordingly.
CRD Schema
packages/system/backup-controller/definitions/backups.cozystack.io_backups.yaml
Changed status.underlyingResources schema from a fixed object (explicit dataVolumes, ip, mac) to a generic type: object with x-kubernetes-preserve-unknown-fields: true, making the field an opaque, self-typed payload.

Sequence Diagram

sequenceDiagram
    participant Controller as BackupJobReconciler
    participant BackupAPI as BackupStatus<br/>(API Object)
    participant Serializer as Runtime<br/>Serialization
    participant Velero as Velero<br/>Backup System

    rect rgba(100,150,200,0.5)
    Note over Controller,Velero: Backup Phase
    Controller->>Controller: collectUnderlyingResources()
    Controller->>Controller: build vmInstanceResources (DataVolumes, IP, MAC) + TypeMeta
    Controller->>Serializer: marshalUnderlyingResources() -> JSON
    Serializer-->>Controller: runtime.RawExtension (opaque)
    Controller->>BackupAPI: store UnderlyingResources = RawExtension
    Controller->>Velero: create Velero Backup (annotation = RawExtension JSON)
    end

    rect rgba(150,200,150,0.5)
    Note over Controller,Velero: Restore Phase
    Controller->>BackupAPI: read Backup.status.UnderlyingResources (RawExtension)
    Controller->>Serializer: getVMInstanceResources(RawExtension) -> vmInstanceResources
    Serializer-->>Controller: VM fields (DataVolumes, IP, MAC)
    Controller->>Controller: createResourceModifiersConfigMap(..., ur)
    Controller->>Velero: create Velero Restore (resource modifiers)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped from structs to a JSON stream,
An opaque blob now carries every VM dream,
Kind and apiVersion tucked in tight,
Marshaled bytes travel through day and night,
I nibble bugs away and watch restores take flight. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(backup): refactor backups .status.underlyingResources' directly and specifically describes the main change: refactoring the underlyingResources field in backup status.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/backups-abstract-app-specific-status

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the UnderlyingResources field in the BackupStatus from a fixed struct to a generic map of runtime.RawExtension. This change enables the backup system to store kind-specific metadata in a flexible, opaque format, with the CRD updated to preserve unknown fields. The controllers have been updated to use new helper functions for marshaling and unmarshaling these resources. Feedback was provided regarding the getVMInstanceResources helper, which currently swallows unmarshaling errors, potentially leading to silent failures during backup or restore operations.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/backupcontroller/velerostrategy_controller.go`:
- Around line 1050-1063: Call resolveUnderlyingResourcesForRestore(...) early
(before any restore-side consumers) and thread the returned ur through the
restore flow instead of reading backup.Status.UnderlyingResources directly;
update prepareForRestore(...) and createResourceModifiersConfigMap(...) to
accept the resolved ur parameter and switch their internal reads from
backup.Status.UnderlyingResources to use the passed ur, and update all callers
to pass the resolved ur so VMDisk/PVC/DataVolume prep (and the OVN patch path)
runs even when backup.Status.UnderlyingResources was pruned.
- Around line 97-111: getVMInstanceResources currently only looks for the
vmInstanceKind key and returns nil for legacy backups that store a flat map
(dataVolumes/ip/mac); update it to detect the old shape by checking for keys
"dataVolumes", "ip" or "mac" in the provided map and, when present, unmarshal
that flat payload into vmInstanceResources as a compatibility path before
returning. Specifically, inside getVMInstanceResources inspect ur for
vmInstanceKind as before, and if not found iterate/check ur for raw entries that
contain "dataVolumes"|"ip"|"mac" (or attempt unmarshalling into an intermediate
map[string]json.RawMessage) and convert/merge those fields into the
vmInstanceResources struct, returning the populated *vmInstanceResources; keep
existing behavior for normal vmInstanceKind handling and for error cases return
nil as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77fe8775-831d-48c9-a5a2-a3ef0ddda3c1

📥 Commits

Reviewing files that changed from the base of the PR and between b4f760c and 1e75531.

📒 Files selected for processing (4)
  • api/backups/v1alpha1/backup_types.go
  • api/backups/v1alpha1/zz_generated.deepcopy.go
  • internal/backupcontroller/velerostrategy_controller.go
  • packages/system/backup-controller/definitions/backups.cozystack.io_backups.yaml

@lllamnyp
Copy link
Copy Markdown
Member

lllamnyp commented Apr 1, 2026

Suggestion: simplify UnderlyingResources from map[string]runtime.RawExtension to *runtime.RawExtension

Looking at the changes in api/backups/v1alpha1/backup_types.go and how UnderlyingResources is consumed in internal/backupcontroller/velerostrategy_controller.go, the map-based design seems like unnecessary indirection. A single *runtime.RawExtension with an inlined TypeMeta would be cleaner.

Current design

UnderlyingResources map[string]runtime.RawExtension `json:"underlyingResources,omitempty"`
  • The map key is always applicationRef.Kind — which is already available in spec.applicationRef.kind. This makes the key redundant (a third copy of the same information).
  • In practice there is only ever one entry in the map (vmInstanceKind). A map-of-one is unnecessary indirection.
  • Consumer code (getVMInstanceResources) does ur[vmInstanceKind] — a stringly-typed dispatch on a key that duplicates information already present elsewhere in the object.

Proposed design

// +optional
// +kubebuilder:pruning:PreserveUnknownFields
UnderlyingResources *runtime.RawExtension `json:"underlyingResources,omitempty"`

The JSON payload would carry its own type information via an inlined TypeMeta:

{
  "kind": "VMInstance",
  "apiVersion": "apps.cozystack.io/v1alpha1",
  "dataVolumes": [...],
  "ip": "10.0.0.5",
  "mac": "00:11:22:33:44:55"
}

Consumer code would first peek at the type, then unmarshal into the correct struct:

// Step 1: peek at the type
type typedResource struct {
    metav1.TypeMeta `json:",inline"`
}
var tr typedResource
json.Unmarshal(backup.Status.UnderlyingResources.Raw, &tr)

// Step 2: dispatch on tr.Kind
switch tr.Kind {
case "VMInstance":
    var res vmInstanceResources
    json.Unmarshal(backup.Status.UnderlyingResources.Raw, &res)
    // ...
}

Advantages

  1. No redundancy — the kind is self-described in the payload and already on spec.applicationRef.kind. The map key was a third copy of the same information.
  2. Simpler schema — a nullable opaque blob is easier to reason about (and to validate / document) than a map of opaque blobs.
  3. Extensible — adding a new application kind just means defining a new struct with TypeMeta embedded; no map-key convention to document or enforce.
  4. Kubernetes-idiomatic — this mirrors how runtime.RawExtension is used elsewhere in the K8s ecosystem (e.g., AdmissionReview.request.object, webhook configurations, etc.).

Trade-off to consider

The map technically allows multiple underlying resource kinds per backup. However, applicationRef already points to a single kind, and collectUnderlyingResources only ever produces one entry, so this capability is unused. If composite apps ever need multiple resource kinds, a []runtime.RawExtension list (each element self-typed) would be a better fit than a map[string]runtime.RawExtension anyway.

Impact on marshalUnderlyingResources / getVMInstanceResources

Both helpers become simpler — marshalUnderlyingResources just serializes the struct directly (with TypeMeta set), and getVMInstanceResources peeks at the kind before unmarshaling. The fallback path in resolveUnderlyingResourcesForRestore (annotation-based) would also be simplified since the annotation would store a single JSON object instead of a map.

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Apr 1, 2026
@androndo androndo enabled auto-merge April 1, 2026 17:04
@androndo androndo disabled auto-merge April 1, 2026 17:19
@androndo androndo force-pushed the fix/backups-abstract-app-specific-status branch from 0a1972d to 16223dc Compare April 1, 2026 17:40
@androndo androndo enabled auto-merge April 1, 2026 17:40
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/backupcontroller/velerostrategy_controller.go`:
- Around line 78-93: The marshaled vmInstanceResources must include Kubernetes
TypeMeta and decoding must validate Kind: update the vmInstanceResources struct
to embed metav1.TypeMeta, modify marshalUnderlyingResources (or a vm-specific
wrapper used there) to set TypeMeta.Kind = vmInstanceKind and
TypeMeta.APIVersion to the appropriate group/version before JSON marshalling,
and update getVMInstanceResources to check the decoded object's Kind equals
vmInstanceKind (return an error if it mismatches) while preserving backward
compatibility by accepting and decoding payloads that lack a Kind field as a
legacy fallback; use the vmInstanceResources type and vmInstanceKind constant to
locate the affected code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 13c886c8-1abd-44dd-9eb9-d83e7ad09376

📥 Commits

Reviewing files that changed from the base of the PR and between 0a1972d and 16223dc.

📒 Files selected for processing (4)
  • api/backups/v1alpha1/backup_types.go
  • api/backups/v1alpha1/zz_generated.deepcopy.go
  • internal/backupcontroller/velerostrategy_controller.go
  • packages/system/backup-controller/definitions/backups.cozystack.io_backups.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/system/backup-controller/definitions/backups.cozystack.io_backups.yaml

Comment on lines +78 to +93
// vmInstanceResources contains VM-specific underlying resources discovered during backup.
type vmInstanceResources struct {
DataVolumes []backupsv1alpha1.DataVolumeResource `json:"dataVolumes,omitempty"`
IP string `json:"ip,omitempty"`
MAC string `json:"mac,omitempty"`
}

// marshalUnderlyingResources serializes application-specific data into a
// runtime.RawExtension suitable for Backup.Status.UnderlyingResources.
func marshalUnderlyingResources(data interface{}) (*runtime.RawExtension, error) {
raw, err := json.Marshal(data)
if err != nil {
return nil, err
}
return &runtime.RawExtension{Raw: raw}, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Get the file and examine the specific lines mentioned
cat -n internal/backupcontroller/velerostrategy_controller.go | sed -n '78,110p'

Repository: cozystack/cozystack

Length of output: 1614


🏁 Script executed:

# Check the API documentation for the underlyingResources field
find . -name "backup_types.go" | grep -E "api.*v1alpha1"

Repository: cozystack/cozystack

Length of output: 101


🏁 Script executed:

# Search for getVMInstanceResources and its implementation
rg "getVMInstanceResources" -A 20 internal/

Repository: cozystack/cozystack

Length of output: 10038


🏁 Script executed:

# Verify the marshal call context around line 320-324
cat -n internal/backupcontroller/velerostrategy_controller.go | sed -n '315,330p'

Repository: cozystack/cozystack

Length of output: 660


🏁 Script executed:

# Check the API documentation for underlyingResources
cat -n api/backups/v1alpha1/backup_types.go | head -200

Repository: cozystack/cozystack

Length of output: 5644


🏁 Script executed:

# Search for underlyingResources documentation
rg "underlyingResources" -B 5 -A 5 api/backups/v1alpha1/backup_types.go

Repository: cozystack/cozystack

Length of output: 548


🏁 Script executed:

# Check the larger context to understand where vmInstanceResources is created
cat -n internal/backupcontroller/velerostrategy_controller.go | sed -n '260,325p'

Repository: cozystack/cozystack

Length of output: 2560


🏁 Script executed:

# Check if there's any Kind being set for vmInstanceResources anywhere
rg "vmInstanceKind" internal/backupcontroller/velerostrategy_controller.go

Repository: cozystack/cozystack

Length of output: 196


🏁 Script executed:

# Verify there are no other places where vmInstanceResources is marshaled
rg "vmInstanceResources\{" internal/backupcontroller/velerostrategy_controller.go

Repository: cozystack/cozystack

Length of output: 118


🏁 Script executed:

# Check if there are constants for vmInstanceKind that could be used
rg "vmInstanceKind|vmDiskAppKind" internal/backupcontroller/velerostrategy_controller.go | head -20

Repository: cozystack/cozystack

Length of output: 298


Add TypeMeta to the marshaled vmInstanceResources blob to match the documented contract.

The API documentation for UnderlyingResources (backup_types.go) explicitly states it should be "a self-typed JSON object carrying an inlined TypeMeta (kind/apiVersion) so the consuming controller can dispatch on the application kind." However, the current implementation marshals a flat vmInstanceResources struct without metav1.TypeMeta, violating this contract. Additionally, getVMInstanceResources does not validate Kind from the blob and instead delegates to caller-side responsibility, which prevents safe dispatch and creates a risk that future non-VM payloads with overlapping fields could be misdecoded.

Embed metav1.TypeMeta in vmInstanceResources, populate Kind and APIVersion during marshaling using the available vmInstanceKind constant, and validate the Kind on decode. Preserve backward compatibility by accepting payloads without Kind as a legacy fallback.

🔧 Possible fix
 type vmInstanceResources struct {
+	metav1.TypeMeta `json:",inline"`
 	DataVolumes []backupsv1alpha1.DataVolumeResource `json:"dataVolumes,omitempty"`
 	IP          string                                `json:"ip,omitempty"`
 	MAC         string                                `json:"mac,omitempty"`
 }

 func getVMInstanceResources(ur *runtime.RawExtension) *vmInstanceResources {
 	if ur == nil || len(ur.Raw) == 0 {
 		return nil
 	}
+	var tm metav1.TypeMeta
+	if err := json.Unmarshal(ur.Raw, &tm); err != nil {
+		return nil
+	}
+	if tm.Kind != "" && tm.Kind != vmInstanceKind {
+		return nil
+	}
 	var res vmInstanceResources
 	if err := json.Unmarshal(ur.Raw, &res); err != nil {
 		return nil
 	}
 	if len(res.DataVolumes) == 0 && res.IP == "" && res.MAC == "" {
 		return nil
 	}
 	return &res
 }

 	return marshalUnderlyingResources(vmInstanceResources{
+		TypeMeta: metav1.TypeMeta{
+			Kind: vmInstanceKind,
+		},
 		DataVolumes: dataVolumes,
 		IP:          ip,
 		MAC:         mac,
 	})

Also applies to: 95–110, 320–324

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/backupcontroller/velerostrategy_controller.go` around lines 78 - 93,
The marshaled vmInstanceResources must include Kubernetes TypeMeta and decoding
must validate Kind: update the vmInstanceResources struct to embed
metav1.TypeMeta, modify marshalUnderlyingResources (or a vm-specific wrapper
used there) to set TypeMeta.Kind = vmInstanceKind and TypeMeta.APIVersion to the
appropriate group/version before JSON marshalling, and update
getVMInstanceResources to check the decoded object's Kind equals vmInstanceKind
(return an error if it mismatches) while preserving backward compatibility by
accepting and decoding payloads that lack a Kind field as a legacy fallback; use
the vmInstanceResources type and vmInstanceKind constant to locate the affected
code.

@androndo androndo merged commit 2bfdd01 into main Apr 1, 2026
7 checks passed
@androndo androndo deleted the fix/backups-abstract-app-specific-status branch April 1, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants