Compare commits

...

69 Commits

Author SHA1 Message Date
Timofei Larkin
e63ed41fee [backups] Demo, DO NOT MERGE
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 20:07:27 +03:00
Andrey Kolkov
5b613836ee [backups] add restore jobs controller
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
(cherry picked from commit 5e15a75d89)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:17:26 +03:00
Timofei Larkin
16d82b13ad Velero backup controller impl (#1762)
## What this PR does

Implement a controller for BackupJobs referencing a Velero strategy. Creates a Backup.velero.io according to the template in the `Velero.strategy.backups.cozystack.io`.

### Release note

```release-note
[backups] Implement a backup strategy controller for Velero strategies.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
Co-authored-by: Andrey Kolkov <androndo@gmail.com>
Co-authored-by: Timofei Larkin <lllamnyp@gmail.com>
(cherry picked from commit 069a3ca9b0)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:12:39 +03:00
Andrey Kolkov
deb8d22bc8 [backups] Scaffold a Velero strategy (#1742)
## What this PR does

This patch adds boilerplate code for a Velero strategy for the backups
API. Implementation can be added into the Reconcile function in a
subsequent PR.

###

```release-note
[backups] Add the boilerplate for a new Velero strategy for backups.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Velero as a supported backup strategy, including a
cluster-scoped Velero resource type and CRD for creating Velero-backed
strategies.
* Backup job controller updated to recognize and dispatch Velero-backed
jobs, with status conditions exposed for monitoring progress and health.
* Introduced basic reconciliation flow for Velero strategy to enable
future behavior and observability.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

(cherry picked from commit 715509f57e)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:09:32 +03:00
Timofei Larkin
6cd06ced9a [backups,dashboard] User-facing UI (#1737)
## What this PR does

Add representation for backup Plans, BackupJobs and Backups in the dashboard and a readme
describing, how to add further resources to the dashboard.

### Release note

```release-note
[backups,dashboard] Add backup Plans, BackupJobs, and Backups to the dashboard.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a Backups section in the dashboard navigation with Plans,
BackupJobs, and Backups.
* Added detailed views for Plan, BackupJob, and Backup showing metadata,
references, status, timings, and artifacts.
* Enhanced list views with custom columns (application refs,
phase/status, created/taken timestamps).

* **Documentation**
* Added a Dashboard integration guide documenting how to add resources,
list/detail wiring, and verification steps.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

(cherry picked from commit 67c526582a)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:09:18 +03:00
Timofei Larkin
64b7984e51 [backups] Stub the Job backup strategy controller (#1720)
## What this PR does

This PR introduces a bare-bones Job backup strategy API type and stubs
out all the boilerplate for a new controller that will handle this
strategy, as well as any others in the `strategy.backups.cozystack.io`
API group.

### Release note

```release-note
[backups] Create stubs and minimal implmentations for controllers for
the strategy.backups.cozystack.io API group.
```

(cherry picked from commit 1f0b5ff9ac)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:09:03 +03:00
Timofei Larkin
f214f2983b [backups] Add indices to core backup resources (#1719)
## What this PR does

This adds custom indexable fields on core backup resources to enable
filtering by backed-up application. This will later be useful for
displaying backup resources in the dashboard: it will be possible to
filter them with a field selector to display only those backup resources
that are relevant to a given application.

### Release note

```release-note
[backups] Enable filtering backup resources by backed up application for
per-app views of backups.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Introduced new Backup, BackupJob, Plan, and RestoreJob API resources
for enhanced backup management.
* Added selectable fields to backup resources, enabling efficient
filtering and querying by application reference.
* Added status subresource with conditions to Plan resources for
improved status tracking and observability.

* **Chores**
  * Updated repository metadata configuration.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

(cherry picked from commit 1ec14d6bd6)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:08:48 +03:00
Timofei Larkin
1ca75ee10d [backups] Fix malformed glob and split in template (#1708)
## What this PR does

A malformed glob ("*" instead of "*.yaml") captured the .gitattributes
file and sent it to the templating engine. Using `split` instead of
`splitList` on a string returned a map instead of a list, so templating
broke on `mustLast`. This patch corrects the errors.

### Release note

```release-note
[backups] Fix template-breaking errors in the backup-controller Helm
chart.
```

(cherry picked from commit 4f5ae287f5)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:08:16 +03:00
Timofei Larkin
8dc1d53874 [backups] Scaffold a backup strategy API group (#1687)
## What this PR does

This patch adds the boilerplate for the `strategy.backups.cozystack.io`
API group that will contain reference backup/restore strategy
implementations that will live in Cozystack core.

### Release note

```release-note
    [backups] Scaffold the `strategy.backups.cozystack.io` API group to
    provide a well-defined point for adding reference strategy
    implementations.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced v1alpha1 backup strategy API group registration, enabling
support for backup strategy resources within the system.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

(cherry picked from commit 03a71eb8de)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:07:58 +03:00
Timofei Larkin
789aca4c84 [backups] Build and deploy backup controller (#1685)
## What this PR does

This patch adds compilation and docker build steps for the backup
controller as well as adding a Helm chart to deploy it as part of the
PaaS bundles.

### Release note

```release-note
[backups] Build and deploy backup controller
```

(cherry picked from commit 89897914fa)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:07:37 +03:00
Timofei Larkin
8b7bc9c1e1 [backups] Implement core backup Plan controller (#1640)
## What this PR does

This patch creates a new backups.cozystack.io API group which includes
backup-related resources owned exclusively by Cozystack core. A
cronjob-like controller is implemented to create backup jobs that will
be handled by appropriate third-party or external controllers.

### Release note

```release-note
[backups] Implement the core backup API and an accompanying controller.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Adds a full backups system: create scheduled Plans (cron), run
BackupJobs with lifecycle phases (Pending→Running→Succeeded/Failed), and
produce Backup artifacts with metadata.
* Adds RestoreJobs to restore from stored Backups and track restore
progress/status.
* Exposes new API resources (Plan, BackupJob, Backup, RestoreJob) so
backups and restores can be declared and observed via the platform.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

(cherry picked from commit 67ecf3d0f6)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-08 19:07:02 +03:00
Nikita
17966bfba1 [Backport release-0.39] [seaweedfs] Traffic locality (#1830)
# Description
Backport of #1748 to `release-0.39`.
2026-01-08 15:57:31 +03:00
nbykov0
a9de6bf07e [seaweed] Use upstream images
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
(cherry picked from commit bcd4b8976a)
2026-01-08 12:56:44 +00:00
nbykov0
765cffdc66 [seaweed] Update to 4.05
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
(cherry picked from commit aede1b9217)
2026-01-08 12:56:44 +00:00
nbykov0
48ebe418b2 [seaweedfs] Traffic locality for seaweedfs
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
(cherry picked from commit 2a8a8a480f)
2026-01-08 12:56:44 +00:00
nbykov0
25f31022f0 [ingress] Add topology anti-affinities
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
(cherry picked from commit 6ca8011dfa)
2026-01-08 12:56:44 +00:00
Andrei Kvapil
5be383610f [Backport release-0.39] [api, lineage] Tolerate all taints (#1827)
# Description
Backport of #1781 to `release-0.39`.
2026-01-07 16:45:11 +01:00
nbykov0
363c8cf066 [cozystack-api] Tolerate all taints
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
(cherry picked from commit 4e602fd55d)
2026-01-07 15:02:50 +00:00
nbykov0
8c8ab1f1a0 [lineage-webhook] Tolerate all taints
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
(cherry picked from commit aa66b8c0d3)
2026-01-07 15:02:50 +00:00
Andrei Kvapil
9405e05185 [Backport release-0.39] fix(linstor): prevent orphaned DRBD devices during toggle-disk retry (#1825)
# Description
Backport of #1823 to `release-0.39`.
2026-01-07 15:13:25 +01:00
Andrei Kvapil
0aee5b4dfa fix(linstor): prevent orphaned DRBD devices during toggle-disk retry
The previous retry logic in toggle-disk removed layer data from controller DB
and recreated it. However, removeLayerData() only deletes from the database
without calling drbdadm down on the satellite, leaving orphaned DRBD devices
in the kernel that occupy ports and block new operations.

This fix changes retry to simply repeat the operation with existing layer data,
allowing the satellite to handle it idempotently.

Upstream: https://github.com/LINBIT/linstor-server/pull/475

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 8151e1e41a)
2026-01-07 14:00:22 +00:00
Andrei Kvapil
8373a8cb3f [Backport release-0.39] [linstor] Build linstor-server with custom patches (#1818)
# Description
Backport of #1726 to `release-0.39`.
2026-01-06 16:21:22 +01:00
Andrei Kvapil
cb0e457dda feat(linstor): build linstor-server with custom patches
Add patches for piraeus-server:
- adjust-on-resfile-change: handle resource file changes
- allow-toggle-disk-retry: enable disk operation retries
- force-metadata-check-on-disk-add: verify metadata on disk addition

Update plunger-satellite script and values.yaml for new build.

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit daa91fd2f3)
2026-01-06 15:19:26 +00:00
Andrei Kvapil
c74b88ce66 [Backport release-0.39] [testing] Add aliases and autocomplete (#1809)
# Description
Backport of #1803 to `release-0.39`.
2026-01-05 20:04:26 +01:00
Timofei Larkin
07982b2646 [testing] Add aliases and autocomplete
## What this PR does

Adds a `k=kubectl` alias and bash completion for kubectl to the
e2e-testing sandbox container to maintainers have an easier time
exec'ing into the CI container when something needs to be debugged.

### Release note

```release-note
[testing] Add k=kubectl alias and enable kubectl completion in the CI
container.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
(cherry picked from commit bf1928c96f)
2026-01-05 18:48:14 +00:00
Andrei Kvapil
d80ea29dfa [Backport release-0.39] [kubernetes] Fix endpoints for cilium-gateway (#1808)
# Description
Backport of #1729 to `release-0.39`.
2026-01-05 18:12:34 +01:00
Andrei Kvapil
143da399ff [kubernetes] Fix endpoints for cilium-gateway
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit f59665208c)
2026-01-05 17:11:56 +00:00
Timofei Larkin
ad3bff6692 [kubeovn] Package from external repo (#1535)
Previously, all system charts have had their versions bumped with every
update of Cozystack, even if the underlying images and templates did not
change. Additionally, some components would have their docker images
rebuilt and get updated digests because of a changed docker context,
even though no relevant code had been changed. This would cause an
upgrade of several HelmReleases on every upgrade of Cozystack, even when
in practice nothing had changed. This is especially noticeable for
charts, such as kubeovn, which is a dependency for almost all other
system HelmReleases. By moving kubeovn out of the monorepo and
decoupling its release cycle from the release cycle of Cozystack, this
issue can be resolved and is another step towards decomposing Cozystack.

```release-note
[kubeovn] Decouple the release cycle of kubeovn from Cozystack's release
cycle, preventing unnecessary redeployments of all Helm releases, when
upgrading Cozystack.
```
2026-01-05 18:05:50 +01:00
Andrei Kvapil
f1852ca841 [Backport release-0.39] [kubevirt-operator] Revert incorrect case change in VM alerts (#1806)
# Description
Backport of #1804 to `release-0.39`.
2026-01-05 16:29:11 +01:00
Aleksei Sviridkin
5914515371 [kubevirt-operator] Revert incorrect case change in VM alerts
Revert PR #1770 which incorrectly changed status check from lowercase
to uppercase. The actual metrics use lowercase:
- kubevirt_vm_info uses status="running" (not "Running")
- kubevirt_vmi_info uses phase="running" (not "Running")

Verified by querying virt-controller metrics in instories cluster.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 36836fd84e)
2026-01-05 15:19:59 +00:00
Andrei Kvapil
5be4fc8f40 Release v0.39.2 (#1789)
This PR prepares the release `v0.39.2`.
2026-01-03 09:57:25 +01:00
cozystack-bot
f4ff44a18c Prepare release v0.39.2
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-01-03 01:38:23 +00:00
Andrei Kvapil
6b5e2e3e77 [Backport release-0.39] [system] Add resource requests and limits to etcd-defrag (#1786)
# Description
Backport of #1785 to `release-0.39`.
2026-01-02 19:13:11 +01:00
Matthieu ROBIN
fc84b80b70 Add resource requests and limits to etcd-defrag
Added resource requests and limits for etcd-defrag container.

Signed-off-by: Matthieu ROBIN <info@matthieurobin.com>
(cherry picked from commit 675eaa6178)
2026-01-02 18:12:42 +00:00
Andrei Kvapil
85a0003ffb [Backport release-0.39] [tenant] Run cleanup job from system namespace (#1777)
# Description
Backport of #1774 to `release-0.39`.
2025-12-30 12:05:45 +01:00
Timofei Larkin
7c766fc27d [tenant] Run cleanup job from system namespace
## What this PR does

The Helm hook that creates a job deleting all applications in a tenant
before deleting the tenant itself now runs this job from the cozy-system
namespace. This prevents conflicts with resource quotas (not enough
resources to run cleanup job) without temporary increases of the quota
or similar vulnerability-introducing hacks.

### Release note

```release-note
[tenant] Run cleanup job in system namespace to avoid conflicts on
resource quotas.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
(cherry picked from commit b94fc6c9c8)
2025-12-30 11:03:18 +00:00
Andrei Kvapil
46d81c806f [Backport release-0.39] [tenant] Allow egress to parent ingress pods (#1776)
# Description
Backport of #1765 to `release-0.39`.
2025-12-30 12:00:19 +01:00
Andrei Kvapil
9300309dfb [Backport release-0.39] [kubevirt-operator] Fix typo in VMNotRunningFor10Minutes alert (#1775)
# Description
Backport of #1770 to `release-0.39`.
2025-12-30 12:00:07 +01:00
Aleksei Sviridkin
d90a76bc25 fix(tenant): allow egress to parent ingress pods
Nested Kubernetes clusters with exposeMethod: Proxied cannot reach their
own external domains because the clusterwide egress policy blocks traffic
to ingress pods in parent namespaces.

This breaks cert-manager HTTP-01 self-check and any scenario where pods
need to access services exposed through parent ingress.

Add egress rule allowing traffic to ingress pods (cozystack.io/service:
ingress) in parent namespaces, following the same pattern as existing
vminsert and etcd rules.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 4dfdbfeb62)
2025-12-30 10:59:45 +00:00
Aleksei Sviridkin
13bfb72ea5 [kubevirt-operator] Fix typo in VMNotRunningFor10Minutes alert
Fix case sensitivity in status check: "running" → "Running".
The metric uses "Running" (capital R), so the lowercase check was
always triggering alerts even for running VMs.

Fixes: https://github.com/cozystack/cozystack/issues/1552

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 7fab919e07)
2025-12-30 10:58:23 +00:00
Andrei Kvapil
56ed29c7fc [Backport release-0.39] [vm] Always expose VMs with a service (#1751)
# Description
Backport of #1738 to `release-0.39`.
2025-12-24 14:38:37 +01:00
Timofei Larkin
1ffa1ef454 [vm] Always expose VMs with a service
## What this PR does

When VMs are created without a public IP, no service is created for them
and they have no in-cluster DNS name. This PR fixes this and resolves
#1731.

### Release note

```release-note
[vm] Always expose VMs with at least a ClusterIP service.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
(cherry picked from commit ba50d01877)
2025-12-24 13:10:14 +00:00
Andrei Kvapil
1826858bb0 Release v0.39.1 (#1749)
This PR prepares the release `v0.39.1`.
2025-12-24 09:16:37 +01:00
cozystack-bot
0e262838d2 Prepare release v0.39.1
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2025-12-23 23:26:06 +00:00
Andrei Kvapil
b610630592 [monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant monitoring (#1712)
[monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant
monitoring
What this PR does
This PR introduces the SLACK_SEVERITY_FILTER environment variable in the
Alerta deployment to enable
filtering of alert severities for Slack notifications based on the
disabledSeverity configuration.
Additionally, it adds a VMAgent resource template for scraping metrics
within tenant namespaces, improving
monitoring granularity and control.

```release-note
[monitoring] Add SLACK_SEVERITY_FILTER for filtering Slack alert severities and VMAgent configuration for
tenant-specific metrics scraping.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **New Features**
  * Added configurable severity filtering for Telegram alerts.
  * Extended Slack severity filtering to accept lists of severities.

* **Bug Fixes / Behavior**
* Severity settings now accept arrays (multiple severities) instead of
single comma-separated strings.

* **Documentation**
* Updated configuration docs and examples to show list-style severity
settings.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-24 00:21:24 +01:00
Andrei Kvapil
357d9f95be Release v0.39.0 (#1747)
This PR prepares the release `v0.39.0`.
2025-12-23 19:07:54 +01:00
cozystack-bot
37a2e38798 Prepare release v0.39.0
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2025-12-23 16:27:57 +00:00
Nikita
da44a65ba9 [system/cilium] Enable topology-aware routing for services (#1734) 2025-12-23 19:17:03 +03:00
Nikita
26da347de8 [Backport release-0.38] [cilium] Enable automatic pod rollout on configmap updates (#1745)
# Description
Backport of #1728 to `release-0.38`.
2025-12-23 15:41:56 +03:00
Nikita
dd2798dbda [Backport release-0.38] [virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config (#1744)
# Description
Backport of #1693 to `release-0.38`.
2025-12-23 15:41:33 +03:00
Andrei Kvapil
fb5b4da2b2 [cilium] Enable automatic pod rollout on configmap updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 97e8d2aa49)
2025-12-23 12:41:25 +00:00
Andrei Kvapil
540e1c6e0d [virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 3975da93c6)
2025-12-23 12:39:33 +00:00
Nikita
1dd8e00e17 [Backport release-0.38] Update SeaweedFS v4.02 (#1732)
# Description
Backport of #1725 to `release-0.38`.
2025-12-18 13:40:31 +03:00
Andrei Kvapil
79165ca2f9 Update SeaweedFS v4.02
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 1805be3c48)
2025-12-18 10:39:52 +00:00
Andrei Kvapil
a8bd179f0d [Backport release-0.38] [apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK (#1709)
# Description
Backport of #1679 to `release-0.38`.
2025-12-10 19:28:16 +01:00
Andrei Kvapil
1491535b35 [apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK
This commit refactors the apiserver REST handlers to use typed objects
(appsv1alpha1.Application) instead of unstructured.Unstructured, eliminating
the need for runtime conversions and simplifying the codebase.

Additionally, it fixes an issue where UnstructuredList objects were using
the first registered kind from typeToGVK instead of the kind from the
object's field when multiple kinds are registered with the same Go type.

This is a more comprehensive fix for the problem addressed in
https://github.com/cozystack/cozystack/pull/1630, which was reverted in
https://github.com/cozystack/cozystack/pull/1677.

The fix includes the upstream fix from kubernetes/kubernetes#135537,
which enables short-circuit path for UnstructuredList similar to regular
Unstructured objects, using GVK from the object field instead of
typeToGVK.

Changes:
- Refactored rest.go handlers to use typed Application objects
- Removed unstructured.Unstructured conversions
- Fixed UnstructuredList GVK handling
- Updated dependencies in go.mod/go.sum
- Added e2e test for OpenAPI validation
- Updated Dockerfile

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit ca29fc855a)
2025-12-10 12:22:57 +00:00
Andrei Kvapil
290c6be04b Release v0.38.4 (#1704)
This PR prepares the release `v0.38.4`.
2025-12-09 19:54:27 +01:00
cozystack-bot
03328dc4e4 Prepare release v0.38.4
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2025-12-09 16:23:14 +00:00
Andrei Kvapil
9311a9e547 [Backport release-0.38] [virtual-machine] Improve check for resizing job (#1701)
# Description
Backport of #1688 to `release-0.38`.
2025-12-09 17:10:14 +01:00
Andrei Kvapil
548b2c0ed3 [virtual-machine] Improve check for resizing job
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 0bab895026)
2025-12-09 16:08:54 +00:00
Andrei Kvapil
202ff3433e [Backport release-0.38] [dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties (#1700)
# Description
Backport of #1692 to `release-0.38`.
2025-12-09 17:05:22 +01:00
Andrei Kvapil
891195018f [dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 578a810413)
2025-12-09 16:04:57 +00:00
Andrei Kvapil
d53861837f [Backport release-0.38] [linstor] Update piraeus-operator v2.10.2 (#1697)
# Description
Backport of #1689 to `release-0.38`.
2025-12-09 14:58:13 +01:00
Andrei Kvapil
f1a75ab864 [linstor] Update piraeus-operator v2.10.2
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 58dd1f5881)
2025-12-09 13:43:46 +00:00
Nikita
2110534e63 Release v0.38.3 (#1686)
This PR prepares the release `v0.38.3`.
2025-12-04 20:07:33 +03:00
cozystack-bot
2e22a6579e Prepare release v0.38.3
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2025-12-04 16:19:17 +00:00
Nikita
35907dd474 [core:installer] Address buildx warnings (#1682)
## What this PR does
Buildx is worried about Dockerfile syntax, this pr fixes it.
```
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 16)
```

### Release note
```release-note
Buildx warnings addressed.
```
2025-12-03 19:42:24 +03:00
Nikita
af56c105c2 [system:coredns] update coredns app labels to match Talos coredns labels (#1675)
## What this PR does
Updates coredns app labels to match Talos coredns labels

### Release note

```release-note
Coredns app labels updated to match Talos coredns labels.
```
2025-12-03 19:42:14 +03:00
Nikita
30e5b71e3f [system:monitoring-agents] rename coredns metrics service (#1676)
## What this PR does
Renames coredns metrics service

### Release note
```release-note
Renamed coredns metrics service not to interfere with coredns service used for name resolution in tenant k8s clusters.
```
2025-12-03 19:41:59 +03:00
172 changed files with 19124 additions and 1141 deletions

View File

@@ -15,9 +15,10 @@ build: build-deps
make -C packages/extra/monitoring image
make -C packages/system/cozystack-api image
make -C packages/system/cozystack-controller image
make -C packages/system/backup-controller image
make -C packages/system/lineage-controller-webhook image
make -C packages/system/cilium image
make -C packages/system/kubeovn image
make -C packages/system/linstor image
make -C packages/system/kubeovn-webhook image
make -C packages/system/kubeovn-plunger image
make -C packages/system/dashboard image

1
api/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
zz_generated_deepcopy.go linguist-generated

View File

@@ -0,0 +1,37 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package v1alpha1 contains API Schema definitions for the v1alpha1 API group.
// +kubebuilder:object:generate=true
// +groupName=strategy.backups.cozystack.io
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
GroupVersion = schema.GroupVersion{Group: "strategy.backups.cozystack.io", Version: "v1alpha1"}
SchemeBuilder = runtime.NewSchemeBuilder(addGroupVersion)
AddToScheme = SchemeBuilder.AddToScheme
)
func addGroupVersion(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, GroupVersion)
return nil
}

View File

@@ -0,0 +1,63 @@
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 defines strategy.backups.cozystack.io API types.
//
// Group: strategy.backups.cozystack.io
// Version: v1alpha1
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&Job{},
&JobList{},
)
return nil
})
}
const (
JobStrategyKind = "Job"
)
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// Job defines a backup strategy using a one-shot Job
type Job struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec JobSpec `json:"spec,omitempty"`
Status JobStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// JobList contains a list of backup Jobs.
type JobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Job `json:"items"`
}
// JobSpec specifies the desired behavior of a backup job.
type JobSpec struct {
// Template holds a PodTemplateSpec with the right shape to
// run a single pod to completion and create a tarball with
// a given apps data. Helm-like Go templates are supported.
// The values of the source application are available under
// `.Values`. `.Release.Name` and `.Release.Namespace` are
// also exported.
Template corev1.PodTemplateSpec `json:"template"`
}
type JobStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

View File

@@ -0,0 +1,64 @@
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 defines strategy.backups.cozystack.io API types.
//
// Group: strategy.backups.cozystack.io
// Version: v1alpha1
package v1alpha1
import (
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&Velero{},
&VeleroList{},
)
return nil
})
}
const (
VeleroStrategyKind = "Velero"
)
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// Velero defines a backup strategy using Velero as the driver.
type Velero struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec VeleroSpec `json:"spec,omitempty"`
Status VeleroStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// VeleroList contains a list of Velero backup strategies.
type VeleroList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Velero `json:"items"`
}
// VeleroSpec specifies the desired strategy for backing up with Velero.
type VeleroSpec struct {
Template VeleroTemplate `json:"template"`
}
// VeleroTemplate describes the data a backup.velero.io should have when
// templated from a Velero backup strategy.
type VeleroTemplate struct {
Spec velerov1.BackupSpec `json:"spec"`
RestoreSpec velerov1.RestoreSpec `json:"restoreSpec"`
}
type VeleroStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

View File

@@ -0,0 +1,237 @@
//go:build !ignore_autogenerated
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Job) DeepCopyInto(out *Job) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Job.
func (in *Job) DeepCopy() *Job {
if in == nil {
return nil
}
out := new(Job)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Job) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobList) DeepCopyInto(out *JobList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Job, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobList.
func (in *JobList) DeepCopy() *JobList {
if in == nil {
return nil
}
out := new(JobList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *JobList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobSpec) DeepCopyInto(out *JobSpec) {
*out = *in
in.Template.DeepCopyInto(&out.Template)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobSpec.
func (in *JobSpec) DeepCopy() *JobSpec {
if in == nil {
return nil
}
out := new(JobSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JobStatus) DeepCopyInto(out *JobStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobStatus.
func (in *JobStatus) DeepCopy() *JobStatus {
if in == nil {
return nil
}
out := new(JobStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Velero) DeepCopyInto(out *Velero) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Velero.
func (in *Velero) DeepCopy() *Velero {
if in == nil {
return nil
}
out := new(Velero)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Velero) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroList) DeepCopyInto(out *VeleroList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Velero, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroList.
func (in *VeleroList) DeepCopy() *VeleroList {
if in == nil {
return nil
}
out := new(VeleroList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *VeleroList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroSpec) DeepCopyInto(out *VeleroSpec) {
*out = *in
in.Template.DeepCopyInto(&out.Template)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroSpec.
func (in *VeleroSpec) DeepCopy() *VeleroSpec {
if in == nil {
return nil
}
out := new(VeleroSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroStatus) DeepCopyInto(out *VeleroStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroStatus.
func (in *VeleroStatus) DeepCopy() *VeleroStatus {
if in == nil {
return nil
}
out := new(VeleroStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroTemplate) DeepCopyInto(out *VeleroTemplate) {
*out = *in
in.Spec.DeepCopyInto(&out.Spec)
in.RestoreSpec.DeepCopyInto(&out.RestoreSpec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroTemplate.
func (in *VeleroTemplate) DeepCopy() *VeleroTemplate {
if in == nil {
return nil
}
out := new(VeleroTemplate)
in.DeepCopyInto(out)
return out
}

View File

@@ -0,0 +1,421 @@
# Cozystack Backups Core API & Contracts (Draft)
## 1. Overview
Cozystacks backup subsystem provides a generic, composable way to back up and restore managed applications:
* Every **application instance** can have one or more **backup plans**.
* Backups are stored in configurable **storage locations**.
* The mechanics of *how* a backup/restore is performed are delegated to **strategy drivers**, each implementing driver-specific **BackupStrategy** CRDs.
The core API:
* Orchestrates **when** backups happen and **where** theyre stored.
* Tracks **what** backups exist and their status.
* Defines contracts with drivers via shared resources (`BackupJob`, `Backup`, `RestoreJob`).
It does **not** implement the backup logic itself.
This document covers only the **core** API and its contracts with drivers, not driver implementations.
---
## 2. Goals and non-goals
### Goals
* Provide a **stable core API** for:
* Declaring **backup plans** per application.
* Configuring **storage targets** (S3, in-cluster bucket, etc.).
* Tracking **backup artifacts**.
* Initiating and tracking **restores**.
* Allow multiple **strategy drivers** to plug in, each supporting specific kinds of applications and strategies.
* Let application/product authors implement backup for their kinds by:
* Creating **Plan** objects referencing a **driver-specific strategy**.
* Not having to write a backup engine themselves.
### Non-goals
* Implement backup logic for any specific application or storage backend.
* Define the internal structure of driver-specific strategy CRDs.
* Handle tenant-facing UI/UX (thats built on top of these APIs).
---
## 3. Architecture
High-level components:
* **Core backups controller(s)** (Cozystack-owned):
* Group: `backups.cozystack.io`
* Own:
* `Plan`
* `BackupJob`
* `Backup`
* `RestoreJob`
* Responsibilities:
* Schedule backups based on `Plan`.
* Create `BackupJob` objects when due.
* Provide stable contracts for drivers to:
* Perform backups and create `Backup`s.
* Perform restores based on `Backup`s.
* **Strategy drivers** (pluggable, possibly third-party):
* Their own API groups, e.g. `jobdriver.backups.cozystack.io`.
* Own **strategy CRDs** (e.g. `JobBackupStrategy`).
* Implement controllers that:
* Watch `BackupJob` / `RestoreJob`.
* Match runs whose `strategyRef` GVK they support.
* Execute backup/restore logic.
* Create and update `Backup` and run statuses.
Strategy drivers and core communicate entirely via Kubernetes objects; there are no webhook/HTTP calls between them.
* **Storage drivers** (pluggable, possibly third-party):
* **TBD**
---
## 4. Core API resources
### 4.1 Plan
**Group/Kind**
`backups.cozystack.io/v1alpha1, Kind=Plan`
**Purpose**
Describe **when**, **how**, and **where** to back up a specific managed application.
**Key fields (spec)**
```go
type PlanSpec struct {
// Application to back up.
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
// Where backups should be stored.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// Driver-specific BackupStrategy to use.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
// When backups should run.
Schedule PlanSchedule `json:"schedule"`
}
```
`PlanSchedule` (initially) supports only cron:
```go
type PlanScheduleType string
const (
PlanScheduleTypeEmpty PlanScheduleType = ""
PlanScheduleTypeCron PlanScheduleType = "cron"
)
```
```go
type PlanSchedule struct {
// Type is the schedule type. Currently only "cron" is supported.
// Defaults to "cron".
Type PlanScheduleType `json:"type,omitempty"`
// Cron expression (required for cron type).
Cron string `json:"cron,omitempty"`
}
```
**Plan reconciliation contract**
Core Plan controller:
1. **Read schedule** from `spec.schedule` and compute the next fire time.
2. When due:
* Create a `BackupJob` in the same namespace:
* `spec.planRef.name = plan.Name`
* `spec.applicationRef = plan.spec.applicationRef`
* `spec.storageRef = plan.spec.storageRef`
* `spec.strategyRef = plan.spec.strategyRef`
* `spec.triggeredBy = "Plan"`
* Set `ownerReferences` so the `BackupJob` is owned by the `Plan`.
The Plan controller does **not**:
* Execute backups itself.
* Modify driver resources or `Backup` objects.
* Touch `BackupJob.spec` after creation.
---
### 4.2 Storage
**API Shape**
TBD
**Storage usage**
* `Plan` and `BackupJob` reference `Storage` via `TypedLocalObjectReference`.
* Drivers read `Storage` to know how/where to store or read artifacts.
* Core treats `Storage` spec as opaque; it does not directly talk to S3 or buckets.
---
### 4.3 BackupJob
**Group/Kind**
`backups.cozystack.io/v1alpha1, Kind=BackupJob`
**Purpose**
Represent a single **execution** of a backup operation, typically created when a `Plan` fires or when a user triggers an ad-hoc backup.
**Key fields (spec)**
```go
type BackupJobSpec struct {
// Plan that triggered this run, if any.
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
// Application to back up.
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
// Storage to use.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// Driver-specific BackupStrategy to use.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
// Informational: what triggered this run ("Plan", "Manual", etc.).
TriggeredBy string `json:"triggeredBy,omitempty"`
}
```
**Key fields (status)**
```go
type BackupJobStatus struct {
Phase BackupJobPhase `json:"phase,omitempty"`
BackupRef *corev1.LocalObjectReference `json:"backupRef,omitempty"`
StartedAt *metav1.Time `json:"startedAt,omitempty"`
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
Message string `json:"message,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
```
`BackupJobPhase` is one of: `Pending`, `Running`, `Succeeded`, `Failed`.
**BackupJob contract with drivers**
* Core **creates** `BackupJob` and must treat `spec` as immutable afterwards.
* Each driver controller:
* Watches `BackupJob`.
* Reconciles runs where `spec.strategyRef.apiGroup/kind` matches its **strategy type(s)**.
* Driver responsibilities:
1. On first reconcile:
* Set `status.startedAt` if unset.
* Set `status.phase = Running`.
2. Resolve inputs:
* Read `Strategy` (driver-owned CRD), `Storage`, `Application`, optionally `Plan`.
3. Execute backup logic (implementation-specific).
4. On success:
* Create a `Backup` resource (see below).
* Set `status.backupRef` to the created `Backup`.
* Set `status.completedAt`.
* Set `status.phase = Succeeded`.
5. On failure:
* Set `status.completedAt`.
* Set `status.phase = Failed`.
* Set `status.message` and conditions.
Drivers must **not** modify `BackupJob.spec` or delete `BackupJob` themselves.
---
### 4.4 Backup
**Group/Kind**
`backups.cozystack.io/v1alpha1, Kind=Backup`
**Purpose**
Represent a single **backup artifact** for a given application, decoupled from a particular run. usable as a stable, listable “thing you can restore from”.
**Key fields (spec)**
```go
type BackupSpec struct {
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
TakenAt metav1.Time `json:"takenAt"`
DriverMetadata map[string]string `json:"driverMetadata,omitempty"`
}
```
**Key fields (status)**
```go
type BackupStatus struct {
Phase BackupPhase `json:"phase,omitempty"` // Pending, Ready, Failed, etc.
Artifact *BackupArtifact `json:"artifact,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
```
`BackupArtifact` describes the artifact (URI, size, checksum).
**Backup contract with drivers**
* On successful completion of a `BackupJob`, the **driver**:
* Creates a `Backup` in the same namespace (typically owned by the `BackupJob`).
* Populates `spec` fields with:
* The application, storage, strategy references.
* `takenAt`.
* Optional `driverMetadata`.
* Sets `status` with:
* `phase = Ready` (or equivalent when fully usable).
* `artifact` describing the stored object.
* Core:
* Treats `Backup` spec as mostly immutable and opaque.
* Uses it to:
* List backups for a given application/plan.
* Anchor `RestoreJob` operations.
* Implement higher-level policies (retention) if needed.
---
### 4.5 RestoreJob
**Group/Kind**
`backups.cozystack.io/v1alpha1, Kind=RestoreJob`
**Purpose**
Represent a single **restore operation** from a `Backup`, either back into the same application or into a new target application.
**Key fields (spec)**
```go
type RestoreJobSpec struct {
// Backup to restore from.
BackupRef corev1.LocalObjectReference `json:"backupRef"`
// Target application; if omitted, drivers SHOULD restore into
// backup.spec.applicationRef.
TargetApplicationRef *corev1.TypedLocalObjectReference `json:"targetApplicationRef,omitempty"`
}
```
**Key fields (status)**
```go
type RestoreJobStatus struct {
Phase RestoreJobPhase `json:"phase,omitempty"` // Pending, Running, Succeeded, Failed
StartedAt *metav1.Time `json:"startedAt,omitempty"`
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
Message string `json:"message,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
```
**RestoreJob contract with drivers**
* RestoreJob is created either manually or by core.
* Driver controller:
1. Watches `RestoreJob`.
2. On reconcile:
* Fetches the referenced `Backup`.
* Determines effective:
* **Strategy**: `backup.spec.strategyRef`.
* **Storage**: `backup.spec.storageRef`.
* **Target application**: `spec.targetApplicationRef` or `backup.spec.applicationRef`.
* If effective strategys GVK is one of its supported strategy types → driver is responsible.
3. Behaviour:
* On first reconcile, set `status.startedAt` and `phase = Running`.
* Resolve `Backup`, `Storage`, `Strategy`, target application.
* Execute restore logic (implementation-specific).
* On success:
* Set `status.completedAt`.
* Set `status.phase = Succeeded`.
* On failure:
* Set `status.completedAt`.
* Set `status.phase = Failed`.
* Set `status.message` and conditions.
Drivers must not modify `RestoreJob.spec` or delete `RestoreJob`.
---
## 5. Strategy drivers (high-level)
Strategy drivers are separate controllers that:
* Define their own **strategy CRDs** (e.g. `JobBackupStrategy`) in their own API groups:
* e.g. `jobdriver.backups.cozystack.io/v1alpha1, Kind=JobBackupStrategy`
* Implement the **BackupJob contract**:
* Watch `BackupJob`.
* Filter by `spec.strategyRef.apiGroup/kind`.
* Execute backup logic.
* Create/update `Backup`.
* Implement the **RestoreJob contract**:
* Watch `RestoreJob`.
* Resolve `Backup`, then effective `strategyRef`.
* Filter by effective strategy GVK.
* Execute restore logic.
The core backups API **does not** dictate:
* The fields and structure of driver strategy specs.
* How drivers implement backup/restore internally (Jobs, snapshots, native operator CRDs, etc.).
Drivers are interchangeable as long as they respect:
* The `BackupJob` and `RestoreJob` contracts.
* The shapes and semantics of `Backup` objects.
---
## 6. Summary
The Cozystack backups core API:
* Uses a single group, `backups.cozystack.io`, for all core CRDs.
* Cleanly separates:
* **When & where** (Plan + Storage) core-owned.
* **What backup artifacts exist** (Backup) driver-created but cluster-visible.
* **Execution lifecycle** (BackupJob, RestoreJob) shared contract boundary.
* Allows multiple strategy drivers to implement backup/restore logic without entangling their implementation with the core API.

View File

@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 defines backups.cozystack.io API types.
//
// Group: backups.cozystack.io
// Version: v1alpha1
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&Backup{},
&BackupList{},
)
return nil
})
}
// BackupPhase represents the lifecycle phase of a Backup.
type BackupPhase string
const (
BackupPhaseEmpty BackupPhase = ""
BackupPhasePending BackupPhase = "Pending"
BackupPhaseReady BackupPhase = "Ready"
BackupPhaseFailed BackupPhase = "Failed"
)
// BackupArtifact describes the stored backup object (tarball, snapshot, etc.).
type BackupArtifact struct {
// URI is a driver-/storage-specific URI pointing to the backup artifact.
// For example: s3://bucket/prefix/file.tar.gz
URI string `json:"uri"`
// SizeBytes is the size of the artifact in bytes, if known.
// +optional
SizeBytes int64 `json:"sizeBytes,omitempty"`
// Checksum is the checksum of the artifact, if computed.
// For example: "sha256:<hex>".
// +optional
Checksum string `json:"checksum,omitempty"`
}
// BackupSpec describes an immutable backup artifact produced by a BackupJob.
type BackupSpec struct {
// ApplicationRef refers to the application that was backed up.
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
// PlanRef refers to the Plan that produced this backup, if any.
// For manually triggered backups, this can be omitted.
// +optional
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
// StorageRef refers to the Storage object that describes where the backup
// artifact is stored.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// StrategyRef refers to the driver-specific BackupStrategy that was used
// to create this backup. This allows the driver to later perform restores.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
// TakenAt is the time at which the backup was taken (as reported by the
// driver). It may differ slightly from metadata.creationTimestamp.
TakenAt metav1.Time `json:"takenAt"`
// DriverMetadata holds driver-specific, opaque metadata associated with
// this backup (for example snapshot IDs, schema versions, etc.).
// This data is not interpreted by the core backup controllers.
// +optional
DriverMetadata map[string]string `json:"driverMetadata,omitempty"`
}
// BackupStatus represents the observed state of a Backup.
type BackupStatus struct {
// Phase is a simple, high-level summary of the backup's state.
// Typical values are: Pending, Ready, Failed.
// +optional
Phase BackupPhase `json:"phase,omitempty"`
// Artifact describes the stored backup object, if available.
// +optional
Artifact *BackupArtifact `json:"artifact,omitempty"`
// Conditions represents the latest available observations of a Backup's state.
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// The field indexing on applicationRef will be needed later to display per-app backup resources.
// +kubebuilder:object:root=true
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
// Backup represents a single backup artifact for a given application.
type Backup struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BackupSpec `json:"spec,omitempty"`
Status BackupStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BackupList contains a list of Backups.
type BackupList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Backup `json:"items"`
}

View File

@@ -0,0 +1,116 @@
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 defines backups.cozystack.io API types.
//
// Group: backups.cozystack.io
// Version: v1alpha1
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&BackupJob{},
&BackupJobList{},
)
return nil
})
}
const (
OwningJobNameLabel = thisGroup + "/owned-by.BackupJobName"
OwningJobNamespaceLabel = thisGroup + "/owned-by.BackupJobNamespace"
)
// BackupJobPhase represents the lifecycle phase of a BackupJob.
type BackupJobPhase string
const (
BackupJobPhaseEmpty BackupJobPhase = ""
BackupJobPhasePending BackupJobPhase = "Pending"
BackupJobPhaseRunning BackupJobPhase = "Running"
BackupJobPhaseSucceeded BackupJobPhase = "Succeeded"
BackupJobPhaseFailed BackupJobPhase = "Failed"
)
// BackupJobSpec describes the execution of a single backup operation.
type BackupJobSpec struct {
// PlanRef refers to the Plan that requested this backup run.
// For ad-hoc/manual backups, this can be omitted.
// +optional
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
// ApplicationRef holds a reference to the managed application whose state
// is being backed up.
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
// StorageRef holds a reference to the Storage object that describes where
// the backup will be stored.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// StrategyRef holds a reference to the driver-specific BackupStrategy object
// that describes how the backup should be created.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
}
// BackupJobStatus represents the observed state of a BackupJob.
type BackupJobStatus struct {
// Phase is a high-level summary of the run's state.
// Typical values: Pending, Running, Succeeded, Failed.
// +optional
Phase BackupJobPhase `json:"phase,omitempty"`
// BackupRef refers to the Backup object created by this run, if any.
// +optional
BackupRef *corev1.LocalObjectReference `json:"backupRef,omitempty"`
// StartedAt is the time at which the backup run started.
// +optional
StartedAt *metav1.Time `json:"startedAt,omitempty"`
// CompletedAt is the time at which the backup run completed (successfully
// or otherwise).
// +optional
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
// Message is a human-readable message indicating details about why the
// backup run is in its current phase, if any.
// +optional
Message string `json:"message,omitempty"`
// Conditions represents the latest available observations of a BackupJob's state.
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// The field indexing on applicationRef will be needed later to display per-app backup resources.
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",priority=0
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
// BackupJob represents a single execution of a backup.
// It is typically created by a Plan controller when a schedule fires.
type BackupJob struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BackupJobSpec `json:"spec,omitempty"`
Status BackupJobStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BackupJobList contains a list of BackupJobs.
type BackupJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BackupJob `json:"items"`
}

View File

@@ -0,0 +1,42 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package v1alpha1 contains API Schema definitions for the v1alpha1 API group.
// +kubebuilder:object:generate=true
// +groupName=backups.cozystack.io
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
thisGroup = "backups.cozystack.io"
thisVersion = "v1alpha1"
)
var (
GroupVersion = schema.GroupVersion{Group: thisGroup, Version: thisVersion}
SchemeBuilder = runtime.NewSchemeBuilder(addGroupVersion)
AddToScheme = SchemeBuilder.AddToScheme
)
func addGroupVersion(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, GroupVersion)
return nil
}

View File

@@ -0,0 +1,98 @@
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 defines backups.cozystack.io API types.
//
// Group: backups.cozystack.io
// Version: v1alpha1
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&Plan{},
&PlanList{},
)
return nil
})
}
type PlanScheduleType string
const (
PlanScheduleTypeEmpty PlanScheduleType = ""
PlanScheduleTypeCron PlanScheduleType = "cron"
)
// Condtions
const (
PlanConditionError = "Error"
)
// The field indexing on applicationRef will be needed later to display per-app backup resources.
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
// Plan describes the schedule, method and storage location for the
// backup of a given target application.
type Plan struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PlanSpec `json:"spec,omitempty"`
Status PlanStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// PlanList contains a list of backup Plans.
type PlanList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Plan `json:"items"`
}
// PlanSpec references the storage, the strategy, the application to be
// backed up and specifies the timetable on which the backups will run.
type PlanSpec struct {
// ApplicationRef holds a reference to the managed application,
// whose state and configuration must be backed up.
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
// StorageRef holds a reference to the Storage object that
// describes the location where the backup will be stored.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// StrategyRef holds a reference to the Strategy object that
// describes, how a backup copy is to be created.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
// Schedule specifies when backup copies are created.
Schedule PlanSchedule `json:"schedule"`
}
// PlanSchedule specifies when backup copies are created.
type PlanSchedule struct {
// Type is the type of schedule specification. Supported values are
// [`cron`]. If omitted, defaults to `cron`.
// +optional
Type PlanScheduleType `json:"type,omitempty"`
// Cron contains the cron spec for scheduling backups. Must be
// specified if the schedule type is `cron`. Since only `cron` is
// supported, omitting this field is not allowed.
// +optional
Cron string `json:"cron,omitempty"`
}
type PlanStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

View File

@@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 defines backups.cozystack.io API types.
//
// Group: backups.cozystack.io
// Version: v1alpha1
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&RestoreJob{},
&RestoreJobList{},
)
return nil
})
}
// RestoreJobPhase represents the lifecycle phase of a RestoreJob.
type RestoreJobPhase string
const (
RestoreJobPhaseEmpty RestoreJobPhase = ""
RestoreJobPhasePending RestoreJobPhase = "Pending"
RestoreJobPhaseRunning RestoreJobPhase = "Running"
RestoreJobPhaseSucceeded RestoreJobPhase = "Succeeded"
RestoreJobPhaseFailed RestoreJobPhase = "Failed"
)
// RestoreJobSpec describes the execution of a single restore operation.
type RestoreJobSpec struct {
// BackupRef refers to the Backup that should be restored.
BackupRef corev1.LocalObjectReference `json:"backupRef"`
// TargetApplicationRef refers to the application into which the backup
// should be restored. If omitted, the driver SHOULD restore into the same
// application as referenced by backup.spec.applicationRef.
// +optional
TargetApplicationRef *corev1.TypedLocalObjectReference `json:"targetApplicationRef,omitempty"`
}
// RestoreJobStatus represents the observed state of a RestoreJob.
type RestoreJobStatus struct {
// Phase is a high-level summary of the run's state.
// Typical values: Pending, Running, Succeeded, Failed.
// +optional
Phase RestoreJobPhase `json:"phase,omitempty"`
// StartedAt is the time at which the restore run started.
// +optional
StartedAt *metav1.Time `json:"startedAt,omitempty"`
// CompletedAt is the time at which the restore run completed (successfully
// or otherwise).
// +optional
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
// Message is a human-readable message indicating details about why the
// restore run is in its current phase, if any.
// +optional
Message string `json:"message,omitempty"`
// Conditions represents the latest available observations of a RestoreJob's state.
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",priority=0
// RestoreJob represents a single execution of a restore from a Backup.
type RestoreJob struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec RestoreJobSpec `json:"spec,omitempty"`
Status RestoreJobStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// RestoreJobList contains a list of RestoreJobs.
type RestoreJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []RestoreJob `json:"items"`
}

View File

@@ -0,0 +1,501 @@
//go:build !ignore_autogenerated
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Backup) DeepCopyInto(out *Backup) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backup.
func (in *Backup) DeepCopy() *Backup {
if in == nil {
return nil
}
out := new(Backup)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Backup) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupArtifact) DeepCopyInto(out *BackupArtifact) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupArtifact.
func (in *BackupArtifact) DeepCopy() *BackupArtifact {
if in == nil {
return nil
}
out := new(BackupArtifact)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupJob) DeepCopyInto(out *BackupJob) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJob.
func (in *BackupJob) DeepCopy() *BackupJob {
if in == nil {
return nil
}
out := new(BackupJob)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BackupJob) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupJobList) DeepCopyInto(out *BackupJobList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]BackupJob, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobList.
func (in *BackupJobList) DeepCopy() *BackupJobList {
if in == nil {
return nil
}
out := new(BackupJobList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BackupJobList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupJobSpec) DeepCopyInto(out *BackupJobSpec) {
*out = *in
if in.PlanRef != nil {
in, out := &in.PlanRef, &out.PlanRef
*out = new(v1.LocalObjectReference)
**out = **in
}
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
in.StorageRef.DeepCopyInto(&out.StorageRef)
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobSpec.
func (in *BackupJobSpec) DeepCopy() *BackupJobSpec {
if in == nil {
return nil
}
out := new(BackupJobSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupJobStatus) DeepCopyInto(out *BackupJobStatus) {
*out = *in
if in.BackupRef != nil {
in, out := &in.BackupRef, &out.BackupRef
*out = new(v1.LocalObjectReference)
**out = **in
}
if in.StartedAt != nil {
in, out := &in.StartedAt, &out.StartedAt
*out = (*in).DeepCopy()
}
if in.CompletedAt != nil {
in, out := &in.CompletedAt, &out.CompletedAt
*out = (*in).DeepCopy()
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobStatus.
func (in *BackupJobStatus) DeepCopy() *BackupJobStatus {
if in == nil {
return nil
}
out := new(BackupJobStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupList) DeepCopyInto(out *BackupList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Backup, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupList.
func (in *BackupList) DeepCopy() *BackupList {
if in == nil {
return nil
}
out := new(BackupList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BackupList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupSpec) DeepCopyInto(out *BackupSpec) {
*out = *in
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
if in.PlanRef != nil {
in, out := &in.PlanRef, &out.PlanRef
*out = new(v1.LocalObjectReference)
**out = **in
}
in.StorageRef.DeepCopyInto(&out.StorageRef)
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
in.TakenAt.DeepCopyInto(&out.TakenAt)
if in.DriverMetadata != nil {
in, out := &in.DriverMetadata, &out.DriverMetadata
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSpec.
func (in *BackupSpec) DeepCopy() *BackupSpec {
if in == nil {
return nil
}
out := new(BackupSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupStatus) DeepCopyInto(out *BackupStatus) {
*out = *in
if in.Artifact != nil {
in, out := &in.Artifact, &out.Artifact
*out = new(BackupArtifact)
**out = **in
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupStatus.
func (in *BackupStatus) DeepCopy() *BackupStatus {
if in == nil {
return nil
}
out := new(BackupStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Plan) DeepCopyInto(out *Plan) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Plan.
func (in *Plan) DeepCopy() *Plan {
if in == nil {
return nil
}
out := new(Plan)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Plan) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlanList) DeepCopyInto(out *PlanList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Plan, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanList.
func (in *PlanList) DeepCopy() *PlanList {
if in == nil {
return nil
}
out := new(PlanList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PlanList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlanSchedule) DeepCopyInto(out *PlanSchedule) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanSchedule.
func (in *PlanSchedule) DeepCopy() *PlanSchedule {
if in == nil {
return nil
}
out := new(PlanSchedule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlanSpec) DeepCopyInto(out *PlanSpec) {
*out = *in
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
in.StorageRef.DeepCopyInto(&out.StorageRef)
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
out.Schedule = in.Schedule
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanSpec.
func (in *PlanSpec) DeepCopy() *PlanSpec {
if in == nil {
return nil
}
out := new(PlanSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlanStatus) DeepCopyInto(out *PlanStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanStatus.
func (in *PlanStatus) DeepCopy() *PlanStatus {
if in == nil {
return nil
}
out := new(PlanStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RestoreJob) DeepCopyInto(out *RestoreJob) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJob.
func (in *RestoreJob) DeepCopy() *RestoreJob {
if in == nil {
return nil
}
out := new(RestoreJob)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *RestoreJob) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RestoreJobList) DeepCopyInto(out *RestoreJobList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]RestoreJob, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobList.
func (in *RestoreJobList) DeepCopy() *RestoreJobList {
if in == nil {
return nil
}
out := new(RestoreJobList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *RestoreJobList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RestoreJobSpec) DeepCopyInto(out *RestoreJobSpec) {
*out = *in
out.BackupRef = in.BackupRef
if in.TargetApplicationRef != nil {
in, out := &in.TargetApplicationRef, &out.TargetApplicationRef
*out = new(v1.TypedLocalObjectReference)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobSpec.
func (in *RestoreJobSpec) DeepCopy() *RestoreJobSpec {
if in == nil {
return nil
}
out := new(RestoreJobSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RestoreJobStatus) DeepCopyInto(out *RestoreJobStatus) {
*out = *in
if in.StartedAt != nil {
in, out := &in.StartedAt, &out.StartedAt
*out = (*in).DeepCopy()
}
if in.CompletedAt != nil {
in, out := &in.CompletedAt, &out.CompletedAt
*out = (*in).DeepCopy()
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobStatus.
func (in *RestoreJobStatus) DeepCopy() *RestoreJobStatus {
if in == nil {
return nil
}
out := new(RestoreJobStatus)
in.DeepCopyInto(out)
return out
}

View File

@@ -0,0 +1,174 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/tls"
"flag"
"os"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
"github.com/cozystack/cozystack/internal/backupcontroller"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(backupsv1alpha1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&secureMetrics, "metrics-secure", true,
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
opts := zap.Options{
Development: false,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
// Rapid Reset CVEs. For more information see:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("disabling http/2")
c.NextProtos = []string{"http/1.1"}
}
if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
webhookServer := webhook.NewServer(webhook.Options{
TLSOpts: tlsOpts,
})
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
TLSOpts: tlsOpts,
}
if secureMetrics {
// FilterProvider is used to protect the metrics endpoint with authn/authz.
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
// generate self-signed certificates for the metrics server. While convenient for development and testing,
// this setup is not recommended for production.
}
// Configure rate limiting for the Kubernetes client
config := ctrl.GetConfigOrDie()
config.QPS = 50.0 // Increased from default 5.0
config.Burst = 100 // Increased from default 10
mgr, err := ctrl.NewManager(config, ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "core.backups.cozystack.io",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
if err = (&backupcontroller.PlanReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Plan")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}

View File

@@ -0,0 +1,188 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/tls"
"flag"
"os"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
"github.com/cozystack/cozystack/internal/backupcontroller"
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(backupsv1alpha1.AddToScheme(scheme))
utilruntime.Must(strategyv1alpha1.AddToScheme(scheme))
utilruntime.Must(velerov1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&secureMetrics, "metrics-secure", true,
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
opts := zap.Options{
Development: false,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
// Rapid Reset CVEs. For more information see:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("disabling http/2")
c.NextProtos = []string{"http/1.1"}
}
if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
webhookServer := webhook.NewServer(webhook.Options{
TLSOpts: tlsOpts,
})
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
TLSOpts: tlsOpts,
}
if secureMetrics {
// FilterProvider is used to protect the metrics endpoint with authn/authz.
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
// generate self-signed certificates for the metrics server. While convenient for development and testing,
// this setup is not recommended for production.
}
// Configure rate limiting for the Kubernetes client
config := ctrl.GetConfigOrDie()
config.QPS = 50.0 // Increased from default 5.0
config.Burst = 100 // Increased from default 10
mgr, err := ctrl.NewManager(config, ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "strategy.backups.cozystack.io",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
if err = (&backupcontroller.BackupJobReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("backup-controller"),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "BackupJob")
os.Exit(1)
}
if err = (&backupcontroller.RestoreJobReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("restore-controller"),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "RestoreJob")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}

View File

@@ -0,0 +1,20 @@
apiVersion: backups.cozystack.io/v1alpha1
kind: BackupJob
metadata:
name: desired-backup
namespace: tenant-root
labels:
backups.cozystack.io/triggered-by: manual
spec:
applicationRef:
apiGroup: apps.cozystack.io
kind: VirtualMachine
name: vm1
storageRef:
apiGroup: apps.cozystack.io
kind: Bucket
name: test-bucket
strategyRef:
apiGroup: strategy.backups.cozystack.io
kind: Velero
name: velero-strategy-default

145
go.mod
View File

@@ -2,33 +2,41 @@
module github.com/cozystack/cozystack
go 1.23.0
go 1.24.0
toolchain go1.24.5
require (
github.com/fluxcd/helm-controller/api v1.1.0
github.com/go-logr/logr v1.4.3
github.com/go-logr/zapr v1.3.0
github.com/google/gofuzz v1.2.0
github.com/onsi/ginkgo/v2 v2.19.0
github.com/onsi/gomega v1.33.1
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/onsi/ginkgo/v2 v2.22.0
github.com/onsi/gomega v1.36.1
github.com/prometheus/client_golang v1.22.0
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/vmware-tanzu/velero v1.17.1
go.uber.org/zap v1.27.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.31.2
k8s.io/apiextensions-apiserver v0.31.2
k8s.io/apimachinery v0.31.2
k8s.io/apiserver v0.31.2
k8s.io/client-go v0.31.2
k8s.io/component-base v0.31.2
k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/apiserver v0.34.1
k8s.io/client-go v0.34.1
k8s.io/component-base v0.34.1
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
sigs.k8s.io/controller-runtime v0.19.0
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/structured-merge-diff/v4 v4.6.0
)
require (
cel.dev/expr v0.24.0 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -36,86 +44,89 @@ require (
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fluxcd/pkg/apis/kustomize v1.6.1 // indirect
github.com/fluxcd/pkg/apis/meta v1.6.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/cel-go v0.21.0 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.4.0 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.etcd.io/etcd/api/v3 v3.5.16 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect
go.etcd.io/etcd/client/v3 v3.5.16 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.etcd.io/etcd/api/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.34.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/kms v0.31.2 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
k8s.io/kms v0.34.1 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
// See: issues.k8s.io/135537
//replace k8s.io/apimachinery => github.com/cozystack/apimachinery v0.0.0-20251201201312-18e522a87614

324
go.sum
View File

@@ -1,11 +1,11 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
@@ -18,7 +18,7 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -26,12 +26,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/helm-controller/api v1.1.0 h1:NS5Wm3U6Kv4w7Cw2sDOV++vf2ecGfFV00x1+2Y3QcOY=
@@ -40,13 +40,13 @@ github.com/fluxcd/pkg/apis/kustomize v1.6.1 h1:22FJc69Mq4i8aCxnKPlddHhSMyI4UPkQk
github.com/fluxcd/pkg/apis/kustomize v1.6.1/go.mod h1:5dvQ4IZwz0hMGmuj8tTWGtarsuxW0rWsxJOwC6i+0V8=
github.com/fluxcd/pkg/apis/meta v1.6.1 h1:maLhcRJ3P/70ArLCY/LF/YovkxXbX+6sTWZwZQBeNq0=
github.com/fluxcd/pkg/apis/meta v1.6.1/go.mod h1:YndB/gxgGZmKfqpAfFxyCDNFJFP0ikpeJzs66jwq280=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
@@ -64,50 +64,48 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -115,111 +113,126 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/vmware-tanzu/velero v1.17.1 h1:ldKeiTuUwkThOw7zrUucNA1NwnLG66zl13YetWAoE0I=
github.com/vmware-tanzu/velero v1.17.1/go.mod h1:3KTxuUN6Un38JzmYAX+8U6j2k6EexGoNNxa8jrJML8U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0=
go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28=
go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q=
go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E=
go.etcd.io/etcd/client/v2 v2.305.13 h1:RWfV1SX5jTU0lbCvpVQe3iPQeAHETWdOTb6pxhd77C8=
go.etcd.io/etcd/client/v2 v2.305.13/go.mod h1:iQnL7fepbiomdXMb3om1rHq96htNNGv2sJkEcZGDRRg=
go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE=
go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50=
go.etcd.io/etcd/pkg/v3 v3.5.13 h1:st9bDWNsKkBNpP4PR1MvM/9NqUPfvYZx/YXegsYEH8M=
go.etcd.io/etcd/pkg/v3 v3.5.13/go.mod h1:N+4PLrp7agI/Viy+dUYpX7iRtSPvKq+w8Y14d1vX+m0=
go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA=
go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw=
go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok=
go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA=
go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE=
go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU=
go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg=
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -228,50 +241,48 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -281,39 +292,44 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0=
k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM=
k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4=
k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE=
k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA=
k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kms v0.31.2 h1:pyx7l2qVOkClzFMIWMVF/FxsSkgd+OIGH7DecpbscJI=
k8s.io/kms v0.31.2/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94=
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 h1:GKE9U8BH16uynoxQii0auTjmmmuZ3O0LFMN6S0lPPhI=
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
k8s.io/kms v0.34.1 h1:iCFOvewDPzWM9fMTfyIPO+4MeuZ0tcZbugxLNSHFG4w=
k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -0,0 +1,226 @@
#!/usr/bin/env bats
# Test variables - stored for teardown
TEST_NAMESPACE='tenant-test'
TEST_BUCKET_NAME='test-backup-bucket'
TEST_VM_NAME='test-backup-vm'
TEST_BACKUPJOB_NAME='test-backup-job'
teardown() {
# Clean up resources (runs even if test fails)
namespace="${TEST_NAMESPACE}"
bucket_name="${TEST_BUCKET_NAME}"
vm_name="${TEST_VM_NAME}"
backupjob_name="${TEST_BACKUPJOB_NAME}"
# Clean up port-forward if still running
pkill -f "kubectl.*port-forward.*seaweedfs-s3" 2>/dev/null || true
# Clean up Velero resources in cozy-velero namespace
# Find Velero backup by pattern matching namespace-backupjob
for backup in $(kubectl -n cozy-velero get backups.velero.io -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true); do
if echo "$backup" | grep -q "^${namespace}-${backupjob_name}-"; then
kubectl -n cozy-velero delete backups.velero.io ${backup} --wait=false 2>/dev/null || true
fi
done
# Clean up BackupStorageLocation and VolumeSnapshotLocation (named: namespace-backupjob)
BSL_NAME="${namespace}-${backupjob_name}"
kubectl -n cozy-velero delete backupstoragelocations.velero.io ${BSL_NAME} --wait=false 2>/dev/null || true
kubectl -n cozy-velero delete volumesnapshotlocations.velero.io ${BSL_NAME} --wait=false 2>/dev/null || true
# Clean up Velero credentials secret
SECRET_NAME="backup-${namespace}-${backupjob_name}-s3-credentials"
kubectl -n cozy-velero delete secret ${SECRET_NAME} --wait=false 2>/dev/null || true
# Clean up BackupJob
kubectl -n ${namespace} delete backupjob ${backupjob_name} --wait=false 2>/dev/null || true
# Clean up Virtual Machine
kubectl -n ${namespace} delete virtualmachines.apps.cozystack.io ${vm_name} --wait=false 2>/dev/null || true
# Clean up Bucket
kubectl -n ${namespace} delete bucket.apps.cozystack.io ${bucket_name} --wait=false 2>/dev/null || true
# Clean up temporary files
rm -f /tmp/bucket-backup-credentials.json
}
print_log() {
echo "# $1" >&3
}
@test "Create Backup for Virtual Machine" {
# Test variables
bucket_name="${TEST_BUCKET_NAME}"
vm_name="${TEST_VM_NAME}"
backupjob_name="${TEST_BACKUPJOB_NAME}"
namespace="${TEST_NAMESPACE}"
print_log "Step 0:Ensure BackupJob and Velero strategy CRDs are installed"
kubectl apply -f packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml
kubectl apply -f packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml
# Wait for CRDs to be ready
kubectl wait --for condition=established --timeout=30s crd backupjobs.backups.cozystack.io
kubectl wait --for condition=established --timeout=30s crd veleroes.strategy.backups.cozystack.io
# Ensure velero-strategy-default resource exists
kubectl apply -f packages/system/backup-controller/templates/strategy.yaml
print_log "Step 1: Create the bucket resource"
kubectl apply -f - <<EOF
apiVersion: apps.cozystack.io/v1alpha1
kind: Bucket
metadata:
name: ${bucket_name}
namespace: ${namespace}
spec: {}
EOF
print_log "Wait for the bucket to be ready"
kubectl -n ${namespace} wait hr bucket-${bucket_name} --timeout=100s --for=condition=ready
kubectl -n ${namespace} wait bucketclaims.objectstorage.k8s.io bucket-${bucket_name} --timeout=300s --for=jsonpath='{.status.bucketReady}'=true
kubectl -n ${namespace} wait bucketaccesses.objectstorage.k8s.io bucket-${bucket_name} --timeout=300s --for=jsonpath='{.status.accessGranted}'=true
# Get bucket credentials for later S3 verification
kubectl -n ${namespace} get secret bucket-${bucket_name} -ojsonpath='{.data.BucketInfo}' | base64 -d > /tmp/bucket-backup-credentials.json
ACCESS_KEY=$(jq -r '.spec.secretS3.accessKeyID' /tmp/bucket-backup-credentials.json)
SECRET_KEY=$(jq -r '.spec.secretS3.accessSecretKey' /tmp/bucket-backup-credentials.json)
BUCKET_NAME=$(jq -r '.spec.bucketName' /tmp/bucket-backup-credentials.json)
print_log "Step 2: Create the Virtual Machine"
kubectl apply -f - <<EOF
apiVersion: apps.cozystack.io/v1alpha1
kind: VirtualMachine
metadata:
name: ${vm_name}
namespace: ${namespace}
spec:
external: false
externalMethod: PortList
externalPorts:
- 22
instanceType: "u1.medium"
instanceProfile: ubuntu
systemDisk:
image: ubuntu
storage: 5Gi
storageClass: replicated
gpus: []
resources: {}
sshKeys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF
test@test
cloudInit: |
#cloud-config
users:
- name: test
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD: ALL']
groups: sudo
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF test@test
cloudInitSeed: ""
EOF
print_log "Wait for VM to be ready"
sleep 5
kubectl -n ${namespace} wait hr virtual-machine-${vm_name} --timeout=10s --for=condition=ready
kubectl -n ${namespace} wait dv virtual-machine-${vm_name} --timeout=150s --for=condition=ready
kubectl -n ${namespace} wait pvc virtual-machine-${vm_name} --timeout=100s --for=jsonpath='{.status.phase}'=Bound
kubectl -n ${namespace} wait vm virtual-machine-${vm_name} --timeout=100s --for=condition=ready
print_log "Step 3: Create BackupJob"
kubectl apply -f - <<EOF
apiVersion: backups.cozystack.io/v1alpha1
kind: BackupJob
metadata:
name: ${backupjob_name}
namespace: ${namespace}
labels:
backups.cozystack.io/triggered-by: e2e-test
spec:
applicationRef:
apiGroup: apps.cozystack.io
kind: VirtualMachine
name: ${vm_name}
storageRef:
apiGroup: apps.cozystack.io
kind: Bucket
name: ${bucket_name}
strategyRef:
apiGroup: strategy.backups.cozystack.io
kind: Velero
name: velero-strategy-default
EOF
print_log "Wait for BackupJob to start"
kubectl -n ${namespace} wait backupjob ${backupjob_name} --timeout=60s --for=jsonpath='{.status.phase}'=Running
print_log "Wait for BackupJob to complete"
kubectl -n ${namespace} wait backupjob ${backupjob_name} --timeout=300s --for=jsonpath='{.status.phase}'=Succeeded
print_log "Verify BackupJob status"
PHASE=$(kubectl -n ${namespace} get backupjob ${backupjob_name} -o jsonpath='{.status.phase}')
[ "$PHASE" = "Succeeded" ]
# Verify BackupJob has a backupRef
BACKUP_REF=$(kubectl -n ${namespace} get backupjob ${backupjob_name} -o jsonpath='{.status.backupRef.name}')
[ -n "$BACKUP_REF" ]
# Find the Velero backup by searching for backups matching the namespace-backupjob pattern
# Format: namespace-backupjob-timestamp
VELERO_BACKUP_NAME=""
VELERO_BACKUP_PHASE=""
print_log "Wait a bit for the backup to be created and appear in the API"
sleep 30
# Find backup by pattern matching namespace-backupjob
for backup in $(kubectl -n cozy-velero get backups.velero.io -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
if echo "$backup" | grep -q "^${namespace}-${backupjob_name}-"; then
VELERO_BACKUP_NAME=$backup
VELERO_BACKUP_PHASE=$(kubectl -n cozy-velero get backups.velero.io $backup -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
break
fi
done
print_log "Verify Velero Backup was found"
[ -n "$VELERO_BACKUP_NAME" ]
echo '# Wait for Velero Backup to complete' >&3
until kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' | grep -q 'Completed\|Failed'; do
sleep 5
done
print_log "Verify Velero Backup is Completed"
timeout 90 sh -ec "until [ \"\$(kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' 2>/dev/null)\" = \"Completed\" ]; do sleep 30; done"
# Final verification
VELERO_BACKUP_PHASE=$(kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
[ "$VELERO_BACKUP_PHASE" = "Completed" ]
print_log "Step 4: Verify S3 has backup data"
# Start port-forwarding to S3 service (with timeout to keep it alive)
bash -c 'timeout 100s kubectl port-forward service/seaweedfs-s3 -n tenant-root 8333:8333 > /dev/null 2>&1 &'
# Wait for port-forward to be ready
timeout 30 sh -ec "until nc -z localhost 8333; do sleep 1; done"
# Wait a bit for backup data to be written to S3
sleep 30
# Set up MinIO client with insecure flag (use environment variable for all commands)
export MC_INSECURE=1
mc alias set local https://localhost:8333 $ACCESS_KEY $SECRET_KEY
# Verify backup directory exists in S3
BACKUP_PATH="${BUCKET_NAME}/backups/${VELERO_BACKUP_NAME}"
mc ls local/${BACKUP_PATH}/ 2>/dev/null
[ $? -eq 0 ]
# Verify backup files exist (at least metadata files)
BACKUP_FILES=$(mc ls local/${BACKUP_PATH}/ 2>/dev/null | wc -l || echo "0")
[ "$BACKUP_FILES" -gt "0" ]
}

View File

@@ -21,14 +21,33 @@
}
@test "Test kinds" {
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.kind')
if [ "$val" != "TenantList" ]; then
echo "Expected kind to be TenantList, got $val"
exit 1
fi
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.items[0].kind')
if [ "$val" != "Tenant" ]; then
echo "Expected kind to be Tenant, got $val"
exit 1
fi
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.kind')
if [ "$val" != "IngressList" ]; then
echo "Expected kind to be IngressList, got $val"
exit 1
fi
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.items[0].kind')
if [ "$val" != "Ingress" ]; then
echo "Expected kind to be Ingress, got $val"
exit 1
fi
}
@test "Create and delete namespace" {
kubectl create ns cozy-test-create-and-delete-namespace --dry-run=client -o yaml | kubectl apply -f -
if ! kubectl delete ns cozy-test-create-and-delete-namespace; then
echo "Failed to delete namespace"
kubectl describe ns cozy-test-create-and-delete-namespace
exit 1
fi
}

View File

@@ -23,6 +23,13 @@ CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-
API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-rules"}"
UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}"
CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4"
TMPDIR=$(mktemp -d)
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/crds
COZY_RD_CRDDIR=packages/system/cozystack-resource-definition-crd/definition
BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions
BACKUPSTRATEGY_CRDDIR=packages/system/backupstrategy-controller/definitions
trap 'rm -rf ${TMPDIR}' EXIT
source "${CODEGEN_PKG}/kube_codegen.sh"
@@ -53,6 +60,12 @@ kube::codegen::gen_openapi \
"${SCRIPT_ROOT}/pkg/apis"
$CONTROLLER_GEN object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=packages/system/cozystack-controller/crds
mv packages/system/cozystack-controller/crds/cozystack.io_cozystackresourcedefinitions.yaml \
packages/system/cozystack-resource-definition-crd/definition/cozystack.io_cozystackresourcedefinitions.yaml
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=${TMPDIR}
mv ${TMPDIR}/cozystack.io_cozystackresourcedefinitions.yaml \
${COZY_RD_CRDDIR}/cozystack.io_cozystackresourcedefinitions.yaml
mv ${TMPDIR}/backups.cozystack.io*.yaml ${BACKUPS_CORE_CRDDIR}/
mv ${TMPDIR}/strategy.backups.cozystack.io*.yaml ${BACKUPSTRATEGY_CRDDIR}/
mv ${TMPDIR}/*.yaml ${COZY_CONTROLLER_CRDDIR}/

View File

@@ -0,0 +1,111 @@
package backupcontroller
import (
"context"
"fmt"
"net/http"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/log"
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
)
// BackupVeleroStrategyReconciler reconciles BackupJob with a strategy referencing
// Velero.strategy.backups.cozystack.io objects.
type BackupJobReconciler struct {
client.Client
dynamic.Interface
meta.RESTMapper
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
func (r *BackupJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("reconciling BackupJob", "namespace", req.Namespace, "name", req.Name)
j := &backupsv1alpha1.BackupJob{}
err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, j)
if err != nil {
if apierrors.IsNotFound(err) {
logger.V(1).Info("BackupJob not found, skipping")
return ctrl.Result{}, nil
}
logger.Error(err, "failed to get BackupJob")
return ctrl.Result{}, err
}
if j.Spec.StrategyRef.APIGroup == nil {
return r.markBackupJobFailed(ctx, j, "StrategyRef.APIGroup is nil")
}
if *j.Spec.StrategyRef.APIGroup != strategyv1alpha1.GroupVersion.Group {
// skip if the strategy group foreign to this controller
return ctrl.Result{}, nil
}
logger.Info("processing BackupJob", "backupjob", j.Name, "strategyKind", j.Spec.StrategyRef.Kind)
switch j.Spec.StrategyRef.Kind {
case strategyv1alpha1.JobStrategyKind:
return r.reconcileJob(ctx, j)
case strategyv1alpha1.VeleroStrategyKind:
return r.reconcileVelero(ctx, j)
default:
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("StrategyRef.Kind not supported: %s", j.Spec.StrategyRef.Kind))
}
}
// SetupWithManager registers our controller with the Manager and sets up watches.
func (r *BackupJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
cfg := mgr.GetConfig()
var err error
if r.Interface, err = dynamic.NewForConfig(cfg); err != nil {
return err
}
var h *http.Client
if h, err = rest.HTTPClientFor(cfg); err != nil {
return err
}
if r.RESTMapper, err = apiutil.NewDynamicRESTMapper(cfg, h); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&backupsv1alpha1.BackupJob{}).
Complete(r)
}
func (r *BackupJobReconciler) markBackupJobFailed(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, message string) (ctrl.Result, error) {
logger := getLogger(ctx)
now := metav1.Now()
backupJob.Status.CompletedAt = &now
backupJob.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed
backupJob.Status.Message = message
// Add condition
backupJob.Status.Conditions = append(backupJob.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "BackupFailed",
Message: message,
LastTransitionTime: now,
})
if err := r.Status().Update(ctx, backupJob); err != nil {
logger.Error(err, "failed to update BackupJob status to Failed")
return ctrl.Result{}, err
}
logger.Debug("BackupJob failed", "message", message)
return ctrl.Result{}, nil
}

View File

@@ -0,0 +1,28 @@
package factory
import (
"fmt"
"time"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func BackupJob(p *backupsv1alpha1.Plan, scheduledFor time.Time) *backupsv1alpha1.BackupJob {
job := &backupsv1alpha1.BackupJob{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%d", p.Name, scheduledFor.Unix()/60),
Namespace: p.Namespace,
},
Spec: backupsv1alpha1.BackupJobSpec{
PlanRef: &corev1.LocalObjectReference{
Name: p.Name,
},
ApplicationRef: *p.Spec.ApplicationRef.DeepCopy(),
StorageRef: *p.Spec.StorageRef.DeepCopy(),
StrategyRef: *p.Spec.StrategyRef.DeepCopy(),
},
}
return job
}

View File

@@ -0,0 +1,20 @@
package backupcontroller
import (
"context"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
)
func (r *BackupJobReconciler) reconcileJob(ctx context.Context, j *backupsv1alpha1.BackupJob) (ctrl.Result, error) {
_ = log.FromContext(ctx)
return ctrl.Result{}, nil
}
func (r *RestoreJobReconciler) reconcileJobRestore(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) (ctrl.Result, error) {
_ = log.FromContext(ctx)
return ctrl.Result{}, nil
}

View File

@@ -0,0 +1,104 @@
package backupcontroller
import (
"context"
"fmt"
"time"
cron "github.com/robfig/cron/v3"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
"github.com/cozystack/cozystack/internal/backupcontroller/factory"
)
const (
minRequeueDelay = 30 * time.Second
startingDeadlineSeconds = 300 * time.Second
)
// PlanReconciler reconciles a Plan object
type PlanReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *PlanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
log.V(2).Info("reconciling")
p := &backupsv1alpha1.Plan{}
if err := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, p); err != nil {
if apierrors.IsNotFound(err) {
log.V(3).Info("Plan not found")
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
tCheck := time.Now().Add(-startingDeadlineSeconds)
sch, err := cron.ParseStandard(p.Spec.Schedule.Cron)
if err != nil {
errWrapped := fmt.Errorf("could not parse cron %s: %w", p.Spec.Schedule.Cron, err)
log.Error(err, "could not parse cron", "cron", p.Spec.Schedule.Cron)
meta.SetStatusCondition(&p.Status.Conditions, metav1.Condition{
Type: backupsv1alpha1.PlanConditionError,
Status: metav1.ConditionTrue,
Reason: "Failed to parse cron spec",
Message: errWrapped.Error(),
})
if err := r.Status().Update(ctx, p); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
// Clear error condition if cron parsing succeeds
if condition := meta.FindStatusCondition(p.Status.Conditions, backupsv1alpha1.PlanConditionError); condition != nil && condition.Status == metav1.ConditionTrue {
meta.SetStatusCondition(&p.Status.Conditions, metav1.Condition{
Type: backupsv1alpha1.PlanConditionError,
Status: metav1.ConditionFalse,
Reason: "Cron spec is valid",
Message: "The cron schedule has been successfully parsed",
})
if err := r.Status().Update(ctx, p); err != nil {
return ctrl.Result{}, err
}
}
tNext := sch.Next(tCheck)
if time.Now().Before(tNext) {
return ctrl.Result{RequeueAfter: tNext.Sub(time.Now())}, nil
}
job := factory.BackupJob(p, tNext)
if err := controllerutil.SetControllerReference(p, job, r.Scheme); err != nil {
return ctrl.Result{}, err
}
if err := r.Create(ctx, job); err != nil {
if apierrors.IsAlreadyExists(err) {
return ctrl.Result{RequeueAfter: startingDeadlineSeconds}, nil
}
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: startingDeadlineSeconds}, nil
}
// SetupWithManager registers our controller with the Manager and sets up watches.
func (r *PlanReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&backupsv1alpha1.Plan{}).
Complete(r)
}

View File

@@ -0,0 +1,139 @@
package backupcontroller
import (
"context"
"fmt"
"net/http"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/log"
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
)
// RestoreJobReconciler reconciles RestoreJob objects.
// It routes RestoreJobs to strategy-specific handlers based on the strategy
// referenced in the Backup that the RestoreJob is restoring from.
type RestoreJobReconciler struct {
client.Client
dynamic.Interface
meta.RESTMapper
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
func (r *RestoreJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("reconciling RestoreJob", "namespace", req.Namespace, "name", req.Name)
restoreJob := &backupsv1alpha1.RestoreJob{}
err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, restoreJob)
if err != nil {
if apierrors.IsNotFound(err) {
logger.V(1).Info("RestoreJob not found, skipping")
return ctrl.Result{}, nil
}
logger.Error(err, "failed to get RestoreJob")
return ctrl.Result{}, err
}
// If already completed, no need to reconcile
if restoreJob.Status.Phase == backupsv1alpha1.RestoreJobPhaseSucceeded ||
restoreJob.Status.Phase == backupsv1alpha1.RestoreJobPhaseFailed {
logger.V(1).Info("RestoreJob already completed, skipping", "phase", restoreJob.Status.Phase)
return ctrl.Result{}, nil
}
// Step 1: Fetch the referenced Backup
backup := &backupsv1alpha1.Backup{}
backupKey := types.NamespacedName{Namespace: req.Namespace, Name: restoreJob.Spec.BackupRef.Name}
if err := r.Get(ctx, backupKey, backup); err != nil {
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("failed to get Backup: %v", err))
}
// Step 2: Determine effective strategy from backup.spec.strategyRef
if backup.Spec.StrategyRef.APIGroup == nil {
return r.markRestoreJobFailed(ctx, restoreJob, "Backup has nil StrategyRef.APIGroup")
}
if *backup.Spec.StrategyRef.APIGroup != strategyv1alpha1.GroupVersion.Group {
return r.markRestoreJobFailed(ctx, restoreJob,
fmt.Sprintf("StrategyRef.APIGroup doesn't match: %s", *backup.Spec.StrategyRef.APIGroup))
}
logger.Info("processing RestoreJob", "restorejob", restoreJob.Name, "backup", backup.Name, "strategyKind", backup.Spec.StrategyRef.Kind)
switch backup.Spec.StrategyRef.Kind {
case strategyv1alpha1.JobStrategyKind:
return r.reconcileJobRestore(ctx, restoreJob, backup)
case strategyv1alpha1.VeleroStrategyKind:
return r.reconcileVeleroRestore(ctx, restoreJob, backup)
default:
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("StrategyRef.Kind not supported: %s", backup.Spec.StrategyRef.Kind))
}
}
// SetupWithManager registers our controller with the Manager and sets up watches.
func (r *RestoreJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
cfg := mgr.GetConfig()
var err error
if r.Interface, err = dynamic.NewForConfig(cfg); err != nil {
return err
}
var h *http.Client
if h, err = rest.HTTPClientFor(cfg); err != nil {
return err
}
if r.RESTMapper, err = apiutil.NewDynamicRESTMapper(cfg, h); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&backupsv1alpha1.RestoreJob{}).
Complete(r)
}
// getTargetApplicationRef determines the effective target application reference.
// According to DESIGN.md, if spec.targetApplicationRef is omitted, drivers SHOULD
// restore into backup.spec.applicationRef.
func (r *RestoreJobReconciler) getTargetApplicationRef(restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) corev1.TypedLocalObjectReference {
if restoreJob.Spec.TargetApplicationRef != nil {
return *restoreJob.Spec.TargetApplicationRef
}
return backup.Spec.ApplicationRef
}
// markRestoreJobFailed updates the RestoreJob status to Failed with the given message.
func (r *RestoreJobReconciler) markRestoreJobFailed(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob, message string) (ctrl.Result, error) {
logger := getLogger(ctx)
now := metav1.Now()
restoreJob.Status.CompletedAt = &now
restoreJob.Status.Phase = backupsv1alpha1.RestoreJobPhaseFailed
restoreJob.Status.Message = message
// Add condition
restoreJob.Status.Conditions = append(restoreJob.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "RestoreFailed",
Message: message,
LastTransitionTime: now,
})
if err := r.Status().Update(ctx, restoreJob); err != nil {
logger.Error(err, "failed to update RestoreJob status to Failed")
return ctrl.Result{}, err
}
logger.Debug("RestoreJob failed", "message", message)
return ctrl.Result{}, nil
}

View File

@@ -0,0 +1,771 @@
package backupcontroller
import (
"context"
"encoding/json"
"fmt"
"reflect"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
"github.com/cozystack/cozystack/internal/template"
"github.com/go-logr/logr"
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
)
func getLogger(ctx context.Context) loggerWithDebug {
return loggerWithDebug{Logger: log.FromContext(ctx)}
}
// loggerWithDebug wraps a logr.Logger and provides a Debug() method
// that maps to V(1).Info() for convenience.
type loggerWithDebug struct {
logr.Logger
}
// Debug logs at debug level (equivalent to V(1).Info())
func (l loggerWithDebug) Debug(msg string, keysAndValues ...interface{}) {
l.Logger.V(1).Info(msg, keysAndValues...)
}
// S3Credentials holds the discovered S3 credentials from a Bucket storageRef
type S3Credentials struct {
BucketName string
Endpoint string
Region string
AccessKeyID string
AccessSecretKey string
}
// bucketInfo represents the structure of BucketInfo stored in the secret
type bucketInfo struct {
Spec struct {
BucketName string `json:"bucketName"`
SecretS3 struct {
Endpoint string `json:"endpoint"`
Region string `json:"region"`
AccessKeyID string `json:"accessKeyID"`
AccessSecretKey string `json:"accessSecretKey"`
} `json:"secretS3"`
} `json:"spec"`
}
const (
defaultRequeueAfter = 5 * time.Second
defaultActiveJobPollingInterval = defaultRequeueAfter
defaultRestoreRequeueAfter = 5 * time.Second
defaultActiveRestorePollingInterval = defaultRestoreRequeueAfter
// Velero requires API objects and secrets to be in the cozy-velero namespace
veleroNamespace = "cozy-velero"
veleroBackupNameMetadataKey = "velero.io/backup-name"
veleroBackupNamespaceMetadataKey = "velero.io/backup-namespace"
)
func storageS3SecretName(namespace, backupJobName string) string {
return fmt.Sprintf("backup-%s-%s-s3-credentials", namespace, backupJobName)
}
func boolPtr(b bool) *bool {
return &b
}
func (r *BackupJobReconciler) reconcileVelero(ctx context.Context, j *backupsv1alpha1.BackupJob) (ctrl.Result, error) {
logger := getLogger(ctx)
logger.Debug("reconciling Velero strategy", "backupjob", j.Name, "phase", j.Status.Phase)
// If already completed, no need to reconcile
if j.Status.Phase == backupsv1alpha1.BackupJobPhaseSucceeded ||
j.Status.Phase == backupsv1alpha1.BackupJobPhaseFailed {
logger.Debug("BackupJob already completed, skipping", "phase", j.Status.Phase)
return ctrl.Result{}, nil
}
// Step 1: On first reconcile, set startedAt (but not phase yet - phase will be set after backup creation)
logger.Debug("checking BackupJob status", "startedAt", j.Status.StartedAt, "phase", j.Status.Phase)
if j.Status.StartedAt == nil {
logger.Debug("setting BackupJob StartedAt")
now := metav1.Now()
j.Status.StartedAt = &now
// Don't set phase to Running yet - will be set after Velero backup is successfully created
if err := r.Status().Update(ctx, j); err != nil {
logger.Error(err, "failed to update BackupJob status")
return ctrl.Result{}, err
}
logger.Debug("set BackupJob StartedAt", "startedAt", j.Status.StartedAt)
} else {
logger.Debug("BackupJob already started", "startedAt", j.Status.StartedAt, "phase", j.Status.Phase)
}
// Step 2: Resolve inputs - Read Strategy, Storage, Application, optionally Plan
logger.Debug("fetching Velero strategy", "strategyName", j.Spec.StrategyRef.Name)
veleroStrategy := &strategyv1alpha1.Velero{}
if err := r.Get(ctx, client.ObjectKey{Name: j.Spec.StrategyRef.Name}, veleroStrategy); err != nil {
if errors.IsNotFound(err) {
logger.Error(err, "Velero strategy not found", "strategyName", j.Spec.StrategyRef.Name)
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("Velero strategy not found: %s", j.Spec.StrategyRef.Name))
}
logger.Error(err, "failed to get Velero strategy")
return ctrl.Result{}, err
}
logger.Debug("fetched Velero strategy", "strategyName", veleroStrategy.Name)
// Step 3: Execute backup logic
// Check if we already created a Velero Backup
if j.Status.StartedAt == nil {
logger.Error(nil, "StartedAt is nil after status update, this should not happen")
return ctrl.Result{RequeueAfter: defaultRequeueAfter}, nil
}
logger.Debug("checking for existing Velero Backup", "namespace", veleroNamespace)
veleroBackupList := &velerov1.BackupList{}
opts := []client.ListOption{
client.InNamespace(veleroNamespace),
client.MatchingLabels{
backupsv1alpha1.OwningJobNamespaceLabel: j.Namespace,
backupsv1alpha1.OwningJobNameLabel: j.Name,
},
}
if err := r.List(ctx, veleroBackupList, opts...); err != nil {
logger.Error(err, "failed to get Velero Backup")
return ctrl.Result{}, err
}
if len(veleroBackupList.Items) == 0 {
// Create Velero Backup
logger.Debug("Velero Backup not found, creating new one")
if err := r.createVeleroBackup(ctx, j, veleroStrategy); err != nil {
logger.Error(err, "failed to create Velero Backup")
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to create Velero Backup: %v", err))
}
// After successful Velero backup creation, set phase to Running
if j.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
logger.Debug("setting BackupJob phase to Running after successful Velero backup creation")
j.Status.Phase = backupsv1alpha1.BackupJobPhaseRunning
if err := r.Status().Update(ctx, j); err != nil {
logger.Error(err, "failed to update BackupJob phase to Running")
return ctrl.Result{}, err
}
}
logger.Debug("created Velero Backup, requeuing")
// Requeue to check status
return ctrl.Result{RequeueAfter: defaultRequeueAfter}, nil
}
if len(veleroBackupList.Items) > 1 {
logger.Error(fmt.Errorf("too many Velero backups for BackupJob"), "found more than one Velero Backup referencing a single BackupJob as owner")
j.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed
if err := r.Status().Update(ctx, j); err != nil {
logger.Error(err, "failed to update BackupJob status")
}
return ctrl.Result{}, nil
}
veleroBackup := veleroBackupList.Items[0].DeepCopy()
logger.Debug("found existing Velero Backup", "phase", veleroBackup.Status.Phase)
// If Velero backup exists but phase is not Running, set it to Running
// This handles the case where the backup was created but phase wasn't set yet
if j.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
logger.Debug("setting BackupJob phase to Running (Velero backup already exists)")
j.Status.Phase = backupsv1alpha1.BackupJobPhaseRunning
if err := r.Status().Update(ctx, j); err != nil {
logger.Error(err, "failed to update BackupJob phase to Running")
return ctrl.Result{}, err
}
}
// Check Velero Backup status
phase := string(veleroBackup.Status.Phase)
if phase == "" {
// Still in progress, requeue
return ctrl.Result{RequeueAfter: defaultActiveJobPollingInterval}, nil
}
// Step 4: On success - Create Backup resource and update status
if phase == "Completed" {
// Check if we already created the Backup resource
if j.Status.BackupRef == nil {
backup, err := r.createBackupResource(ctx, j, veleroBackup)
if err != nil {
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to create Backup resource: %v", err))
}
now := metav1.Now()
j.Status.BackupRef = &corev1.LocalObjectReference{Name: backup.Name}
j.Status.CompletedAt = &now
j.Status.Phase = backupsv1alpha1.BackupJobPhaseSucceeded
if err := r.Status().Update(ctx, j); err != nil {
logger.Error(err, "failed to update BackupJob status")
return ctrl.Result{}, err
}
logger.Debug("BackupJob succeeded", "backup", backup.Name)
}
return ctrl.Result{}, nil
}
// Step 5: On failure
if phase == "Failed" || phase == "PartiallyFailed" {
message := fmt.Sprintf("Velero Backup failed with phase: %s", phase)
if len(veleroBackup.Status.ValidationErrors) > 0 {
message = fmt.Sprintf("%s: %v", message, veleroBackup.Status.ValidationErrors)
}
return r.markBackupJobFailed(ctx, j, message)
}
// Still in progress (InProgress, New, etc.)
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}
// resolveBucketStorageRef discovers S3 credentials from a Bucket storageRef
// It follows this flow:
// 1. Get the Bucket resource (apps.cozystack.io/v1alpha1)
// 2. Find the BucketAccess that references this bucket
// 3. Get the secret from BucketAccess.spec.credentialsSecretName
// 4. Decode BucketInfo from secret.data.BucketInfo and extract S3 credentials
func (r *BackupJobReconciler) resolveBucketStorageRef(ctx context.Context, storageRef corev1.TypedLocalObjectReference, namespace string) (*S3Credentials, error) {
logger := getLogger(ctx)
// Step 1: Get the Bucket resource
bucket := &unstructured.Unstructured{}
bucket.SetGroupVersionKind(schema.GroupVersionKind{
Group: *storageRef.APIGroup,
Version: "v1alpha1",
Kind: storageRef.Kind,
})
if *storageRef.APIGroup != "apps.cozystack.io" {
return nil, fmt.Errorf("Unsupported storage APIGroup: %v, expected apps.cozystack.io", storageRef.APIGroup)
}
bucketKey := client.ObjectKey{Namespace: namespace, Name: storageRef.Name}
if err := r.Get(ctx, bucketKey, bucket); err != nil {
return nil, fmt.Errorf("failed to get Bucket %s: %w", storageRef.Name, err)
}
// Step 2: Determine the bucket claim name
// For apps.cozystack.io Bucket, the BucketClaim name is typically the same as the Bucket name
// or follows a pattern. Based on the templates, it's usually the Release.Name which equals the Bucket name
bucketName := storageRef.Name
// Step 3: Get BucketAccess by name (assuming BucketAccess name matches bucketName)
bucketAccess := &unstructured.Unstructured{}
bucketAccess.SetGroupVersionKind(schema.GroupVersionKind{
Group: "objectstorage.k8s.io",
Version: "v1alpha1",
Kind: "BucketAccess",
})
bucketAccessKey := client.ObjectKey{Name: "bucket-" + bucketName, Namespace: namespace}
if err := r.Get(ctx, bucketAccessKey, bucketAccess); err != nil {
return nil, fmt.Errorf("failed to get BucketAccess %s in namespace %s: %w", bucketName, namespace, err)
}
// Step 4: Get the secret name from BucketAccess
secretName, found, err := unstructured.NestedString(bucketAccess.Object, "spec", "credentialsSecretName")
if err != nil {
return nil, fmt.Errorf("failed to get credentialsSecretName from BucketAccess: %w", err)
}
if !found || secretName == "" {
return nil, fmt.Errorf("credentialsSecretName not found in BucketAccess %s", bucketAccessKey.Name)
}
// Step 5: Get the secret
secret := &corev1.Secret{}
secretKey := client.ObjectKey{Namespace: namespace, Name: secretName}
if err := r.Get(ctx, secretKey, secret); err != nil {
return nil, fmt.Errorf("failed to get secret %s: %w", secretName, err)
}
// Step 6: Decode BucketInfo from secret.data.BucketInfo
bucketInfoData, found := secret.Data["BucketInfo"]
if !found {
return nil, fmt.Errorf("BucketInfo key not found in secret %s", secretName)
}
// Parse JSON value
var info bucketInfo
if err := json.Unmarshal(bucketInfoData, &info); err != nil {
return nil, fmt.Errorf("failed to unmarshal BucketInfo from secret %s: %w", secretName, err)
}
// Step 7: Extract and return S3 credentials
creds := &S3Credentials{
BucketName: info.Spec.BucketName,
Endpoint: info.Spec.SecretS3.Endpoint,
Region: info.Spec.SecretS3.Region,
AccessKeyID: info.Spec.SecretS3.AccessKeyID,
AccessSecretKey: info.Spec.SecretS3.AccessSecretKey,
}
logger.Debug("resolved S3 credentials from Bucket storageRef",
"bucket", storageRef.Name,
"bucketName", creds.BucketName,
"endpoint", creds.Endpoint)
return creds, nil
}
// createS3CredsForVelero creates or updates a Kubernetes Secret containing
// Velero S3 credentials in the format expected by Velero's cloud-credentials plugin.
func (r *BackupJobReconciler) createS3CredsForVelero(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, creds *S3Credentials) error {
logger := getLogger(ctx)
secretName := storageS3SecretName(backupJob.Namespace, backupJob.Name)
secretNamespace := veleroNamespace
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: secretNamespace,
},
Type: corev1.SecretTypeOpaque,
StringData: map[string]string{
"cloud": fmt.Sprintf(`[default]
aws_access_key_id=%s
aws_secret_access_key=%s
services = seaweed-s3
[services seaweed-s3]
s3 =
endpoint_url = %s
`, creds.AccessKeyID, creds.AccessSecretKey, creds.Endpoint),
},
}
foundSecret := &corev1.Secret{}
secretKey := client.ObjectKey{Name: secretName, Namespace: secretNamespace}
err := r.Get(ctx, secretKey, foundSecret)
if err != nil && errors.IsNotFound(err) {
// Create the Secret
if err := r.Create(ctx, secret); err != nil {
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "SecretCreationFailed",
fmt.Sprintf("Failed to create Velero credentials secret %s/%s: %v", secretNamespace, secretName, err))
return fmt.Errorf("failed to create Velero credentials secret: %w", err)
}
logger.Debug("created Velero credentials secret", "secret", secretName)
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "SecretCreated",
fmt.Sprintf("Created Velero credentials secret %s/%s", secretNamespace, secretName))
} else if err == nil {
// Update if necessary - only update if the secret data has actually changed
// Compare the new secret data with existing secret data
existingData := foundSecret.Data
if existingData == nil {
existingData = make(map[string][]byte)
}
newData := make(map[string][]byte)
for k, v := range secret.StringData {
newData[k] = []byte(v)
}
// Check if data has changed
dataChanged := false
if len(existingData) != len(newData) {
dataChanged = true
} else {
for k, newVal := range newData {
existingVal, exists := existingData[k]
if !exists || !reflect.DeepEqual(existingVal, newVal) {
dataChanged = true
break
}
}
}
if dataChanged {
foundSecret.StringData = secret.StringData
foundSecret.Data = nil // Clear .Data so .StringData will be used
if err := r.Update(ctx, foundSecret); err != nil {
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "SecretUpdateFailed",
fmt.Sprintf("Failed to update Velero credentials secret %s/%s: %v", secretNamespace, secretName, err))
return fmt.Errorf("failed to update Velero credentials secret: %w", err)
}
logger.Debug("updated Velero credentials secret", "secret", secretName)
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "SecretUpdated",
fmt.Sprintf("Updated Velero credentials secret %s/%s", secretNamespace, secretName))
} else {
logger.Debug("Velero credentials secret data unchanged, skipping update", "secret", secretName)
}
} else if err != nil {
return fmt.Errorf("error checking for existing Velero credentials secret: %w", err)
}
return nil
}
// createBackupStorageLocation creates or updates a Velero BackupStorageLocation resource.
func (r *BackupJobReconciler) createBackupStorageLocation(ctx context.Context, bsl *velerov1.BackupStorageLocation) error {
logger := getLogger(ctx)
foundBSL := &velerov1.BackupStorageLocation{}
bslKey := client.ObjectKey{Name: bsl.Name, Namespace: bsl.Namespace}
err := r.Get(ctx, bslKey, foundBSL)
if err != nil && errors.IsNotFound(err) {
// Create the BackupStorageLocation
if err := r.Create(ctx, bsl); err != nil {
return fmt.Errorf("failed to create BackupStorageLocation: %w", err)
}
logger.Debug("created BackupStorageLocation", "name", bsl.Name, "namespace", bsl.Namespace)
} else if err == nil {
// Update if necessary - use patch to avoid conflicts with Velero's status updates
// Only update if the spec has actually changed
if !reflect.DeepEqual(foundBSL.Spec, bsl.Spec) {
// Retry on conflict since Velero may be updating status concurrently
for i := 0; i < 3; i++ {
if err := r.Get(ctx, bslKey, foundBSL); err != nil {
return fmt.Errorf("failed to get BackupStorageLocation for update: %w", err)
}
foundBSL.Spec = bsl.Spec
if err := r.Update(ctx, foundBSL); err != nil {
if errors.IsConflict(err) && i < 2 {
logger.Debug("conflict updating BackupStorageLocation, retrying", "attempt", i+1)
time.Sleep(100 * time.Millisecond)
continue
}
return fmt.Errorf("failed to update BackupStorageLocation: %w", err)
}
logger.Debug("updated BackupStorageLocation", "name", bsl.Name, "namespace", bsl.Namespace)
return nil
}
} else {
logger.Debug("BackupStorageLocation spec unchanged, skipping update", "name", bsl.Name, "namespace", bsl.Namespace)
}
} else if err != nil {
return fmt.Errorf("error checking for existing BackupStorageLocation: %w", err)
}
return nil
}
// createVolumeSnapshotLocation creates or updates a Velero VolumeSnapshotLocation resource.
func (r *BackupJobReconciler) createVolumeSnapshotLocation(ctx context.Context, vsl *velerov1.VolumeSnapshotLocation) error {
logger := getLogger(ctx)
foundVSL := &velerov1.VolumeSnapshotLocation{}
vslKey := client.ObjectKey{Name: vsl.Name, Namespace: vsl.Namespace}
err := r.Get(ctx, vslKey, foundVSL)
if err != nil && errors.IsNotFound(err) {
// Create the VolumeSnapshotLocation
if err := r.Create(ctx, vsl); err != nil {
return fmt.Errorf("failed to create VolumeSnapshotLocation: %w", err)
}
logger.Debug("created VolumeSnapshotLocation", "name", vsl.Name, "namespace", vsl.Namespace)
} else if err == nil {
// Update if necessary - only update if the spec has actually changed
if !reflect.DeepEqual(foundVSL.Spec, vsl.Spec) {
// Retry on conflict since Velero may be updating status concurrently
for i := 0; i < 3; i++ {
if err := r.Get(ctx, vslKey, foundVSL); err != nil {
return fmt.Errorf("failed to get VolumeSnapshotLocation for update: %w", err)
}
foundVSL.Spec = vsl.Spec
if err := r.Update(ctx, foundVSL); err != nil {
if errors.IsConflict(err) && i < 2 {
logger.Debug("conflict updating VolumeSnapshotLocation, retrying", "attempt", i+1)
time.Sleep(100 * time.Millisecond)
continue
}
return fmt.Errorf("failed to update VolumeSnapshotLocation: %w", err)
}
logger.Debug("updated VolumeSnapshotLocation", "name", vsl.Name, "namespace", vsl.Namespace)
return nil
}
} else {
logger.Debug("VolumeSnapshotLocation spec unchanged, skipping update", "name", vsl.Name, "namespace", vsl.Namespace)
}
} else if err != nil {
return fmt.Errorf("error checking for existing VolumeSnapshotLocation: %w", err)
}
return nil
}
func (r *BackupJobReconciler) createVeleroBackup(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, strategy *strategyv1alpha1.Velero) error {
logger := getLogger(ctx)
logger.Debug("createVeleroBackup called", "strategy", strategy.Name)
mapping, err := r.RESTMapping(schema.GroupKind{Group: *backupJob.Spec.ApplicationRef.APIGroup, Kind: backupJob.Spec.ApplicationRef.Kind})
if err != nil {
return err
}
ns := backupJob.Namespace
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
ns = ""
}
app, err := r.Resource(mapping.Resource).Namespace(ns).Get(ctx, backupJob.Spec.ApplicationRef.Name, metav1.GetOptions{})
if err != nil {
return err
}
veleroBackupSpec, err := template.Template(&strategy.Spec.Template.Spec, app.Object)
if err != nil {
return err
}
veleroBackup := &velerov1.Backup{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s.%s-", backupJob.Namespace, backupJob.Name),
Namespace: veleroNamespace,
Labels: map[string]string{
backupsv1alpha1.OwningJobNameLabel: backupJob.Name,
backupsv1alpha1.OwningJobNamespaceLabel: backupJob.Namespace,
},
},
Spec: *veleroBackupSpec,
}
name := veleroBackup.GenerateName
if err := r.Create(ctx, veleroBackup); err != nil {
if veleroBackup.Name != "" {
name = veleroBackup.Name
}
logger.Error(err, "failed to create Velero Backup", "name", veleroBackup.Name)
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "VeleroBackupCreationFailed",
fmt.Sprintf("Failed to create Velero Backup %s/%s: %v", veleroNamespace, name, err))
return err
}
logger.Debug("created Velero Backup", "name", veleroBackup.Name, "namespace", veleroBackup.Namespace)
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "VeleroBackupCreated",
fmt.Sprintf("Created Velero Backup %s/%s", veleroNamespace, name))
return nil
}
func (r *BackupJobReconciler) createBackupResource(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, veleroBackup *velerov1.Backup) (*backupsv1alpha1.Backup, error) {
logger := getLogger(ctx)
// Extract artifact information from Velero Backup
// Create a basic artifact referencing the Velero backup
artifact := &backupsv1alpha1.BackupArtifact{
URI: fmt.Sprintf("velero://%s/%s", backupJob.Namespace, veleroBackup.Name),
}
// Get takenAt from Velero Backup creation timestamp or status
takenAt := metav1.Now()
if veleroBackup.Status.StartTimestamp != nil {
takenAt = *veleroBackup.Status.StartTimestamp
} else if !veleroBackup.CreationTimestamp.IsZero() {
takenAt = veleroBackup.CreationTimestamp
}
// Extract driver metadata (e.g., Velero backup name)
driverMetadata := map[string]string{
veleroBackupNameMetadataKey: veleroBackup.Name,
veleroBackupNamespaceMetadataKey: veleroBackup.Namespace,
}
backup := &backupsv1alpha1.Backup{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s", backupJob.Name),
Namespace: backupJob.Namespace,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: backupJob.APIVersion,
Kind: backupJob.Kind,
Name: backupJob.Name,
UID: backupJob.UID,
Controller: boolPtr(true),
},
},
},
Spec: backupsv1alpha1.BackupSpec{
ApplicationRef: backupJob.Spec.ApplicationRef,
StorageRef: backupJob.Spec.StorageRef,
StrategyRef: backupJob.Spec.StrategyRef,
TakenAt: takenAt,
DriverMetadata: driverMetadata,
},
Status: backupsv1alpha1.BackupStatus{
Phase: backupsv1alpha1.BackupPhaseReady,
},
}
if backupJob.Spec.PlanRef != nil {
backup.Spec.PlanRef = backupJob.Spec.PlanRef
}
if artifact != nil {
backup.Status.Artifact = artifact
}
if err := r.Create(ctx, backup); err != nil {
logger.Error(err, "failed to create Backup resource")
return nil, err
}
logger.Debug("created Backup resource", "name", backup.Name)
return backup, nil
}
// reconcileVeleroRestore handles restore operations for Velero strategy.
func (r *RestoreJobReconciler) reconcileVeleroRestore(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) (ctrl.Result, error) {
logger := getLogger(ctx)
logger.Debug("reconciling Velero strategy restore", "restorejob", restoreJob.Name, "backup", backup.Name)
// Step 1: On first reconcile, set startedAt and phase = Running
if restoreJob.Status.StartedAt == nil {
logger.Debug("setting RestoreJob StartedAt and phase to Running")
now := metav1.Now()
restoreJob.Status.StartedAt = &now
restoreJob.Status.Phase = backupsv1alpha1.RestoreJobPhaseRunning
if err := r.Status().Update(ctx, restoreJob); err != nil {
logger.Error(err, "failed to update RestoreJob status")
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: defaultRestoreRequeueAfter}, nil
}
// Step 2: Resolve inputs - Read Strategy, Storage, target Application
logger.Debug("fetching Velero strategy", "strategyName", backup.Spec.StrategyRef.Name)
veleroStrategy := &strategyv1alpha1.Velero{}
if err := r.Get(ctx, client.ObjectKey{Name: backup.Spec.StrategyRef.Name}, veleroStrategy); err != nil {
if errors.IsNotFound(err) {
logger.Error(err, "Velero strategy not found", "strategyName", backup.Spec.StrategyRef.Name)
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("Velero strategy not found: %s", backup.Spec.StrategyRef.Name))
}
logger.Error(err, "failed to get Velero strategy")
return ctrl.Result{}, err
}
logger.Debug("fetched Velero strategy", "strategyName", veleroStrategy.Name)
// Get Velero backup name from Backup's driverMetadata
veleroBackupName, ok := backup.Spec.DriverMetadata[veleroBackupNameMetadataKey]
if !ok {
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("Backup missing Velero backup name in driverMetadata (key: %s)", veleroBackupNameMetadataKey))
}
// Step 3: Execute restore logic
// Check if we already created a Velero Restore
logger.Debug("checking for existing Velero Restore", "namespace", veleroNamespace)
veleroRestoreList := &velerov1.RestoreList{}
opts := []client.ListOption{
client.InNamespace(veleroNamespace),
client.MatchingLabels{
backupsv1alpha1.OwningJobNameLabel: restoreJob.Name,
backupsv1alpha1.OwningJobNamespaceLabel: restoreJob.Namespace,
},
}
if err := r.List(ctx, veleroRestoreList, opts...); err != nil {
logger.Error(err, "failed to get Velero Restore")
return ctrl.Result{}, err
}
if len(veleroRestoreList.Items) == 0 {
// Create Velero Restore
logger.Debug("Velero Restore not found, creating new one")
if err := r.createVeleroRestore(ctx, restoreJob, backup, veleroStrategy, veleroBackupName); err != nil {
logger.Error(err, "failed to create Velero Restore")
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("failed to create Velero Restore: %v", err))
}
logger.Debug("created Velero Restore, requeuing")
// Requeue to check status
return ctrl.Result{RequeueAfter: defaultRestoreRequeueAfter}, nil
}
if len(veleroRestoreList.Items) > 1 {
logger.Error(fmt.Errorf("too many Velero restores for RestoreJob"), "found more than one Velero Restore referencing a single RestoreJob as owner")
return r.markRestoreJobFailed(ctx, restoreJob, "found multiple Velero Restores for this RestoreJob")
}
veleroRestore := veleroRestoreList.Items[0].DeepCopy()
logger.Debug("found existing Velero Restore", "phase", veleroRestore.Status.Phase)
// Check Velero Restore status
phase := string(veleroRestore.Status.Phase)
if phase == "" {
// Still in progress, requeue
return ctrl.Result{RequeueAfter: defaultActiveRestorePollingInterval}, nil
}
// Step 4: On success
if phase == "Completed" {
now := metav1.Now()
restoreJob.Status.CompletedAt = &now
restoreJob.Status.Phase = backupsv1alpha1.RestoreJobPhaseSucceeded
if err := r.Status().Update(ctx, restoreJob); err != nil {
logger.Error(err, "failed to update RestoreJob status")
return ctrl.Result{}, err
}
logger.Debug("RestoreJob succeeded")
return ctrl.Result{}, nil
}
// Step 5: On failure
if phase == "Failed" || phase == "PartiallyFailed" {
message := fmt.Sprintf("Velero Restore failed with phase: %s", phase)
if veleroRestore.Status.FailureReason != "" {
message = fmt.Sprintf("%s: %s", message, veleroRestore.Status.FailureReason)
}
return r.markRestoreJobFailed(ctx, restoreJob, message)
}
// Still in progress (InProgress, New, etc.)
return ctrl.Result{RequeueAfter: defaultRestoreRequeueAfter}, nil
}
// createVeleroRestore creates a Velero Restore resource.
func (r *RestoreJobReconciler) createVeleroRestore(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup, strategy *strategyv1alpha1.Velero, veleroBackupName string) error {
logger := getLogger(ctx)
logger.Debug("createVeleroRestore called", "strategy", strategy.Name, "veleroBackupName", veleroBackupName)
// Determine target application reference
targetAppRef := r.getTargetApplicationRef(restoreJob, backup)
// Get the target application object for templating
mapping, err := r.RESTMapping(schema.GroupKind{Group: *targetAppRef.APIGroup, Kind: targetAppRef.Kind})
if err != nil {
return fmt.Errorf("failed to get REST mapping for target application: %w", err)
}
ns := restoreJob.Namespace
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
ns = ""
}
app, err := r.Resource(mapping.Resource).Namespace(ns).Get(ctx, targetAppRef.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get target application: %w", err)
}
// Template the restore spec from the strategy
veleroRestoreSpec, err := template.Template(&strategy.Spec.Template.RestoreSpec, app.Object)
if err != nil {
return fmt.Errorf("failed to template Velero Restore spec: %w", err)
}
// Set the backupName in the spec (required by Velero)
veleroRestoreSpec.BackupName = veleroBackupName
veleroRestore := &velerov1.Restore{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s.%s-", restoreJob.Namespace, restoreJob.Name),
Namespace: veleroNamespace,
Labels: map[string]string{
backupsv1alpha1.OwningJobNameLabel: restoreJob.Name,
backupsv1alpha1.OwningJobNamespaceLabel: restoreJob.Namespace,
},
},
Spec: *veleroRestoreSpec,
}
name := veleroRestore.GenerateName
if err := r.Create(ctx, veleroRestore); err != nil {
if veleroRestore.Name != "" {
name = veleroRestore.Name
}
logger.Error(err, "failed to create Velero Restore", "name", veleroRestore.Name)
r.Recorder.Event(restoreJob, corev1.EventTypeWarning, "VeleroRestoreCreationFailed",
fmt.Sprintf("Failed to create Velero Restore %s/%s: %v", veleroNamespace, name, err))
return err
}
logger.Debug("created Velero Restore", "name", veleroRestore.Name, "namespace", veleroRestore.Namespace)
r.Recorder.Event(restoreJob, corev1.EventTypeNormal, "VeleroRestoreCreated",
fmt.Sprintf("Created Velero Restore %s/%s", veleroNamespace, name))
return nil
}

View File

@@ -0,0 +1,355 @@
# Dashboard Resource Integration Guide
This guide explains how to add a new Kubernetes resource to the Cozystack dashboard. The dashboard provides a unified interface for viewing and managing Kubernetes resources through custom table views, detail pages, and sidebar navigation.
## Overview
Adding a new resource to the dashboard requires three main components:
1. **CustomColumnsOverride**: Defines how the resource appears in list/table views
2. **Factory**: Defines the detail page layout for individual resource instances
3. **Sidebar Entry**: Adds navigation to the resource in the sidebar menu
## Prerequisites
- The resource must have a Kubernetes CustomResourceDefinition (CRD) or be a built-in Kubernetes resource
- Know the resource's API group, version, kind, and plural name
- Understand the resource's spec structure to display relevant fields
## Step-by-Step Guide
### Step 1: Add CustomColumnsOverride
The CustomColumnsOverride defines the columns shown in the resource list table and how clicking on a row navigates to the detail page.
**Location**: `internal/controller/dashboard/static_refactored.go` in `CreateAllCustomColumnsOverrides()`
**Example**:
```go
// Stock namespace backups cozystack io v1alpha1 plans
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/plans", []any{
createCustomColumnWithJsonPath("Name", ".metadata.name", "Plan", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/plan-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
createApplicationRefColumn("Application"),
createTimestampColumn("Created", ".metadata.creationTimestamp"),
}),
```
**Key Components**:
- **ID Format**: `stock-namespace-/{group}/{version}/{plural}`
- **Name Column**: Use `createCustomColumnWithJsonPath()` with:
- Badge value: Resource kind in PascalCase (e.g., "Plan", "Service")
- Link href: `/openapi-ui/{2}/{namespace}/factory/{resource-details}/{name}`
- **Additional Columns**: Use helper functions like:
- `createStringColumn()`: Simple string values
- `createTimestampColumn()`: Timestamp fields
- `createReadyColumn()`: Ready status from conditions
- Custom helpers for complex fields
**Helper Functions Available**:
- `createCustomColumnWithJsonPath(name, jsonPath, badgeValue, badgeColor, linkHref)`: Column with badge and link
- `createStringColumn(name, jsonPath)`: Simple string column
- `createTimestampColumn(name, jsonPath)`: Timestamp with formatting
- `createReadyColumn()`: Ready status column
- `createBoolColumn(name, jsonPath)`: Boolean column
- `createArrayColumn(name, jsonPath)`: Array column
### Step 2: Add Factory (Detail Page)
The Factory defines the detail page layout when viewing an individual resource instance.
**Location**: `internal/controller/dashboard/static_refactored.go` in `CreateAllFactories()`
**Using Unified Factory Approach** (Recommended):
```go
// Resource details factory using unified approach
resourceConfig := UnifiedResourceConfig{
Name: "resource-details", // Must match the href in CustomColumnsOverride
ResourceType: "factory",
Kind: "ResourceKind", // PascalCase
Plural: "resources", // lowercase plural
Title: "resource", // lowercase singular
}
resourceTabs := []any{
map[string]any{
"key": "details",
"label": "Details",
"children": []any{
contentCard("details-card", map[string]any{
"marginBottom": "24px",
}, []any{
antdText("details-title", true, "Resource details", map[string]any{
"fontSize": 20,
"marginBottom": "12px",
}),
spacer("details-spacer", 16),
antdRow("details-grid", []any{48, 12}, []any{
antdCol("col-left", 12, []any{
antdFlexVertical("col-left-stack", 24, []any{
// Metadata fields: Name, Namespace, Created, etc.
}),
}),
antdCol("col-right", 12, []any{
antdFlexVertical("col-right-stack", 24, []any{
// Spec fields
}),
}),
}),
}),
},
},
}
resourceSpec := createUnifiedFactory(resourceConfig, resourceTabs, []any{"/api/clusters/{2}/k8s/apis/{group}/{version}/namespaces/{3}/{plural}/{6}"})
// Add to return list
return []*dashboardv1alpha1.Factory{
// ... other factories
createFactory("resource-details", resourceSpec),
}
```
**Key Components**:
- **Factory Key**: Must match the href path segment (e.g., `plan-details` matches `/factory/plan-details/...`)
- **API Endpoint**: `/api/clusters/{2}/k8s/apis/{group}/{version}/namespaces/{3}/{plural}/{6}`
- `{2}`: Cluster name
- `{3}`: Namespace
- `{6}`: Resource name
- **Sidebar Tags**: Automatically set to `["{lowercase-kind}-sidebar"]` by `createUnifiedFactory()`
- **Tabs**: Define the detail page content (Details, YAML, etc.)
**UI Helper Functions**:
- `contentCard(id, style, children)`: Container card
- `antdText(id, strong, text, style)`: Text element
- `antdFlexVertical(id, gap, children)`: Vertical flex container
- `antdRow(id, gutter, children)`: Row layout
- `antdCol(id, span, children)`: Column layout
- `parsedText(id, text, style)`: Text with JSON path parsing
- `parsedTextWithFormatter(id, text, formatter)`: Formatted text (e.g., timestamp)
- `spacer(id, space)`: Spacing element
**Displaying Fields**:
```go
antdFlexVertical("field-block", 4, []any{
antdText("field-label", true, "Field Label", nil),
parsedText("field-value", "{reqsJsonPath[0]['.spec.fieldName']['-']}", nil),
}),
```
### Step 3: Add Sidebar Entry
The sidebar entry adds navigation to the resource in the sidebar menu.
**Location**: `internal/controller/dashboard/sidebar.go` in `ensureSidebar()`
**3a. Add to keysAndTags** (around line 110-116):
```go
// Add sidebar for {group} {kind} resource
keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}
```
**3b. Add Sidebar Section** (if creating a new section, around line 169):
```go
// Add hardcoded {SectionName} section
menuItems = append(menuItems, map[string]any{
"key": "{section-key}",
"label": "{SectionName}",
"children": []any{
map[string]any{
"key": "{plural}",
"label": "{ResourceLabel}",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/{group}/{version}/{plural}",
},
},
}),
```
**3c. Add Sidebar ID to targetIDs** (around line 220):
```go
"stock-project-factory-{lowercase-kind}-details",
```
**3d. Update Category Ordering** (if adding a new section):
- Update the comment around line 29 to include the new section
- Update `orderCategoryLabels()` function if needed
- Add skip condition in the category loop (around line 149)
**Important Notes**:
- The sidebar tag (`{lowercase-kind}-sidebar`) must match what the Factory uses
- The link format: `/openapi-ui/{clusterName}/{namespace}/api-table/{group}/{version}/{plural}`
- All sidebars share the same `keysAndTags` and `menuItems`, so changes affect all sidebar instances
### Step 4: Verify Integration
1. **Check Factory-Sidebar Connection**:
- Factory uses `sidebarTags: ["{lowercase-kind}-sidebar"]`
- Sidebar has `keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}`
- Sidebar ID `stock-project-factory-{lowercase-kind}-details` exists in `targetIDs`
2. **Check Navigation Flow**:
- Sidebar link → List table (CustomColumnsOverride)
- List table Name column → Detail page (Factory)
- All paths use consistent naming
3. **Test**:
- Verify the resource appears in the sidebar
- Verify the list table displays correctly
- Verify clicking a resource navigates to the detail page
- Verify the detail page displays all relevant fields
## Common Patterns
### Displaying Object References
For fields that reference other resources (e.g., `applicationRef`, `storageRef`):
```go
// In CustomColumnsOverride
createApplicationRefColumn("Application"), // Uses helper function
// In Factory details tab
parsedText("application-ref-value",
"{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}",
nil),
```
### Displaying Timestamps
```go
antdFlexVertical("created-block", 4, []any{
antdText("time-label", true, "Created", nil),
antdFlex("time-block", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "time-icon",
"text": "🌐",
},
},
map[string]any{
"type": "parsedText",
"data": map[string]any{
"formatter": "timestamp",
"id": "time-value",
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
},
},
}),
}),
```
### Displaying Namespace with Link
```go
antdFlexVertical("meta-namespace-block", 8, []any{
antdText("meta-namespace-label", true, "Namespace", nil),
antdFlex("header-row", 6, []any{
// Badge component
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "header-badge",
"text": "NS",
"title": "namespace",
"style": map[string]any{
"backgroundColor": "#a25792ff",
// ... badge styles
},
},
},
// Link component
map[string]any{
"type": "antdLink",
"data": map[string]any{
"id": "namespace-link",
"text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
"href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
},
},
}),
}),
```
## File Reference
- **CustomColumnsOverride**: `internal/controller/dashboard/static_refactored.go``CreateAllCustomColumnsOverrides()`
- **Factory**: `internal/controller/dashboard/static_refactored.go``CreateAllFactories()`
- **Sidebar**: `internal/controller/dashboard/sidebar.go``ensureSidebar()`
- **Helper Functions**: `internal/controller/dashboard/static_helpers.go`
- **UI Helpers**: `internal/controller/dashboard/ui_helpers.go`
- **Unified Helpers**: `internal/controller/dashboard/unified_helpers.go`
## AI Agent Prompt Template
Use this template when asking an AI agent to add a new resource to the dashboard:
```
Please add support for the {ResourceKind} resource ({group}/{version}/{plural}) to the Cozystack dashboard.
Resource Details:
- API Group: {group}
- Version: {version}
- Kind: {ResourceKind}
- Plural: {plural}
- Namespaced: {true/false}
Requirements:
1. Add a CustomColumnsOverride in CreateAllCustomColumnsOverrides() with:
- ID: stock-namespace-/{group}/{version}/{plural}
- Name column with {ResourceKind} badge linking to /factory/{lowercase-kind}-details/{name}
- Additional columns: {list relevant columns}
2. Add a Factory in CreateAllFactories() with:
- Key: {lowercase-kind}-details
- API endpoint: /api/clusters/{2}/k8s/apis/{group}/{version}/namespaces/{3}/{plural}/{6}
- Details tab showing all spec fields:
{list spec fields to display}
- Use createUnifiedFactory() approach
3. Add sidebar entry in ensureSidebar():
- Add keysAndTags entry: keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}
- Add sidebar section: {specify section name or "add to existing section"}
- Add to targetIDs: "stock-project-factory-{lowercase-kind}-details"
4. Ensure Factory sidebarTags matches the keysAndTags entry
Please follow the existing patterns in the codebase, particularly the Plan resource implementation as a reference.
```
## Example: Plan Resource
The Plan resource (`backups.cozystack.io/v1alpha1/plans`) serves as a complete reference implementation:
- **CustomColumnsOverride**: [Diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-8309b1db3362715b3d94a8b0beae7e95d3ccaf248d4f8702aaa12fba398da895R374-R380) in `static_refactored.go`
- **Factory**: [Diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-8309b1db3362715b3d94a8b0beae7e95d3ccaf248d4f8702aaa12fba398da895R1443-R1558) in `static_refactored.go`
- **Sidebar**: [Diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-be79027f7179e457a8f10e225bb921a197ffa390eb8f916d8d21379fadd54a56) in `sidebar.go`
- **Helper Function**: `createApplicationRefColumn()` in `static_helpers.go` ([diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-f17bcccc089cac3a8e965b13b9ab26e678d45bfc9a58d842399f218703e06a08R1026-R1046))
Review [this implementation](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513) for a complete working example.
## Troubleshooting
### Resource doesn't appear in sidebar
- Check that `keysAndTags["{plural}"]` is set correctly
- Verify the sidebar section is added to `menuItems`
- Ensure the sidebar ID is in `targetIDs`
### Clicking resource doesn't navigate to detail page
- Verify the CustomColumnsOverride href matches the Factory key
- Check that the Factory key is exactly `{lowercase-kind}-details`
- Ensure the Factory is added to the return list in `CreateAllFactories()`
### Detail page shows wrong sidebar
- Verify Factory `sidebarTags` matches `keysAndTags["{plural}"]`
- Check that the sidebar ID `stock-project-factory-{lowercase-kind}-details` exists
- Ensure all sidebars are updated (they share the same `keysAndTags`)
### Fields not displaying correctly
- Verify JSON paths are correct (use `.spec.fieldName` format)
- Check that `reqsJsonPath[0]` index is used for single resource views
- Ensure field names match the actual resource spec structure
## Additional Resources
- Kubernetes API Conventions: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
- Dashboard API Types: `api/dashboard/v1alpha1/`
- Resource Types: `api/backups/v1alpha1/` (example)

View File

@@ -105,8 +105,26 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
"properties": map[string]any{},
}
// Check if there's a spec property
specProp, ok := props["spec"].(map[string]any)
if !ok {
return map[string]any{}, nil
}
specProps, ok := specProp["properties"].(map[string]any)
if !ok {
return map[string]any{}, nil
}
// Create spec.properties structure in schema
schemaProps := schema["properties"].(map[string]any)
specSchema := map[string]any{
"properties": map[string]any{},
}
schemaProps["spec"] = specSchema
// Process spec properties recursively
processSpecProperties(props, schema["properties"].(map[string]any))
processSpecProperties(specProps, specSchema["properties"].(map[string]any))
return schema, nil
}

View File

@@ -9,41 +9,46 @@ func TestBuildMultilineStringSchema(t *testing.T) {
// Test OpenAPI schema with various field types
openAPISchema := `{
"properties": {
"simpleString": {
"type": "string",
"description": "A simple string field"
},
"stringWithEnum": {
"type": "string",
"enum": ["option1", "option2"],
"description": "String with enum should be skipped"
},
"numberField": {
"type": "number",
"description": "Number field should be skipped"
},
"nestedObject": {
"spec": {
"type": "object",
"properties": {
"nestedString": {
"simpleString": {
"type": "string",
"description": "Nested string should get multilineString"
"description": "A simple string field"
},
"nestedStringWithEnum": {
"stringWithEnum": {
"type": "string",
"enum": ["a", "b"],
"description": "Nested string with enum should be skipped"
}
}
},
"arrayOfObjects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"itemString": {
"type": "string",
"description": "String in array item"
"enum": ["option1", "option2"],
"description": "String with enum should be skipped"
},
"numberField": {
"type": "number",
"description": "Number field should be skipped"
},
"nestedObject": {
"type": "object",
"properties": {
"nestedString": {
"type": "string",
"description": "Nested string should get multilineString"
},
"nestedStringWithEnum": {
"type": "string",
"enum": ["a", "b"],
"description": "Nested string with enum should be skipped"
}
}
},
"arrayOfObjects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"itemString": {
"type": "string",
"description": "String in array item"
}
}
}
}
}
@@ -70,33 +75,44 @@ func TestBuildMultilineStringSchema(t *testing.T) {
t.Fatal("schema.properties is not a map")
}
// Check simpleString
simpleString, ok := props["simpleString"].(map[string]any)
// Check spec property exists
spec, ok := props["spec"].(map[string]any)
if !ok {
t.Fatal("simpleString not found in properties")
t.Fatal("spec not found in properties")
}
specProps, ok := spec["properties"].(map[string]any)
if !ok {
t.Fatal("spec.properties is not a map")
}
// Check simpleString
simpleString, ok := specProps["simpleString"].(map[string]any)
if !ok {
t.Fatal("simpleString not found in spec.properties")
}
if simpleString["type"] != "multilineString" {
t.Errorf("simpleString should have type multilineString, got %v", simpleString["type"])
}
// Check stringWithEnum should not be present (or should not have multilineString)
if stringWithEnum, ok := props["stringWithEnum"].(map[string]any); ok {
if stringWithEnum, ok := specProps["stringWithEnum"].(map[string]any); ok {
if stringWithEnum["type"] == "multilineString" {
t.Error("stringWithEnum should not have multilineString type")
}
}
// Check numberField should not be present
if numberField, ok := props["numberField"].(map[string]any); ok {
if numberField, ok := specProps["numberField"].(map[string]any); ok {
if numberField["type"] != nil {
t.Error("numberField should not have any type override")
}
}
// Check nested object
nestedObject, ok := props["nestedObject"].(map[string]any)
nestedObject, ok := specProps["nestedObject"].(map[string]any)
if !ok {
t.Fatal("nestedObject not found in properties")
t.Fatal("nestedObject not found in spec.properties")
}
nestedProps, ok := nestedObject["properties"].(map[string]any)
if !ok {
@@ -113,9 +129,9 @@ func TestBuildMultilineStringSchema(t *testing.T) {
}
// Check array of objects
arrayOfObjects, ok := props["arrayOfObjects"].(map[string]any)
arrayOfObjects, ok := specProps["arrayOfObjects"].(map[string]any)
if !ok {
t.Fatal("arrayOfObjects not found in properties")
t.Fatal("arrayOfObjects not found in spec.properties")
}
items, ok := arrayOfObjects["items"].(map[string]any)
if !ok {

View File

@@ -26,7 +26,7 @@ import (
// - Tenant Info (/openapi-ui/{clusterName}/{namespace}/factory/info-details/info)
// - All other sections are built from CRDs where spec.dashboard != nil.
// - Categories are ordered strictly as:
// Marketplace, IaaS, PaaS, NaaS, <others A→Z>, Resources, Administration
// Marketplace, IaaS, PaaS, NaaS, <others A→Z>, Resources, Backups, Administration
// - Items within each category: sort by Weight (desc), then Label (A→Z).
func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
// Build the full menu once.
@@ -112,6 +112,15 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
keysAndTags["secrets"] = []any{"secret-sidebar"}
keysAndTags["ingresses"] = []any{"ingress-sidebar"}
// Add sidebar for backups.cozystack.io Plan resource
keysAndTags["plans"] = []any{"plan-sidebar"}
// Add sidebar for backups.cozystack.io BackupJob resource
keysAndTags["backupjobs"] = []any{"backupjob-sidebar"}
// Add sidebar for backups.cozystack.io Backup resource
keysAndTags["backups"] = []any{"backup-sidebar"}
// 3) Sort items within each category by Weight (desc), then Label (A→Z)
for cat := range categories {
sort.Slice(categories[cat], func(i, j int) bool {
@@ -123,10 +132,10 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
}
// 4) Order categories strictly:
// Marketplace (hardcoded), IaaS, PaaS, NaaS, <others A→Z>, Resources, Administration
// Marketplace (hardcoded), IaaS, PaaS, NaaS, <others A→Z>, Resources, Backups (hardcoded), Administration (hardcoded)
orderedCats := orderCategoryLabels(categories)
// 5) Build menuItems (hardcode "Marketplace"; then dynamic categories; then hardcode "Administration")
// 5) Build menuItems (hardcode "Marketplace"; then dynamic categories; then hardcode "Backups" and "Administration")
menuItems := []any{
map[string]any{
"key": "marketplace",
@@ -142,8 +151,8 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
}
for _, cat := range orderedCats {
// Skip "Marketplace" and "Administration" here since they're hardcoded
if strings.EqualFold(cat, "Marketplace") || strings.EqualFold(cat, "Administration") {
// Skip "Marketplace", "Backups", and "Administration" here since they're hardcoded
if strings.EqualFold(cat, "Marketplace") || strings.EqualFold(cat, "Backups") || strings.EqualFold(cat, "Administration") {
continue
}
children := []any{}
@@ -163,6 +172,29 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
}
}
// Add hardcoded Backups section
menuItems = append(menuItems, map[string]any{
"key": "backups",
"label": "Backups",
"children": []any{
map[string]any{
"key": "plans",
"label": "Plans",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/backups.cozystack.io/v1alpha1/plans",
},
map[string]any{
"key": "backupjobs",
"label": "BackupJobs",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/backups.cozystack.io/v1alpha1/backupjobs",
},
map[string]any{
"key": "backups",
"label": "Backups",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/backups.cozystack.io/v1alpha1/backups",
},
},
})
// Add hardcoded Administration section
menuItems = append(menuItems, map[string]any{
"key": "administration",
@@ -201,6 +233,9 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
"stock-project-factory-kube-service-details",
"stock-project-factory-kube-secret-details",
"stock-project-factory-kube-ingress-details",
"stock-project-factory-plan-details",
"stock-project-factory-backupjob-details",
"stock-project-factory-backup-details",
"stock-project-api-form",
"stock-project-api-table",
"stock-project-builtin-form",

View File

@@ -1023,6 +1023,27 @@ func createReadyColumn() map[string]any {
}
}
// createApplicationRefColumn creates a column that displays
// applicationRef in the format "Kind.apiGroup/name"
func createApplicationRefColumn(name string) map[string]any {
return map[string]any{
"name": name,
"type": "factory",
"customProps": map[string]any{
"disableEventBubbling": true,
"items": []any{
map[string]any{
"type": "parsedText",
"data": map[string]any{
"id": "application-ref-text",
"text": "{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}",
},
},
},
},
}
}
// createConverterBytesColumn creates a column with ConverterBytes component
func createConverterBytesColumn(name, jsonPath string) map[string]any {
return map[string]any{

View File

@@ -371,6 +371,30 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid
createCustomColumnWithJsonPath("Name", ".metadata.name", "TenantNamespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.name']['-']}/factory/marketplace"),
createTimestampColumn("Created", ".metadata.creationTimestamp"),
}),
// Stock namespace backups cozystack io v1alpha1 plans
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/plans", []any{
createCustomColumnWithJsonPath("Name", ".metadata.name", "Plan", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/plan-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
createApplicationRefColumn("Application"),
createTimestampColumn("Created", ".metadata.creationTimestamp"),
}),
// Stock namespace backups cozystack io v1alpha1 backupjobs
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/backupjobs", []any{
createCustomColumnWithJsonPath("Name", ".metadata.name", "BackupJob", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/backupjob-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
createStringColumn("Phase", ".status.phase"),
createApplicationRefColumn("Application"),
createTimestampColumn("Created", ".metadata.creationTimestamp"),
}),
// Stock namespace backups cozystack io v1alpha1 backups
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/backups", []any{
createCustomColumnWithJsonPath("Name", ".metadata.name", "Backup", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/backup-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
createStringColumn("Phase", ".status.phase"),
createApplicationRefColumn("Application"),
createTimestampColumn("Taken At", ".spec.takenAt"),
createTimestampColumn("Created", ".metadata.creationTimestamp"),
}),
}
}
@@ -1433,6 +1457,434 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
}
workloadmonitorSpec := createFactorySpec("workloadmonitor-details", []any{"workloadmonitor-sidebar"}, []any{"/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloadmonitors/{6}"}, workloadmonitorHeader, workloadmonitorTabs)
// Plan details factory using unified approach
planConfig := UnifiedResourceConfig{
Name: "plan-details",
ResourceType: "factory",
Kind: "Plan",
Plural: "plans",
Title: "plan",
}
planTabs := []any{
map[string]any{
"key": "details",
"label": "Details",
"children": []any{
contentCard("details-card", map[string]any{
"marginBottom": "24px",
}, []any{
antdText("details-title", true, "Plan details", map[string]any{
"fontSize": 20,
"marginBottom": "12px",
}),
spacer("details-spacer", 16),
antdRow("details-grid", []any{48, 12}, []any{
antdCol("col-left", 12, []any{
antdFlexVertical("col-left-stack", 24, []any{
antdFlexVertical("meta-name-block", 4, []any{
antdText("meta-name-label", true, "Name", nil),
parsedText("meta-name-value", "{reqsJsonPath[0]['.metadata.name']['-']}", nil),
}),
antdFlexVertical("meta-namespace-block", 8, []any{
antdText("meta-namespace-label", true, "Namespace", nil),
antdFlex("header-row", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "header-badge",
"text": "NS",
"title": "namespace",
"style": map[string]any{
"backgroundColor": "#a25792ff",
"borderRadius": "20px",
"color": "#fff",
"display": "inline-block",
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
"fontSize": "15px",
"fontWeight": 400,
"lineHeight": "24px",
"minWidth": 24,
"padding": "0 9px",
"textAlign": "center",
"whiteSpace": "nowrap",
},
},
},
map[string]any{
"type": "antdLink",
"data": map[string]any{
"id": "namespace-link",
"text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
"href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
},
},
}),
}),
antdFlexVertical("meta-created-block", 4, []any{
antdText("time-label", true, "Created", nil),
antdFlex("time-block", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "time-icon",
"text": "🌐",
},
},
map[string]any{
"type": "parsedText",
"data": map[string]any{
"formatter": "timestamp",
"id": "time-value",
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
},
},
}),
}),
}),
}),
antdCol("col-right", 12, []any{
antdFlexVertical("col-right-stack", 24, []any{
antdFlexVertical("spec-application-ref-block", 4, []any{
antdText("application-ref-label", true, "Application", nil),
parsedText("application-ref-value", "{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}", nil),
}),
antdFlexVertical("spec-storage-ref-block", 4, []any{
antdText("storage-ref-label", true, "Storage", nil),
parsedText("storage-ref-value", "{reqsJsonPath[0]['.spec.storageRef.kind']['-']}.{reqsJsonPath[0]['.spec.storageRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.storageRef.name']['-']}", nil),
}),
antdFlexVertical("spec-strategy-ref-block", 4, []any{
antdText("strategy-ref-label", true, "Strategy", nil),
parsedText("strategy-ref-value", "{reqsJsonPath[0]['.spec.strategyRef.kind']['-']}.{reqsJsonPath[0]['.spec.strategyRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.strategyRef.name']['-']}", nil),
}),
antdFlexVertical("spec-schedule-type-block", 4, []any{
antdText("schedule-type-label", true, "Schedule Type", nil),
parsedText("schedule-type-value", "{reqsJsonPath[0]['.spec.schedule.type']['-']}", nil),
}),
antdFlexVertical("spec-schedule-cron-block", 4, []any{
antdText("schedule-cron-label", true, "Schedule Cron", nil),
parsedText("schedule-cron-value", "{reqsJsonPath[0]['.spec.schedule.cron']['-']}", nil),
}),
}),
}),
}),
}),
},
},
}
planSpec := createUnifiedFactory(planConfig, planTabs, []any{"/api/clusters/{2}/k8s/apis/backups.cozystack.io/v1alpha1/namespaces/{3}/plans/{6}"})
// BackupJob details factory using unified approach
backupJobConfig := UnifiedResourceConfig{
Name: "backupjob-details",
ResourceType: "factory",
Kind: "BackupJob",
Plural: "backupjobs",
Title: "backupjob",
}
backupJobTabs := []any{
map[string]any{
"key": "details",
"label": "Details",
"children": []any{
contentCard("details-card", map[string]any{
"marginBottom": "24px",
}, []any{
antdText("details-title", true, "BackupJob details", map[string]any{
"fontSize": 20,
"marginBottom": "12px",
}),
spacer("details-spacer", 16),
antdRow("details-grid", []any{48, 12}, []any{
antdCol("col-left", 12, []any{
antdFlexVertical("col-left-stack", 24, []any{
antdFlexVertical("meta-name-block", 4, []any{
antdText("meta-name-label", true, "Name", nil),
parsedText("meta-name-value", "{reqsJsonPath[0]['.metadata.name']['-']}", nil),
}),
antdFlexVertical("meta-namespace-block", 8, []any{
antdText("meta-namespace-label", true, "Namespace", nil),
antdFlex("header-row", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "header-badge",
"text": "NS",
"title": "namespace",
"style": map[string]any{
"backgroundColor": "#a25792ff",
"borderRadius": "20px",
"color": "#fff",
"display": "inline-block",
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
"fontSize": "15px",
"fontWeight": 400,
"lineHeight": "24px",
"minWidth": 24,
"padding": "0 9px",
"textAlign": "center",
"whiteSpace": "nowrap",
},
},
},
map[string]any{
"type": "antdLink",
"data": map[string]any{
"id": "namespace-link",
"text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
"href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
},
},
}),
}),
antdFlexVertical("meta-created-block", 4, []any{
antdText("time-label", true, "Created", nil),
antdFlex("time-block", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "time-icon",
"text": "🌐",
},
},
map[string]any{
"type": "parsedText",
"data": map[string]any{
"formatter": "timestamp",
"id": "time-value",
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
},
},
}),
}),
}),
}),
antdCol("col-right", 12, []any{
antdFlexVertical("col-right-stack", 24, []any{
antdFlexVertical("status-phase-block", 4, []any{
antdText("phase-label", true, "Phase", nil),
parsedText("phase-value", "{reqsJsonPath[0]['.status.phase']['-']}", nil),
}),
antdFlexVertical("spec-plan-ref-block", 4, []any{
antdText("plan-ref-label", true, "Plan Ref", nil),
parsedText("plan-ref-value", "{reqsJsonPath[0]['.spec.planRef.name']['-']}", nil),
}),
antdFlexVertical("spec-application-ref-block", 4, []any{
antdText("application-ref-label", true, "Application", nil),
parsedText("application-ref-value", "{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}", nil),
}),
antdFlexVertical("spec-storage-ref-block", 4, []any{
antdText("storage-ref-label", true, "Storage", nil),
parsedText("storage-ref-value", "{reqsJsonPath[0]['.spec.storageRef.kind']['-']}.{reqsJsonPath[0]['.spec.storageRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.storageRef.name']['-']}", nil),
}),
antdFlexVertical("spec-strategy-ref-block", 4, []any{
antdText("strategy-ref-label", true, "Strategy", nil),
parsedText("strategy-ref-value", "{reqsJsonPath[0]['.spec.strategyRef.name']['-']}", nil),
}),
antdFlexVertical("status-backup-ref-block", 4, []any{
antdText("backup-ref-label", true, "Backup Ref", nil),
parsedText("backup-ref-value", "{reqsJsonPath[0]['.status.backupRef.name']['-']}", nil),
}),
antdFlexVertical("status-started-at-block", 4, []any{
antdText("started-at-label", true, "Started At", nil),
antdFlex("time-block", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "time-icon",
"text": "🌐",
},
},
map[string]any{
"type": "parsedText",
"data": map[string]any{
"formatter": "timestamp",
"id": "time-value",
"text": "{reqsJsonPath[0]['.status.startedAt']['-']}",
},
},
}),
}),
antdFlexVertical("status-completed-at-block", 4, []any{
antdText("completed-at-label", true, "Completed At", nil),
antdFlex("time-block", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "time-icon",
"text": "🌐",
},
},
map[string]any{
"type": "parsedText",
"data": map[string]any{
"formatter": "timestamp",
"id": "time-value",
"text": "{reqsJsonPath[0]['.status.completedAt']['-']}",
},
},
}),
}),
antdFlexVertical("status-message-block", 4, []any{
antdText("message-label", true, "Message", nil),
parsedText("message-value", "{reqsJsonPath[0]['.status.message']['-']}", nil),
}),
}),
}),
}),
}),
},
},
}
backupJobSpec := createUnifiedFactory(backupJobConfig, backupJobTabs, []any{"/api/clusters/{2}/k8s/apis/backups.cozystack.io/v1alpha1/namespaces/{3}/backupjobs/{6}"})
// Backup details factory using unified approach
backupConfig := UnifiedResourceConfig{
Name: "backup-details",
ResourceType: "factory",
Kind: "Backup",
Plural: "backups",
Title: "backup",
}
backupTabs := []any{
map[string]any{
"key": "details",
"label": "Details",
"children": []any{
contentCard("details-card", map[string]any{
"marginBottom": "24px",
}, []any{
antdText("details-title", true, "Backup details", map[string]any{
"fontSize": 20,
"marginBottom": "12px",
}),
spacer("details-spacer", 16),
antdRow("details-grid", []any{48, 12}, []any{
antdCol("col-left", 12, []any{
antdFlexVertical("col-left-stack", 24, []any{
antdFlexVertical("meta-name-block", 4, []any{
antdText("meta-name-label", true, "Name", nil),
parsedText("meta-name-value", "{reqsJsonPath[0]['.metadata.name']['-']}", nil),
}),
antdFlexVertical("meta-namespace-block", 8, []any{
antdText("meta-namespace-label", true, "Namespace", nil),
antdFlex("header-row", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "header-badge",
"text": "NS",
"title": "namespace",
"style": map[string]any{
"backgroundColor": "#a25792ff",
"borderRadius": "20px",
"color": "#fff",
"display": "inline-block",
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
"fontSize": "15px",
"fontWeight": 400,
"lineHeight": "24px",
"minWidth": 24,
"padding": "0 9px",
"textAlign": "center",
"whiteSpace": "nowrap",
},
},
},
map[string]any{
"type": "antdLink",
"data": map[string]any{
"id": "namespace-link",
"text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
"href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
},
},
}),
}),
antdFlexVertical("meta-created-block", 4, []any{
antdText("time-label", true, "Created", nil),
antdFlex("time-block", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "time-icon",
"text": "🌐",
},
},
map[string]any{
"type": "parsedText",
"data": map[string]any{
"formatter": "timestamp",
"id": "time-value",
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
},
},
}),
}),
}),
}),
antdCol("col-right", 12, []any{
antdFlexVertical("col-right-stack", 24, []any{
antdFlexVertical("status-phase-block", 4, []any{
antdText("phase-label", true, "Phase", nil),
parsedText("phase-value", "{reqsJsonPath[0]['.status.phase']['-']}", nil),
}),
antdFlexVertical("spec-taken-at-block", 4, []any{
antdText("taken-at-label", true, "Taken At", nil),
antdFlex("time-block", 6, []any{
map[string]any{
"type": "antdText",
"data": map[string]any{
"id": "time-icon",
"text": "🌐",
},
},
map[string]any{
"type": "parsedText",
"data": map[string]any{
"formatter": "timestamp",
"id": "time-value",
"text": "{reqsJsonPath[0]['.spec.takenAt']['-']}",
},
},
}),
}),
antdFlexVertical("spec-plan-ref-block", 4, []any{
antdText("plan-ref-label", true, "Plan Ref", nil),
parsedText("plan-ref-value", "{reqsJsonPath[0]['.spec.planRef.name']['-']}", nil),
}),
antdFlexVertical("spec-application-ref-block", 4, []any{
antdText("application-ref-label", true, "Application", nil),
parsedText("application-ref-value", "{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}", nil),
}),
antdFlexVertical("spec-storage-ref-block", 4, []any{
antdText("storage-ref-label", true, "Storage", nil),
parsedText("storage-ref-value", "{reqsJsonPath[0]['.spec.storageRef.kind']['-']}.{reqsJsonPath[0]['.spec.storageRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.storageRef.name']['-']}", nil),
}),
antdFlexVertical("spec-strategy-ref-block", 4, []any{
antdText("strategy-ref-label", true, "Strategy", nil),
parsedText("strategy-ref-value", "{reqsJsonPath[0]['.spec.strategyRef.kind']['-']}.{reqsJsonPath[0]['.spec.strategyRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.strategyRef.name']['-']}", nil),
}),
antdFlexVertical("status-artifact-uri-block", 4, []any{
antdText("artifact-uri-label", true, "Artifact URI", nil),
parsedText("artifact-uri-value", "{reqsJsonPath[0]['.status.artifact.uri']['-']}", nil),
}),
antdFlexVertical("status-artifact-size-block", 4, []any{
antdText("artifact-size-label", true, "Artifact Size", nil),
parsedText("artifact-size-value", "{reqsJsonPath[0]['.status.artifact.sizeBytes']['-']}", nil),
}),
antdFlexVertical("status-artifact-checksum-block", 4, []any{
antdText("artifact-checksum-label", true, "Artifact Checksum", nil),
parsedText("artifact-checksum-value", "{reqsJsonPath[0]['.status.artifact.checksum']['-']}", nil),
}),
}),
}),
}),
}),
},
},
}
backupSpec := createUnifiedFactory(backupConfig, backupTabs, []any{"/api/clusters/{2}/k8s/apis/backups.cozystack.io/v1alpha1/namespaces/{3}/backups/{6}"})
return []*dashboardv1alpha1.Factory{
createFactory("marketplace", marketplaceSpec),
createFactory("namespace-details", namespaceSpec),
@@ -1442,6 +1894,9 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
createFactory("kube-service-details", serviceSpec),
createFactory("kube-ingress-details", ingressSpec),
createFactory("workloadmonitor-details", workloadmonitorSpec),
createFactory("plan-details", planSpec),
createFactory("backupjob-details", backupJobSpec),
createFactory("backup-details", backupSpec),
}
}

View File

@@ -0,0 +1,68 @@
package template
import (
"bytes"
"encoding/json"
tmpl "text/template"
)
func Template[T any](obj *T, templateContext map[string]any) (*T, error) {
b, err := json.Marshal(obj)
if err != nil {
return nil, err
}
var unstructured any
err = json.Unmarshal(b, &unstructured)
if err != nil {
return nil, err
}
templateFunc := func(in string) string {
out, err := template(in, templateContext)
if err != nil {
return in
}
return out
}
unstructured = mapAtStrings(unstructured, templateFunc)
b, err = json.Marshal(unstructured)
if err != nil {
return nil, err
}
var out T
err = json.Unmarshal(b, &out)
if err != nil {
return nil, err
}
return &out, nil
}
func mapAtStrings(v any, f func(string) string) any {
switch x := v.(type) {
case map[string]any:
for k, val := range x {
x[k] = mapAtStrings(val, f)
}
return x
case []any:
for i, val := range x {
x[i] = mapAtStrings(val, f)
}
return x
case string:
return f(x)
default:
return v
}
}
func template(in string, templateContext map[string]any) (string, error) {
tpl, err := tmpl.New("this").Parse(in)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, templateContext); err != nil {
return "", err
}
return buf.String(), nil
}

View File

@@ -0,0 +1,68 @@
package template
import (
"encoding/json"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestTemplate_PodTemplateSpec(t *testing.T) {
original := corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: "my-pod",
Labels: map[string]string{
"app": "demo",
},
Annotations: map[string]string{
"note": "hello",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "{{ .Release.Name }}",
Image: "nginx:1.21",
Args: []string{"--flag={{ .Values.value }}"},
Env: []corev1.EnvVar{
{
Name: "FOO",
Value: "{{ .Release.Namespace }}",
},
},
},
},
},
}
templateContext := map[string]any{
"Release": map[string]any{
"Name": "foo",
"Namespace": "notdefault",
},
"Values": map[string]any{
"value": 3,
},
}
reference := *original.DeepCopy()
reference.Spec.Containers[0].Name = "foo"
reference.Spec.Containers[0].Args[0] = "--flag=3"
reference.Spec.Containers[0].Env[0].Value = "notdefault"
got, err := Template(&original, templateContext)
if err != nil {
t.Fatalf("Template returned error: %v", err)
}
b1, err := json.Marshal(reference)
t.Logf("reference:\n%s", string(b1))
if err != nil {
t.Fatalf("failed to marshal reference value: %v", err)
}
b2, err := json.Marshal(got)
t.Logf("got:\n%s", string(b2))
if err != nil {
t.Fatalf("failed to marshal transformed value: %v", err)
}
if string(b1) != string(b2) {
t.Fatalf("transformed value not equal to reference value, expected: %s, got: %s", string(b1), string(b2))
}
}

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:e0a07082bb6fc6aeaae2315f335386f1705a646c72f9e0af512aebbca5cb2b15
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:b7633717cd7449c0042ae92d8ca9b36e4d69566561f5c7d44e21058e7d05c6d5

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:2d39989846c3579dd020b9f6c77e6e314cc81aa344eaac0f6d633e723c17196d
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:e1c0eb54d0ddadc8c7d72cee4f4282d56c957bf4ff0f62ef37db6b38dd0e8a01

View File

@@ -1,5 +1,5 @@
diff --git a/pkg/controller/kubevirteps/kubevirteps_controller.go b/pkg/controller/kubevirteps/kubevirteps_controller.go
index 53388eb8e..28644236f 100644
index 53388eb8e..873060251 100644
--- a/pkg/controller/kubevirteps/kubevirteps_controller.go
+++ b/pkg/controller/kubevirteps/kubevirteps_controller.go
@@ -12,7 +12,6 @@ import (
@@ -10,12 +10,17 @@ index 53388eb8e..28644236f 100644
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -669,35 +668,50 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
@@ -666,38 +665,62 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
// for extracting the nodes it does not matter what type of address we are dealing with
// all nodes with an endpoint for a corresponding slice will be selected.
nodeSet := sets.Set[string]{}
+ hasEndpointsWithoutNodeName := false
for _, slice := range tenantSlices {
for _, endpoint := range slice.Endpoints {
// find all unique nodes that correspond to an endpoint in a tenant slice
+ if endpoint.NodeName == nil {
+ klog.Warningf("Skipping endpoint without NodeName in slice %s/%s", slice.Namespace, slice.Name)
+ hasEndpointsWithoutNodeName = true
+ continue
+ }
nodeSet.Insert(*endpoint.NodeName)
@@ -23,6 +28,13 @@ index 53388eb8e..28644236f 100644
}
- klog.Infof("Desired nodes for service %s in namespace %s: %v", service.Name, service.Namespace, sets.List(nodeSet))
+ // Fallback: if no endpoints with NodeName were found, but there are endpoints without NodeName,
+ // distribute traffic to all VMIs (similar to ExternalTrafficPolicy=Cluster behavior)
+ if nodeSet.Len() == 0 && hasEndpointsWithoutNodeName {
+ klog.Infof("No endpoints with NodeName found for service %s/%s, falling back to all VMIs", service.Namespace, service.Name)
+ return c.getAllVMIEndpoints()
+ }
+
+ klog.Infof("Desired nodes for service %s/%s: %v", service.Namespace, service.Name, sets.List(nodeSet))
for _, node := range sets.List(nodeSet) {
@@ -68,7 +80,7 @@ index 53388eb8e..28644236f 100644
desiredEndpoints = append(desiredEndpoints, &discovery.Endpoint{
Addresses: []string{i.IP},
Conditions: discovery.EndpointConditions{
@@ -705,9 +719,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
@@ -705,9 +728,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
Serving: &serving,
Terminating: &terminating,
},
@@ -80,6 +92,71 @@ index 53388eb8e..28644236f 100644
}
}
}
@@ -716,6 +739,64 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
return desiredEndpoints
}
+// getAllVMIEndpoints returns endpoints for all VMIs in the infra namespace.
+// This is used as a fallback when tenant endpoints don't have NodeName specified,
+// similar to ExternalTrafficPolicy=Cluster behavior where traffic is distributed to all nodes.
+func (c *Controller) getAllVMIEndpoints() []*discovery.Endpoint {
+ var endpoints []*discovery.Endpoint
+
+ // List all VMIs in the infra namespace
+ vmiList, err := c.infraDynamic.
+ Resource(kubevirtv1.VirtualMachineInstanceGroupVersionKind.GroupVersion().WithResource("virtualmachineinstances")).
+ Namespace(c.infraNamespace).
+ List(context.TODO(), metav1.ListOptions{})
+ if err != nil {
+ klog.Errorf("Failed to list VMIs in namespace %q: %v", c.infraNamespace, err)
+ return endpoints
+ }
+
+ for _, obj := range vmiList.Items {
+ vmi := &kubevirtv1.VirtualMachineInstance{}
+ err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, vmi)
+ if err != nil {
+ klog.Errorf("Failed to convert Unstructured to VirtualMachineInstance: %v", err)
+ continue
+ }
+
+ if vmi.Status.NodeName == "" {
+ klog.Warningf("Skipping VMI %s/%s: NodeName is empty", vmi.Namespace, vmi.Name)
+ continue
+ }
+ nodeNamePtr := &vmi.Status.NodeName
+
+ ready := vmi.Status.Phase == kubevirtv1.Running
+ serving := vmi.Status.Phase == kubevirtv1.Running
+ terminating := vmi.Status.Phase == kubevirtv1.Failed || vmi.Status.Phase == kubevirtv1.Succeeded
+
+ for _, i := range vmi.Status.Interfaces {
+ if i.Name == "default" {
+ if i.IP == "" {
+ klog.Warningf("VMI %s/%s interface %q has no IP, skipping", vmi.Namespace, vmi.Name, i.Name)
+ continue
+ }
+ endpoints = append(endpoints, &discovery.Endpoint{
+ Addresses: []string{i.IP},
+ Conditions: discovery.EndpointConditions{
+ Ready: &ready,
+ Serving: &serving,
+ Terminating: &terminating,
+ },
+ NodeName: nodeNamePtr,
+ })
+ break
+ }
+ }
+ }
+
+ klog.Infof("Fallback: created %d endpoints from all VMIs in namespace %s", len(endpoints), c.infraNamespace)
+ return endpoints
+}
+
func (c *Controller) ensureEndpointSliceLabels(slice *discovery.EndpointSlice, svc *v1.Service) (map[string]string, bool) {
labels := make(map[string]string)
labelsChanged := false
diff --git a/pkg/controller/kubevirteps/kubevirteps_controller_test.go b/pkg/controller/kubevirteps/kubevirteps_controller_test.go
index 1c97035b4..d205d0bed 100644
--- a/pkg/controller/kubevirteps/kubevirteps_controller_test.go

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:d5c836ba33cf5dbed7e6f866784f668f80ffe69179e7c75847b680111984eefb
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:ae0c5e3e005388d865617097a9fe4b4e8a2be6b62cec893e08f2143d0acde241

View File

@@ -3,7 +3,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "tenant.name" . }}-cleanup
namespace: {{ include "tenant.name" . }}
namespace: cozy-system
annotations:
helm.sh/hook: pre-delete
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
@@ -39,13 +39,13 @@ roleRef:
subjects:
- kind: ServiceAccount
name: {{ include "tenant.name" . }}-cleanup
namespace: {{ include "tenant.name" . }}
namespace: cozy-system
---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "tenant.name" . }}-cleanup
namespace: {{ include "tenant.name" . }}
namespace: cozy-system
annotations:
helm.sh/hook: pre-delete
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded

View File

@@ -67,6 +67,19 @@ spec:
{{- end }}
{{- end }}
{{- end }}
{{- if ne (include "tenant.name" .) "tenant-root" }}
- toEndpoints:
{{- if hasPrefix "tenant-" .Release.Namespace }}
{{- $parts := splitList "-" .Release.Namespace }}
{{- range $i, $v := $parts }}
{{- if ne $i 0 }}
- matchLabels:
cozystack.io/service: ingress
"k8s:io.kubernetes.pod.namespace": {{ join "-" (slice $parts 0 (add $i 1)) }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy

View File

@@ -69,3 +69,36 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
{{- end }}
{{- $uuid }}
{{- end }}
{{/*
Node Affinity for Windows VMs
*/}}
{{- define "virtual-machine.nodeAffinity" -}}
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" -}}
{{- if $configMap -}}
{{- $dedicatedNodesForWindowsVMs := get $configMap.data "dedicatedNodesForWindowsVMs" -}}
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
affinity:
nodeAffinity:
{{- if $isWindows }}
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: scheduling.cozystack.io/vm-windows
operator: In
values:
- "true"
{{- else }}
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: scheduling.cozystack.io/vm-windows
operator: NotIn
values:
- "true"
{{- end }}
{{- end -}}
{{- end -}}
{{- end -}}

View File

@@ -1,4 +1,3 @@
{{- if .Values.external }}
---
apiVersion: v1
kind: Service
@@ -7,17 +6,24 @@ metadata:
labels:
apps.cozystack.io/user-service: "true"
{{- include "virtual-machine.labels" . | nindent 4 }}
{{- if .Values.external }}
annotations:
networking.cozystack.io/wholeIP: "true"
{{- end }}
spec:
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
{{- if .Values.external }}
externalTrafficPolicy: Local
{{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }}
allocateLoadBalancerNodePorts: false
{{- end }}
{{- else }}
clusterIP: None
{{- end }}
selector:
{{- include "virtual-machine.selectorLabels" . | nindent 4 }}
ports:
{{- if .Values.external }}
{{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }}
- port: 65535
{{- else }}
@@ -27,4 +33,6 @@ spec:
targetPort: {{ . }}
{{- end }}
{{- end }}
{{- else }}
- port: 65535
{{- end }}

View File

@@ -27,7 +27,11 @@
{{- if and $existingPVC $desiredStorage -}}
{{- $currentStorage := $existingPVC.spec.resources.requests.storage | toString -}}
{{- if not (eq $currentStorage $desiredStorage) -}}
{{- $needResizePVC = true -}}
{{- $oldSize := (include "cozy-lib.resources.toFloat" $currentStorage) | float64 -}}
{{- $newSize := (include "cozy-lib.resources.toFloat" $desiredStorage) | float64 -}}
{{- if gt $newSize $oldSize -}}
{{- $needResizePVC = true -}}
{{- end -}}
{{- end -}}
{{- end -}}

View File

@@ -124,6 +124,8 @@ spec:
terminationGracePeriodSeconds: 30
{{- include "virtual-machine.nodeAffinity" . | nindent 6 }}
volumes:
- name: systemdisk
dataVolume:

View File

@@ -1,5 +1,17 @@
{{- $existingPVC := lookup "v1" "PersistentVolumeClaim" .Release.Namespace .Release.Name }}
{{- if and $existingPVC (ne ($existingPVC.spec.resources.requests.storage | toString) .Values.storage) -}}
{{- $shouldResize := false -}}
{{- if and $existingPVC .Values.storage -}}
{{- $currentStorage := $existingPVC.spec.resources.requests.storage | toString -}}
{{- if ne $currentStorage .Values.storage -}}
{{- $oldSize := (include "cozy-lib.resources.toFloat" $currentStorage) | float64 -}}
{{- $newSize := (include "cozy-lib.resources.toFloat" .Values.storage) | float64 -}}
{{- if gt $newSize $oldSize -}}
{{- $shouldResize = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $shouldResize -}}
apiVersion: batch/v1
kind: Job
metadata:
@@ -23,6 +35,7 @@ spec:
command: ["sh", "-xec"]
args:
- |
echo "Resizing PVC to {{ .Values.storage }}..."
kubectl patch pvc {{ .Release.Name }} -p '{"spec":{"resources":{"requests":{"storage":"{{ .Values.storage }}"}}}}'
---
apiVersion: v1

View File

@@ -69,3 +69,36 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
{{- end }}
{{- $uuid }}
{{- end }}
{{/*
Node Affinity for Windows VMs
*/}}
{{- define "virtual-machine.nodeAffinity" -}}
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" -}}
{{- if $configMap -}}
{{- $dedicatedNodesForWindowsVMs := get $configMap.data "dedicatedNodesForWindowsVMs" -}}
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
affinity:
nodeAffinity:
{{- if $isWindows }}
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: scheduling.cozystack.io/vm-windows
operator: In
values:
- "true"
{{- else }}
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: scheduling.cozystack.io/vm-windows
operator: NotIn
values:
- "true"
{{- end }}
{{- end -}}
{{- end -}}
{{- end -}}

View File

@@ -1,4 +1,3 @@
{{- if .Values.external }}
---
apiVersion: v1
kind: Service
@@ -7,17 +6,24 @@ metadata:
labels:
apps.cozystack.io/user-service: "true"
{{- include "virtual-machine.labels" . | nindent 4 }}
{{- if .Values.external }}
annotations:
networking.cozystack.io/wholeIP: "true"
{{- end }}
spec:
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
{{- if .Values.external }}
externalTrafficPolicy: Local
{{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }}
allocateLoadBalancerNodePorts: false
{{- end }}
{{- else }}
clusterIP: None
{{- end }}
selector:
{{- include "virtual-machine.selectorLabels" . | nindent 4 }}
ports:
{{- if .Values.external }}
{{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }}
- port: 65535
{{- else }}
@@ -27,4 +33,6 @@ spec:
targetPort: {{ . }}
{{- end }}
{{- end }}
{{- else }}
- port: 65535
{{- end }}

View File

@@ -95,6 +95,9 @@ spec:
noCloud: {}
{{- end }}
terminationGracePeriodSeconds: 30
{{- include "virtual-machine.nodeAffinity" . | nindent 6 }}
volumes:
{{- range .Values.disks }}
- name: disk-{{ .name }}

View File

@@ -1,4 +1,4 @@
FROM golang:1.24-alpine as k8s-await-election-builder
FROM golang:1.24-alpine AS k8s-await-election-builder
ARG K8S_AWAIT_ELECTION_GITREPO=https://github.com/LINBIT/k8s-await-election
ARG K8S_AWAIT_ELECTION_VERSION=0.4.1
@@ -13,7 +13,7 @@ RUN git clone ${K8S_AWAIT_ELECTION_GITREPO} /usr/local/go/k8s-await-election/ \
&& make \
&& mv ./out/k8s-await-election-${TARGETARCH} /k8s-await-election
FROM golang:1.24-alpine as builder
FROM golang:1.24-alpine AS builder
ARG TARGETOS
ARG TARGETARCH

View File

@@ -1,2 +1,2 @@
cozystack:
image: ghcr.io/cozystack/cozystack/installer:v0.38.2@sha256:9ff92b655de6f9bea3cba4cd42dcffabd9aace6966dcfb1cc02dda2420ea4a15
image: ghcr.io/cozystack/cozystack/installer:v0.39.2@sha256:1c0eef43e1a06f5bf653c0bb7a2821a623fcad71c41c2c15b080fe0f6c809e39

View File

@@ -112,6 +112,12 @@ releases:
disableTelemetry: true
{{- end }}
- name: backup-controller
releaseName: backup-controller
chart: cozy-backup-controller
namespace: cozy-backup-controller
dependsOn: [cilium,kubeovn]
- name: lineage-controller-webhook
releaseName: lineage-controller-webhook
chart: cozy-lineage-controller-webhook

View File

@@ -56,6 +56,11 @@ releases:
disableTelemetry: true
{{- end }}
- name: backup-controller
releaseName: backup-controller
chart: cozy-backup-controller
namespace: cozy-backup-controller
- name: lineage-controller-webhook
releaseName: lineage-controller-webhook
chart: cozy-lineage-controller-webhook

0
packages/core/testing/Chart.yaml Executable file → Normal file
View File

0
packages/core/testing/Makefile Executable file → Normal file
View File

10
packages/core/testing/images/e2e-sandbox/Dockerfile Executable file → Normal file
View File

@@ -9,7 +9,7 @@ ARG TARGETOS
ARG TARGETARCH
RUN apt update -q
RUN apt install -yq --no-install-recommends psmisc genisoimage ca-certificates qemu-kvm qemu-utils iproute2 iptables wget xz-utils netcat curl jq make git
RUN apt install -yq --no-install-recommends psmisc genisoimage ca-certificates qemu-kvm qemu-utils iproute2 iptables wget xz-utils netcat curl jq make git bash-completion
RUN curl -sSL "https://github.com/siderolabs/talos/releases/download/v${TALOSCTL_VERSION}/talosctl-${TARGETOS}-${TARGETARCH}" -o /usr/local/bin/talosctl \
&& chmod +x /usr/local/bin/talosctl
RUN curl -sSL "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/${TARGETOS}/${TARGETARCH}/kubectl" -o /usr/local/bin/kubectl \
@@ -21,5 +21,13 @@ RUN curl -sSL "https://fluxcd.io/install.sh" | bash
RUN curl -sSL "https://github.com/cozystack/cozypkg/raw/refs/heads/main/hack/install.sh" | sh -s -- -v "${COZYPKG_VERSION}"
RUN curl https://dl.min.io/client/mc/release/${TARGETOS}-${TARGETARCH}/mc --create-dirs -o /usr/local/bin/mc \
&& chmod +x /usr/local/bin/mc
RUN <<'EOF'
cat <<'EOT' >> /etc/bash.bashrc
. /etc/bash_completion
. <(kubectl completion bash)
alias k=kubectl
complete -F __start_kubectl k
EOT
EOF
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

2
packages/core/testing/values.yaml Executable file → Normal file
View File

@@ -1,2 +1,2 @@
e2e:
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v0.38.2@sha256:84be9e42bc2c04b0765c8b89e0a9728c49ebf4676a92522b007af96ae9aec68d
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v0.39.2@sha256:9fe32b5faf233bdbae27859f1c6aaa229230b0ac9ee5212a1ddf2763182cea0f

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/matchbox:v0.38.2@sha256:9cd7f46fcae119a3f8e35b428b018d0cb6da7b0cdd2ce764cc9fbf6dcd903f27
ghcr.io/cozystack/cozystack/matchbox:v0.39.2@sha256:80da0287d6705ffaec918d1d7dc02f1d0a512045c56b78afb2707743d09e629e

View File

@@ -12,6 +12,13 @@ spec:
containers:
- name: etcd-defrag
image: ghcr.io/ahrtr/etcd-defrag:v0.13.0
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
args:
- --endpoints={{ range $i, $e := until (int .Values.replicas) }}{{ if $i }},{{ end }}https://{{ $.Release.Name }}-{{ $i }}.{{ $.Release.Name }}-headless.{{ $.Release.Namespace }}.svc:2379{{ end }}
- --cacert=/etc/etcd/pki/client/cert/ca.crt

View File

@@ -55,25 +55,26 @@
### Alerta configuration
| Name | Description | Type | Value |
| ----------------------------------------- | ----------------------------------------------------------------- | ---------- | ------- |
| `alerta` | Configuration for the Alerta service. | `object` | `{}` |
| `alerta.storage` | Persistent volume size for the database. | `string` | `10Gi` |
| `alerta.storageClassName` | StorageClass used for the database. | `string` | `""` |
| `alerta.resources` | Resource configuration. | `object` | `{}` |
| `alerta.resources.requests` | Resource requests. | `object` | `{}` |
| `alerta.resources.requests.cpu` | CPU request. | `quantity` | `100m` |
| `alerta.resources.requests.memory` | Memory request. | `quantity` | `256Mi` |
| `alerta.resources.limits` | Resource limits. | `object` | `{}` |
| `alerta.resources.limits.cpu` | CPU limit. | `quantity` | `1` |
| `alerta.resources.limits.memory` | Memory limit. | `quantity` | `1Gi` |
| `alerta.alerts` | Alert routing configuration. | `object` | `{}` |
| `alerta.alerts.telegram` | Configuration for Telegram alerts. | `object` | `{}` |
| `alerta.alerts.telegram.token` | Telegram bot token. | `string` | `""` |
| `alerta.alerts.telegram.chatID` | Telegram chat ID(s), separated by commas. | `string` | `""` |
| `alerta.alerts.telegram.disabledSeverity` | List of severities without alerts (e.g. "informational,warning"). | `string` | `""` |
| `alerta.alerts.slack` | Configuration for Slack alerts. | `object` | `{}` |
| `alerta.alerts.slack.url` | Configuration uri for Slack alerts. | `string` | `""` |
| Name | Description | Type | Value |
| ----------------------------------------- | --------------------------------------------------------------------- | ---------- | ------- |
| `alerta` | Configuration for the Alerta service. | `object` | `{}` |
| `alerta.storage` | Persistent volume size for the database. | `string` | `10Gi` |
| `alerta.storageClassName` | StorageClass used for the database. | `string` | `""` |
| `alerta.resources` | Resource configuration. | `object` | `{}` |
| `alerta.resources.requests` | Resource requests. | `object` | `{}` |
| `alerta.resources.requests.cpu` | CPU request. | `quantity` | `100m` |
| `alerta.resources.requests.memory` | Memory request. | `quantity` | `256Mi` |
| `alerta.resources.limits` | Resource limits. | `object` | `{}` |
| `alerta.resources.limits.cpu` | CPU limit. | `quantity` | `1` |
| `alerta.resources.limits.memory` | Memory limit. | `quantity` | `1Gi` |
| `alerta.alerts` | Alert routing configuration. | `object` | `{}` |
| `alerta.alerts.telegram` | Configuration for Telegram alerts. | `object` | `{}` |
| `alerta.alerts.telegram.token` | Telegram bot token. | `string` | `""` |
| `alerta.alerts.telegram.chatID` | Telegram chat ID(s), separated by commas. | `string` | `""` |
| `alerta.alerts.telegram.disabledSeverity` | List of severities without alerts (e.g. ["informational","warning"]). | `[]string` | `[]` |
| `alerta.alerts.slack` | Configuration for Slack alerts. | `object` | `{}` |
| `alerta.alerts.slack.url` | Configuration uri for Slack alerts. | `string` | `""` |
| `alerta.alerts.slack.disabledSeverity` | List of severities without alerts (e.g. ["informational","warning"]). | `[]string` | `[]` |
### Grafana configuration

View File

@@ -129,13 +129,19 @@ spec:
value: "{{ .Values.alerta.alerts.telegram.token }}"
- name: TELEGRAM_WEBHOOK_URL
value: "https://{{ printf "alerta.%s" (.Values.host | default $host) }}/api/webhooks/telegram?api-key={{ $apiKey }}"
{{- if .Values.alerta.alerts.telegram.disabledSeverity }}
- name: TELEGRAM_DISABLE_NOTIFICATION_SEVERITY
value: "{{ .Values.alerta.alerts.telegram.disabledSeverity }}"
value: {{ .Values.alerta.alerts.telegram.disabledSeverity | toJson | quote }}
{{- end }}
{{- end }}
{{- if .Values.alerta.alerts.slack.url }}
- name: "SLACK_WEBHOOK_URL"
value: "{{ .Values.alerta.alerts.slack.url }}"
{{- if .Values.alerta.alerts.slack.disabledSeverity }}
- name: SLACK_SEVERITY_FILTER
value: {{ .Values.alerta.alerts.slack.disabledSeverity | toJson | quote }}
{{- end }}
{{- end }}
ports:

View File

@@ -20,6 +20,14 @@
"url"
],
"properties": {
"disabledSeverity": {
"description": "List of severities without alerts (e.g. [\"informational\",\"warning\"]).",
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"url": {
"description": "Configuration uri for Slack alerts.",
"type": "string",
@@ -42,9 +50,12 @@
"default": ""
},
"disabledSeverity": {
"description": "List of severities without alerts (e.g. \"informational,warning\").",
"type": "string",
"default": ""
"description": "List of severities without alerts (e.g. [\"informational\",\"warning\"]).",
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"token": {
"description": "Telegram bot token.",

View File

@@ -99,10 +99,11 @@ logsStorages:
## @typedef {struct} TelegramAlerts - Telegram alert configuration.
## @field {string} token - Telegram bot token.
## @field {string} chatID - Telegram chat ID(s), separated by commas.
## @field {string} [disabledSeverity] - List of severities without alerts (e.g. "informational,warning").
## @field {[]string} [disabledSeverity] - List of severities without alerts (e.g. ["informational","warning"]).
## @typedef {struct} SlackAlerts - Slack alert configuration.
## @field {string} url - Configuration uri for Slack alerts.
## @field {[]string} [disabledSeverity] - List of severities without alerts (e.g. ["informational","warning"]).
## @typedef {struct} Alerts - Alert routing configuration.
## @field {TelegramAlerts} [telegram] - Configuration for Telegram alerts.
@@ -129,9 +130,10 @@ alerta:
telegram:
token: ""
chatID: ""
disabledSeverity: ""
disabledSeverity: []
slack:
url: ""
disabledSeverity: []
##
## @section Grafana configuration
##

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.38.2@sha256:ff3281fe53a97d2cd5cd94bd4c4d8ff08189508729869bb39b3f60c80da5f919
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.39.2@sha256:b180a6c4ce4d46cdf31af2731cc8b477e80ade99d4296d507ec5edb38c56a1ea

View File

@@ -1,11 +1,13 @@
OUT=../../_out/repos/system
CHARTS := $(shell find . -maxdepth 2 -name Chart.yaml | awk -F/ '{print $$2}')
CHARTS := $(shell grep -F 'version: 0.0.0' */Chart.yaml | cut -f1 -d/)
VERSIONED_CHARTS := $(shell grep '^version:' */Chart.yaml | grep -Fv '0.0.0' | cut -f1 -d/)
include ../../scripts/common-envs.mk
repo:
rm -rf "$(OUT)"
helm package -d "$(OUT)" $(CHARTS) --version $(COZYSTACK_VERSION)
helm package -d "$(OUT)" $(VERSIONED_CHARTS)
cd "$(OUT)" && helm repo index .
fix-charts:

View File

@@ -0,0 +1,3 @@
apiVersion: v2
name: cozy-backup-controller
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process

View File

@@ -0,0 +1,18 @@
NAME=backup-controller
NAMESPACE=cozy-backup-controller
include ../../../scripts/common-envs.mk
include ../../../scripts/package.mk
image: image-backup-controller
image-backup-controller:
docker buildx build -f images/backup-controller/Dockerfile ../../.. \
--tag $(REGISTRY)/backup-controller:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/backup-controller:latest \
--cache-to type=inline \
--metadata-file images/backup-controller.json \
$(BUILDX_ARGS)
IMAGE="$(REGISTRY)/backup-controller:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/backup-controller.json -o json -r)" \
yq -i '.backupController.image = strenv(IMAGE)' values.yaml
rm -f images/backup-controller.json

View File

@@ -0,0 +1,2 @@
* linguist-generated
.gitattributes -linguist-generated

View File

@@ -0,0 +1,241 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.4
name: backupjobs.backups.cozystack.io
spec:
group: backups.cozystack.io
names:
kind: BackupJob
listKind: BackupJobList
plural: backupjobs
singular: backupjob
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.phase
name: Phase
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: |-
BackupJob represents a single execution of a backup.
It is typically created by a Plan controller when a schedule fires.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: BackupJobSpec describes the execution of a single backup
operation.
properties:
applicationRef:
description: |-
ApplicationRef holds a reference to the managed application whose state
is being backed up.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
planRef:
description: |-
PlanRef refers to the Plan that requested this backup run.
For ad-hoc/manual backups, this can be omitted.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
storageRef:
description: |-
StorageRef holds a reference to the Storage object that describes where
the backup will be stored.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
strategyRef:
description: |-
StrategyRef holds a reference to the driver-specific BackupStrategy object
that describes how the backup should be created.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
required:
- applicationRef
- storageRef
- strategyRef
type: object
status:
description: BackupJobStatus represents the observed state of a BackupJob.
properties:
backupRef:
description: BackupRef refers to the Backup object created by this
run, if any.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
completedAt:
description: |-
CompletedAt is the time at which the backup run completed (successfully
or otherwise).
format: date-time
type: string
conditions:
description: Conditions represents the latest available observations
of a BackupJob's state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
message:
description: |-
Message is a human-readable message indicating details about why the
backup run is in its current phase, if any.
type: string
phase:
description: |-
Phase is a high-level summary of the run's state.
Typical values: Pending, Running, Succeeded, Failed.
type: string
startedAt:
description: StartedAt is the time at which the backup run started.
format: date-time
type: string
type: object
type: object
selectableFields:
- jsonPath: .spec.applicationRef.apiGroup
- jsonPath: .spec.applicationRef.kind
- jsonPath: .spec.applicationRef.name
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,238 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.4
name: backups.backups.cozystack.io
spec:
group: backups.cozystack.io
names:
kind: Backup
listKind: BackupList
plural: backups
singular: backup
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Backup represents a single backup artifact for a given application.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: BackupSpec describes an immutable backup artifact produced
by a BackupJob.
properties:
applicationRef:
description: ApplicationRef refers to the application that was backed
up.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
driverMetadata:
additionalProperties:
type: string
description: |-
DriverMetadata holds driver-specific, opaque metadata associated with
this backup (for example snapshot IDs, schema versions, etc.).
This data is not interpreted by the core backup controllers.
type: object
planRef:
description: |-
PlanRef refers to the Plan that produced this backup, if any.
For manually triggered backups, this can be omitted.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
storageRef:
description: |-
StorageRef refers to the Storage object that describes where the backup
artifact is stored.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
strategyRef:
description: |-
StrategyRef refers to the driver-specific BackupStrategy that was used
to create this backup. This allows the driver to later perform restores.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
takenAt:
description: |-
TakenAt is the time at which the backup was taken (as reported by the
driver). It may differ slightly from metadata.creationTimestamp.
format: date-time
type: string
required:
- applicationRef
- storageRef
- strategyRef
- takenAt
type: object
status:
description: BackupStatus represents the observed state of a Backup.
properties:
artifact:
description: Artifact describes the stored backup object, if available.
properties:
checksum:
description: |-
Checksum is the checksum of the artifact, if computed.
For example: "sha256:<hex>".
type: string
sizeBytes:
description: SizeBytes is the size of the artifact in bytes, if
known.
format: int64
type: integer
uri:
description: |-
URI is a driver-/storage-specific URI pointing to the backup artifact.
For example: s3://bucket/prefix/file.tar.gz
type: string
required:
- uri
type: object
conditions:
description: Conditions represents the latest available observations
of a Backup's state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
phase:
description: |-
Phase is a simple, high-level summary of the backup's state.
Typical values are: Pending, Ready, Failed.
type: string
type: object
type: object
selectableFields:
- jsonPath: .spec.applicationRef.apiGroup
- jsonPath: .spec.applicationRef.kind
- jsonPath: .spec.applicationRef.name
served: true
storage: true

View File

@@ -0,0 +1,200 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.4
name: plans.backups.cozystack.io
spec:
group: backups.cozystack.io
names:
kind: Plan
listKind: PlanList
plural: plans
singular: plan
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: |-
Plan describes the schedule, method and storage location for the
backup of a given target application.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: |-
PlanSpec references the storage, the strategy, the application to be
backed up and specifies the timetable on which the backups will run.
properties:
applicationRef:
description: |-
ApplicationRef holds a reference to the managed application,
whose state and configuration must be backed up.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
schedule:
description: Schedule specifies when backup copies are created.
properties:
cron:
description: |-
Cron contains the cron spec for scheduling backups. Must be
specified if the schedule type is `cron`. Since only `cron` is
supported, omitting this field is not allowed.
type: string
type:
description: |-
Type is the type of schedule specification. Supported values are
[`cron`]. If omitted, defaults to `cron`.
type: string
type: object
storageRef:
description: |-
StorageRef holds a reference to the Storage object that
describes the location where the backup will be stored.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
strategyRef:
description: |-
StrategyRef holds a reference to the Strategy object that
describes, how a backup copy is to be created.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
required:
- applicationRef
- schedule
- storageRef
- strategyRef
type: object
status:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
type: object
type: object
selectableFields:
- jsonPath: .spec.applicationRef.apiGroup
- jsonPath: .spec.applicationRef.kind
- jsonPath: .spec.applicationRef.name
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,174 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.4
name: restorejobs.backups.cozystack.io
spec:
group: backups.cozystack.io
names:
kind: RestoreJob
listKind: RestoreJobList
plural: restorejobs
singular: restorejob
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.phase
name: Phase
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: RestoreJob represents a single execution of a restore from a
Backup.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: RestoreJobSpec describes the execution of a single restore
operation.
properties:
backupRef:
description: BackupRef refers to the Backup that should be restored.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
targetApplicationRef:
description: |-
TargetApplicationRef refers to the application into which the backup
should be restored. If omitted, the driver SHOULD restore into the same
application as referenced by backup.spec.applicationRef.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
required:
- backupRef
type: object
status:
description: RestoreJobStatus represents the observed state of a RestoreJob.
properties:
completedAt:
description: |-
CompletedAt is the time at which the restore run completed (successfully
or otherwise).
format: date-time
type: string
conditions:
description: Conditions represents the latest available observations
of a RestoreJob's state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
message:
description: |-
Message is a human-readable message indicating details about why the
restore run is in its current phase, if any.
type: string
phase:
description: |-
Phase is a high-level summary of the run's state.
Typical values: Pending, Running, Succeeded, Failed.
type: string
startedAt:
description: StartedAt is the time at which the restore run started.
format: date-time
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,23 @@
FROM golang:1.24-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
COPY go.mod go.sum ./
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go mod download
COPY api api/
COPY pkg pkg/
COPY cmd cmd/
COPY internal internal/
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /backup-controller cmd/backup-controller/main.go
FROM scratch
COPY --from=builder /backup-controller /backup-controller
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ENTRYPOINT ["/backup-controller"]

View File

@@ -0,0 +1,4 @@
{{- range $path, $_ := .Files.Glob "definitions/*.yaml" }}
---
{{ $.Files.Get $path }}
{{- end }}

View File

@@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backup-controller
labels:
app: backup-controller
spec:
replicas: {{ .Values.backupController.replicas }}
selector:
matchLabels:
app: backup-controller
template:
metadata:
labels:
app: backup-controller
spec:
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
serviceAccountName: backup-controller
containers:
- name: backup-controller
image: "{{ .Values.backupController.image }}"
args:
- --leader-elect
{{- if .Values.backupController.metrics.enable }}
- --metrics-bind-address={{ .Values.backupController.metrics.bindAddress }}
{{- end }}
{{- if .Values.backupController.debug }}
- --zap-log-level=debug
{{- else }}
- --zap-log-level=info
{{- end }}
ports:
- name: metrics
containerPort: {{ splitList ":" .Values.backupController.metrics.bindAddress | mustLast }}
- name: health
containerPort: 8081
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 15
periodSeconds: 20
{{- with .Values.backupController.resources }}
resources: {{- . | toYaml | nindent 10 }}
{{- end }}

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backups.cozystack.io:core-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: backups.cozystack.io:core-controller
subjects:
- kind: ServiceAccount
name: backup-controller
namespace: {{ .Release.Namespace }}

View File

@@ -0,0 +1,32 @@
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: backups.cozystack.io:core-controller
rules:
- apiGroups: ["backups.cozystack.io"]
resources: ["plans"]
verbs: ["get", "list", "watch"]
- apiGroups: ["backups.cozystack.io"]
resources: ["backupjobs"]
verbs: ["create", "get", "list", "watch", "update", "patch"]
- apiGroups: ["backups.cozystack.io"]
resources: ["backupjobs/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["backups.cozystack.io"]
resources: ["backups"]
verbs: ["create", "get", "list", "watch"]
- apiGroups: ["apps.cozystack.io"]
resources: ["buckets", "bucketaccesses", "virtualmachines"]
verbs: ["get", "list", "watch"]
- apiGroups: ["objectstorage.k8s.io"]
resources: ["buckets", "bucketaccesses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "list", "watch", "update", "patch"]
- apiGroups: ["kubevirt.io"]
resources: ["virtualmachines"]
verbs: ["get", "list", "watch"]
- apiGroups: ["velero.io"]
resources: ["backups", "backupstoragelocations", "volumesnapshotlocations", "restores"]
verbs: ["create", "get", "list", "watch", "update", "patch"]

View File

@@ -0,0 +1,4 @@
kind: ServiceAccount
apiVersion: v1
metadata:
name: backup-controller

View File

@@ -0,0 +1,5 @@
apiVersion: strategy.backups.cozystack.io/v1alpha1
kind: Velero
metadata:
name: velero-strategy-default
spec: {}

View File

@@ -0,0 +1,14 @@
backupController:
image: ""
replicas: 2
debug: false
metrics:
enabled: true
bindAddress: ":8443"
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi

View File

@@ -0,0 +1,3 @@
apiVersion: v2
name: cozy-backup-controller
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process

View File

@@ -0,0 +1,18 @@
NAME=backupstrategy-controller
NAMESPACE=cozy-backup-controller
include ../../../scripts/common-envs.mk
include ../../../scripts/package.mk
image: image-backupstrategy-controller
image-backupstrategy-controller:
docker buildx build -f images/backupstrategy-controller/Dockerfile ../../.. \
--tag $(REGISTRY)/backupstrategy-controller:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/backupstrategy-controller:latest \
--cache-to type=inline \
--metadata-file images/backupstrategy-controller.json \
$(BUILDX_ARGS)
IMAGE="$(REGISTRY)/backupstrategy-controller:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/backupstrategy-controller.json -o json -r)" \
yq -i '.backupStrategyController.image = strenv(IMAGE)' values.yaml
rm -f images/backupstrategy-controller.json

View File

@@ -0,0 +1 @@
*.yaml linguist-generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
FROM golang:1.24-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
COPY go.mod go.sum ./
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go mod download
COPY api api/
COPY pkg pkg/
COPY cmd cmd/
COPY internal internal/
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /backupstrategy-controller cmd/backupstrategy-controller/main.go
FROM scratch
COPY --from=builder /backupstrategy-controller /backupstrategy-controller
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ENTRYPOINT ["/backupstrategy-controller"]

View File

@@ -0,0 +1,4 @@
{{- range $path, $_ := .Files.Glob "definitions/*.yaml" }}
---
{{ $.Files.Get $path }}
{{- end }}

View File

@@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backupstrategy-controller
labels:
app: backupstrategy-controller
spec:
replicas: {{ .Values.backupStrategyController.replicas }}
selector:
matchLabels:
app: backupstrategy-controller
template:
metadata:
labels:
app: backupstrategy-controller
spec:
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
serviceAccountName: backupstrategy-controller
containers:
- name: backupstrategy-controller
image: "{{ .Values.backupStrategyController.image }}"
args:
- --leader-elect
{{- if .Values.backupStrategyController.metrics.enable }}
- --metrics-bind-address={{ .Values.backupStrategyController.metrics.bindAddress }}
{{- end }}
{{- if .Values.backupStrategyController.debug }}
- --zap-log-level=debug
{{- else }}
- --zap-log-level=info
{{- end }}
ports:
- name: metrics
containerPort: {{ splitList ":" .Values.backupStrategyController.metrics.bindAddress | mustLast }}
- name: health
containerPort: 8081
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 15
periodSeconds: 20
{{- with .Values.backupStrategyController.resources }}
resources: {{- . | toYaml | nindent 10 }}
{{- end }}

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backups.cozystack.io:strategy-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: backups.cozystack.io:strategy-controller
subjects:
- kind: ServiceAccount
name: backupstrategy-controller
namespace: {{ .Release.Namespace }}

View File

@@ -0,0 +1,11 @@
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: backups.cozystack.io:strategy-controller
rules:
- apiGroups: ["strategy.backups.cozystack.io"]
resources: ["*"]
verbs: ["get", "list", "watch"]
- apiGroups: ["backups.cozystack.io"]
resources: ["backupjobs", "restorejobs"]
verbs: ["get", "list", "watch"]

View File

@@ -0,0 +1,4 @@
kind: ServiceAccount
apiVersion: v1
metadata:
name: backupstrategy-controller

View File

@@ -0,0 +1,14 @@
backupStrategyController:
image: ""
replicas: 2
debug: false
metrics:
enabled: true
bindAddress: ":8443"
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:3825c9b4b6238f88f1b0de73bd18866a7e5f83f178d28fe2830f3bf24efb187d
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:7dde8d748442c4457fb7bf67c0e0f4abf0da60af00a9171c6d4c0bfcc8a8b9c5

View File

@@ -10,6 +10,7 @@ cilium:
enabled: true
loadBalancer:
algorithm: maglev
serviceTopology: true
ipam:
mode: "kubernetes"
image:
@@ -18,3 +19,6 @@ cilium:
digest: "sha256:81262986a41487bfa3d0465091d3a386def5bd1ab476350bd4af2fdee5846fe6"
envoy:
enabled: false
rollOutCiliumPods: true
operator:
rollOutPods: true

View File

@@ -3,3 +3,6 @@ coredns:
repository: registry.k8s.io/coredns/coredns
tag: v1.12.4
replicaCount: 2
k8sAppLabelOverride: kube-dns
service:
name: kube-dns

View File

@@ -21,6 +21,8 @@ spec:
labels:
app: cozystack-api
spec:
tolerations:
- operator: Exists
serviceAccountName: cozystack-api
{{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }}
nodeSelector:

View File

@@ -1,5 +1,5 @@
cozystackAPI:
image: ghcr.io/cozystack/cozystack/cozystack-api:v0.38.2@sha256:d17f1c59658731e5a2063c3db348adbc03b5cd31720052016b68449164cf2f14
image: ghcr.io/cozystack/cozystack/cozystack-api:v0.39.2@sha256:e859dc81b4905fa163b75b1d567e91bd1eac572f85931954c2dc646cb07d3959
localK8sAPIEndpoint:
enabled: true
replicas: 2

View File

@@ -1,6 +1,6 @@
cozystackController:
image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.38.2@sha256:468b2eccbc0aa00bd3d72d56624a46e6ba178fa279cdd19248af74d32ea7d319
image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.39.2@sha256:b96283a1774f23735849802069a907cb85f4fd57df89f8740a954a7d2922afc3
debug: false
disableTelemetry: false
cozystackVersion: "v0.38.2"
cozystackVersion: "v0.39.2"
cozystackAPIKind: "DaemonSet"

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{{- $brandingConfig:= lookup "v1" "ConfigMap" "cozy-system" "cozystack-branding" }}
{{- $tenantText := "v0.38.2" }}
{{- $tenantText := "v0.39.2" }}
{{- $footerText := "Cozystack" }}
{{- $titleText := "Cozystack Dashboard" }}
{{- $logoText := "" }}

Some files were not shown because too many files have changed in this diff Show More