Compare commits

...

57 Commits

Author SHA1 Message Date
Timofei Larkin
15ce380f14 [backups][WIP] Draft Job strategy implementation
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-12 16:12:57 +03:00
Timofei Larkin
ee2a34ca81 [backups] Stub the Job backup strategy controller
## 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.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-12 16:07:48 +03:00
Timofei Larkin
0d71525f7e [backups] Scaffold a backup strategy API group
## 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.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-12 13:24:54 +03:00
Timofei Larkin
4f5ae287f5 [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.
```
2025-12-10 13:37:13 +04:00
Timofei Larkin
6b8c490b1d [backups] Fix malformed glob and split in template
## 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.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-10 11:55:37 +03:00
Andrei Kvapil
8513dd6b3f [virtual-machine] Improve check for resizing job (#1688)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

PVC resizing now only occurs when storage is being increased, preventing
unintended storage reduction operations

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[virtual-machine] Improve check for resizing job
```

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

* **Bug Fixes**
* PVC resizing now only triggers when the requested storage increases,
preventing unintended shrink attempts.

* **Enhancements**
* More robust storage-size comparison and validation to accurately
detect growth.
* Resize operations now run only when needed, reducing unnecessary jobs.

* **New Features**
* Added conditional pre-install/pre-upgrade hook workflow to perform
controlled PVC resize jobs.

* **Chores**
  * Minor template formatting cleanup.

<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-09 17:08:39 +01:00
Andrei Kvapil
0bab895026 [virtual-machine] Improve check for resizing job
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-09 17:08:16 +01:00
Andrei Kvapil
349677ffe9 [dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties (#1692)
## What this PR does

Fixes the logic for generating CustomFormsOverride schema to properly
nest properties under `spec.properties` instead of directly under
`properties`.

Changes:
- Updated `buildMultilineStringSchema` to check for `spec` property in
OpenAPI schema
- Process `spec.properties` instead of root `properties`
- Create schema structure as `schema.properties.spec.properties.*`
- Updated tests to reflect the new nested structure

### Release note

```release-note
[dashboard] Fix CustomFormsOverride schema generation to nest properties under spec.properties
```


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

## Summary by CodeRabbit

* **Refactor**
* Updated custom form schema generation to organize form fields within a
nested structure for improved organization.

* **Tests**
* Expanded test coverage to validate the new schema organization and
field type handling across nested levels.

<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-09 17:04:41 +01:00
Andrei Kvapil
1f47fbc3dd [linstor] Update piraeus-operator v2.10.2 (#1689)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

This release updates LINSTOR CSI to fix issues with the new fsck
behaviour.

```
MountVolume.SetUp failed for volume "pvc-37245cc3-bf28-4da4-a127-e5c9c03cc088" : rpc error: code = Internal desc = NodePublishVolume failed for pvc-37245cc3-bf28-4da4-a127-e5c9c03cc088: failed to run fsck on device '/dev/zvol/data/pvc-37245cc3-bf28-4da4-a127-e5c9c03cc088_00000': failed to run fsck: output: "fsck from util-linux 2.41
/dev/zd832 is mounted.
e2fsck: Cannot continue, aborting.
```

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[linstor] Update piraeus-operator v2.10.2
```
2025-12-09 14:38:02 +01:00
Andrei Kvapil
d079dd4731 [ci] Update korthout/backport-action@v3.2.1
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-09 14:34:37 +01:00
Andrei Kvapil
f3207fcd10 [ci] Improve backport workflow with merge_commits skip and conflict resolution (#1694)
This PR improves the backport workflow by:

- Adding `merge_commits: skip` to skip merge commits during backport
- Adding `conflict_resolution: draft_commit_conflicts` to create draft
PRs when conflicts occur instead of failing
- Removing the 'Report if backport failed' step as it's no longer needed
with the new conflict resolution strategy

These changes ensure that backports handle merge commits and conflicts
more gracefully.

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

## Summary by CodeRabbit

* **Chores**
* Enhanced the backport workflow to support simultaneous backporting to
current and previous release branches based on PR labels
* Added validation to ensure target branches exist before attempting
backports
* Improved workflow reliability by separating preparation and execution
stages

<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-09 14:25:32 +01:00
Andrei Kvapil
650e5290ea [ci] Improve backport workflow with merge_commits skip and conflict resolution
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-09 14:25:06 +01:00
Andrei Kvapil
19586e1eec Fix: Add missing components to distro-full bundle (#1620)
The `distro-full` bundle was missing critical components that exist in
paas-full, causing multiple pod failures during installation. This PR
adds the missing packages and fixes dependency issues.

When installing cozystack with bundle-name: "distro-full", several pods
failed to start:

  1. CozystackResourceDefinition CRD missing
    - cozystack-controller pod: CrashLoopBackOff
    - lineage-controller-webhook pods: CrashLoopBackOff
- Error: no matches for kind "CozystackResourceDefinition" in version
"cozystack.io/v1alpha1"


a861814c24/packages/system/cozystack-resource-definition-crd/definition/cozystack.io_cozystackresourcedefinitions.yaml (L1-L14)

  2. selfsigned-cluster-issuer ClusterIssuer missing
- snapshot-validation-webhook pods: ContainerCreating (waiting for TLS
secret)
    - snapshot-controller HelmRelease: Failed to install (timeout)
- Error: clusterissuer.cert-manager.io "selfsigned-cluster-issuer" not
found


a861814c24/packages/system/cert-manager-issuers/templates/cluster-issuers.yaml (L52-L57)


a861814c24/packages/system/snapshot-controller/template/clusterissuer.yaml (L1-L8)

  3. Cascading failures
    - linstor HelmRelease: Blocked (depends on snapshot-controller)
    



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

* **New Features**
* Added two new resource-definition components to the platform
distribution for enhanced configuration management.

* **Improvements**
* Made certificate issuer components required during deployment (no
longer optional).
* Adjusted snapshot controller startup order to wait for certificate
issuers, improving startup reliability.

<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-09 13:23:53 +01:00
Andrei Kvapil
578a810413 [dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-08 23:27:24 +01:00
Timofei Larkin
89897914fa [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
```
2025-12-07 16:20:09 +04:00
Timofei Larkin
892855276b [backups] Build and deploy backup controller
## 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
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-07 14:36:04 +03:00
Andrei Kvapil
58dd1f5881 [linstor] Update piraeus-operator v2.10.2
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-06 19:57:11 +01:00
Timofei Larkin
67ecf3d0f6 [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 -->
2025-12-06 00:15:02 +04:00
Timofei Larkin
38d6b98a70 [kubeovn] Package from external repo (#1535)
## What this PR does

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

```release-note
[kubeovn] Decouple the release cycle of kubeovn from Cozystack's release
cycle, preventing unnecessary redeployments of all Helm releases, when
upgrading Cozystack.
```
2025-12-05 23:33:50 +04:00
Apinant U-suwantim
0a93972c4f fixed: correct dependsOn distro-full
Signed-off-by: Apinant U-suwantim <Hello@Apinant.dev>
2025-12-05 22:15:22 +07:00
Apinant U-suwantim
da4d6053bb fixed: missing component bundle distro-full
Signed-off-by: Apinant U-suwantim <Hello@Apinant.dev>
2025-12-05 22:15:22 +07:00
Timofei Larkin
28c933161a Merge branch 'main' into feat/external-kubeovn-build
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-05 18:47:08 +04:00
Timofei Larkin
e50950b7a1 [kubeovn] Bump versions, drop build from Makefile
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-05 17:46:04 +03:00
Andrei Kvapil
b1a55f5a38 Granular kubernetes application extensions dependencies (#1683)
## What this PR does
More granular k8s app extensions dependencies.

As a part of the change, packages were updated:
- monitoring-agents: metrics-server moved to a separate package
- monitoring-agents: prometheus-operator-crds moved to a separate
package

Main Cozystack bundles were updated to reflect packages changes. 

### Release note
```release-note
Granular k8s extensions dependencies.
```

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

* **New Features**
* Added a standalone metrics-server release and a
prometheus-operator-crds release.

* **Improvements**
* Expanded Prometheus-related CRDs with new configuration options and
status subresources.
* Adjusted monitoring orchestration so components (cilium, metrics,
CRDs) install in a clearer order.

* **Refactor**
* Split metrics-server out of the monitoring-agents bundle and moved
Prometheus CRDs into a dedicated package.

<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-05 14:44:53 +01:00
Andrei Kvapil
1ee6eb8482 Update Flux Operator (v0.33.0) (#1649)
https://github.com/fluxcd/flux2/discussions/5572

There are some upgrade instructions for Flux 2.7, but I don't think that
they apply to Cozystack.

They might be relevant for some users though, depending on how they are
using Flux in their clusters - Cozystack only uses HelmReleases AFAIK
(and now gitrepository and helmrelease for external apps) so there is no
problem with the Flux 2.7 upgrade, which brings Image APIs to GA (v1)

That change has no impact on Cozystack because we don't depend on the
Flux Image APIs yet.

I haven't tested the upgrade to Flux 2.7, but I will include the bump in
the FluxInstance definition since Flux 2.7 has been out for a while
already, has had several patch releases, and is probably stable for the
next CozyStack release now!

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

* **New Features**
* Added distribution variant option for FluxInstance (upstream-alpine,
enterprise-alpine, enterprise-distroless)
* Added status fields to improve tracking of force and reconcile
annotation handling across resources

* **Chores**
* Bumped Flux Operator and Flux Instance charts to 0.33.0 (appVersion
v0.33.0)
  * Upgraded Flux distribution to 2.7.x
  * Added/cleaned up network policy templates and README badges

<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-05 14:42:43 +01:00
Andrei Kvapil
cbfa99148b [monitoring] Improve tenant metrics collection (#1684)
This patch introduces vmagent support inside tenant-level monitoring
stacks, ensuring that all tenant workloads have consistent and reliable
metrics scraping. By deploying a vmagent per tenant and wiring it to the
configured VictoriaMetrics short- and long-term storage, metrics are now
properly collected, labeled, and remote-written. This resolves gaps
where tenant environments previously missed metrics due to the absence
of dedicated agents.

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Release note
```release-note
[monitoring,platform] Add vmagent integration into tenant monitoring
stacks to ensure complete and correct metrics collection for all tenants.
```

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

* **New Features**
* Added VictoriaMetrics Agent (Vmagent) support in monitoring:
configurable external labels and remote-write endpoints for metric
collection.

* **Documentation**
* Added README documentation for the new Vmagent configuration options
and example defaults.

<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-05 14:41:10 +01:00
Andrei Kvapil
54d0f52245 [postgres] Add version management system with automated version updates (#1671)
This PR adds a version management system for PostgreSQL that
automatically fetches and updates supported versions from the
CloudNativePG PostgreSQL container registry.

## Changes
- Added `hack/update-versions.sh` script to fetch and update PostgreSQL
versions
- Added `files/versions.yaml` to store version mappings
- Added `templates/_versions.tpl` template for version handling
- Updated `values.yaml` with version enum and parameter
- Updated `templates/db.yaml` and `templates/init-job.yaml` to use
version management
- Updated `Makefile` to include version update workflow

## Features
- Automatically fetches supported major versions from GitHub
- Queries container registry for latest minor versions
- Updates version enum in values.yaml
- Maintains version mappings in versions.yaml

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

* **New Features**
* Added PostgreSQL version selection (v13–v18) with default v18; images
now follow the selected version via a resolved version map.

* **Chores**
* Added an automated update workflow to refresh supported PostgreSQL
versions and regenerate mappings.
  * Packaging ignore updated to exclude hack tooling.

* **Schema**
* Exposed a version property in chart and resource schemas for
validation and ordering.

<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-05 14:38:50 +01:00
Andrei Kvapil
d0bfb6e2fc [kubernetes] Add version management system with automated version updates (#1672)
This PR adds a version management system for Kubernetes that
automatically fetches and updates supported versions based on kamaji.

## Changes
- Added `hack/update-versions.sh` script to fetch and update Kubernetes
versions
- Updated `files/versions.yaml` with version mappings
- Updated `values.yaml` with version enum and parameter
- Updated `Makefile` to include version update workflow
- Added `hack` directory to `.helmignore`

## Features
- Automatically fetches Kubernetes version from kamaji repository
- Gets latest patch version for the supported minor version
- Queries container registry for available versions
- Updates version enum in values.yaml using reliable awk-based approach
- Updates KUBERNETES_VERSION in Makefile
- Maintains version mappings in versions.yaml

## Workflow
- Reads kamaji version from Dockerfile
- Fetches Kubernetes version from kamaji's kubeadm_version.go
- Finds latest patch version for that minor version
- Includes supported version + 5 previous minor versions
- Updates all configuration files automatically

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

## Summary by CodeRabbit

* **New Features**
* Kubernetes version parameter now uses an enum constraint to limit
selection to supported versions (v1.33, v1.32, v1.31, v1.30, v1.29,
v1.28).

* **Documentation**
  * Simplified Kubernetes version field description for clarity.

* **Chores**
* Updated Kubernetes patch version mappings (v1.32→v1.32.10,
v1.31→v1.31.14).
  * Relaxed add-on schema validation requirements.

<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-05 14:38:22 +01:00
Andrei Kvapil
f86896eceb [redis] Add version management system with automated version updates (#1681)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[redis] Add version management system with automated version updates
```
2025-12-05 14:37:56 +01:00
Andrei Kvapil
cb320f9d48 [mariadb] Add version management system with automated version updates (#1680)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[mariadb] Add version management system with automated version updates
```

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

## Summary by CodeRabbit

* **New Features**
* Added configurable MariaDB version selection for deployments.
Supported versions: v11.8, v11.4, v10.11, and v10.6 (default: v11.8).

<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-05 14:37:30 +01:00
Timofei Larkin
a7b423934f [backups] Implement core backup Plan controller
## 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.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-05 11:58:00 +03:00
nbykov0
b1a7e9560e [apps:kubernetes] dont delete cleanup job on fail
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-04 12:10:56 +03:00
nbykov0
fe9d334880 [apps:kubernetes] update dependencies
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-04 12:10:56 +03:00
nbykov0
1e2b66131c [apps:kubernetes] separate prometheus-operator-crds
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-04 12:10:56 +03:00
nbykov0
8d14bcb598 [system:victoria-metrics-operator] separate prometheus-operator-crds
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-04 12:10:56 +03:00
nbykov0
825390c209 [core:bundles] separate prometheus-operator-crds
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-04 12:10:46 +03:00
IvanHunters
09fd7c4094 fix crd
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2025-12-03 22:30:29 +03:00
IvanHunters
8928731abf [monitoring] Improve tenant metrics collection
This patch introduces vmagent support inside tenant-level monitoring
stacks, ensuring that all tenant workloads have consistent and reliable
metrics scraping. By deploying a vmagent per tenant and wiring it to the
configured VictoriaMetrics short- and long-term storage, metrics are now
properly collected, labeled, and remote-written. This resolves gaps
where tenant environments previously missed metrics due to the absence
of dedicated agents.

```release-note
[monitoring,platform] Add vmagent integration into tenant monitoring
stacks to ensure complete and correct metrics collection for all tenants.
```

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2025-12-03 22:18:19 +03:00
nbykov0
153635379a [system] separate prometheus-operator-crds
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-03 16:36:14 +03:00
nbykov0
fbb2ea095a [apps:kubernetes] separate metrics-server
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-03 16:36:03 +03:00
nbykov0
50c1d1a067 [core:bundles] separate metrics-server
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-02 21:02:39 +03:00
Nikita
52ebcae8a2 [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-02 18:10:26 +03:00
nbykov0
5314d61987 [core:installer] Fix buildx warnings
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-02 17:34:07 +03:00
Nikita
8b29c53a45 [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-02 14:48:34 +03:00
Nikita
4e4a5606d7 [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-02 12:55:29 +03:00
nbykov0
f2a4f1b1c8 [system:coredns] rename service to kube-dns
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-02 12:54:36 +03:00
Andrei Kvapil
33128748e6 [mariadb] Add version management system with automated version updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-02 00:36:00 +01:00
Andrei Kvapil
7e1cad26e7 [redis] Add version management system with automated version updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-02 00:34:52 +01:00
Andrei Kvapil
3d5118f5b3 [postgres] Add version management system with automated version updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-01 23:56:55 +01:00
Andrei Kvapil
06a25c1c45 [kubernetes] Add version management system with automated version updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-01 23:55:27 +01:00
nbykov0
67e47256e2 [system:monitoring-agents] separate metrics-server
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-01 20:51:26 +03:00
nbykov0
df277b350c [system:coredns] update app coredns labels to match Talos coredns labels
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-01 17:51:27 +03:00
nbykov0
644d71eef7 [system:monitoring-agents] rename coredns metrics service
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-01 17:50:02 +03:00
Kingdon B
8e351f1827 Upgrade Flux to 2.7 series
Flux 2.7.3 is currently the latest release, (I think Flux 2.7.4 is still
just around the corner.)

Signed-off-by: Kingdon B <kingdon@urmanac.com>
2025-11-23 19:40:08 -05:00
Kingdon B
38a4adfaa3 update flux-instance chart as well (v0.33.0)
Signed-off-by: Kingdon B <kingdon@urmanac.com>
2025-11-23 19:39:57 -05:00
Timofei Larkin
03885f5ae2 Merge branch 'main' into feat/external-kubeovn-build 2025-11-19 07:46:51 +03:00
Timofei Larkin
d26d3e1f40 [kubeovn] Package from external repo
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.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-10-19 16:00:56 +03:00
183 changed files with 14272 additions and 1136 deletions

View File

@@ -2,7 +2,7 @@ name: Automatic Backport
on:
pull_request_target:
types: [closed] # fires when PR is closed (merged)
types: [closed, labeled] # fires when PR is closed (merged) or labeled
concurrency:
group: backport-${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -13,22 +13,46 @@ permissions:
pull-requests: write
jobs:
backport:
# Determine which backports are needed
prepare:
if: |
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'backport')
(
contains(github.event.pull_request.labels.*.name, 'backport') ||
contains(github.event.pull_request.labels.*.name, 'backport-previous') ||
(github.event.action == 'labeled' && (github.event.label.name == 'backport' || github.event.label.name == 'backport-previous'))
)
runs-on: [self-hosted]
outputs:
backport_current: ${{ steps.labels.outputs.backport }}
backport_previous: ${{ steps.labels.outputs.backport_previous }}
current_branch: ${{ steps.branches.outputs.current_branch }}
previous_branch: ${{ steps.branches.outputs.previous_branch }}
steps:
# 1. Decide which maintenance branch should receive the backport
- name: Determine target maintenance branch
id: target
- name: Check which labels are present
id: labels
uses: actions/github-script@v7
with:
script: |
let rel;
const pr = context.payload.pull_request;
const labels = pr.labels.map(l => l.name);
const isBackport = labels.includes('backport');
const isBackportPrevious = labels.includes('backport-previous');
core.setOutput('backport', isBackport ? 'true' : 'false');
core.setOutput('backport_previous', isBackportPrevious ? 'true' : 'false');
console.log(`backport label: ${isBackport}, backport-previous label: ${isBackportPrevious}`);
- name: Determine target branches
id: branches
uses: actions/github-script@v7
with:
script: |
// Get latest release
let latestRelease;
try {
rel = await github.rest.repos.getLatestRelease({
latestRelease = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo
});
@@ -36,18 +60,70 @@ jobs:
core.setFailed('No existing releases found; cannot determine backport target.');
return;
}
const [maj, min] = rel.data.tag_name.replace(/^v/, '').split('.');
const branch = `release-${maj}.${min}`;
core.setOutput('branch', branch);
console.log(`Latest release ${rel.data.tag_name}; backporting to ${branch}`);
const [maj, min] = latestRelease.data.tag_name.replace(/^v/, '').split('.');
const currentBranch = `release-${maj}.${min}`;
const prevMin = parseInt(min) - 1;
const previousBranch = prevMin >= 0 ? `release-${maj}.${prevMin}` : '';
core.setOutput('current_branch', currentBranch);
core.setOutput('previous_branch', previousBranch);
console.log(`Current branch: ${currentBranch}, Previous branch: ${previousBranch || 'N/A'}`);
// Verify previous branch exists if we need it
if (previousBranch && '${{ steps.labels.outputs.backport_previous }}' === 'true') {
try {
await github.rest.repos.getBranch({
owner: context.repo.owner,
repo: context.repo.repo,
branch: previousBranch
});
console.log(`Previous branch ${previousBranch} exists`);
} catch (e) {
core.setFailed(`Previous branch ${previousBranch} does not exist.`);
return;
}
}
backport:
needs: prepare
if: |
github.event.pull_request.merged == true &&
(needs.prepare.outputs.backport_current == 'true' || needs.prepare.outputs.backport_previous == 'true')
runs-on: [self-hosted]
strategy:
matrix:
backport_type: [current, previous]
steps:
# 1. Determine target branch based on matrix
- name: Set target branch
id: target
if: |
(matrix.backport_type == 'current' && needs.prepare.outputs.backport_current == 'true') ||
(matrix.backport_type == 'previous' && needs.prepare.outputs.backport_previous == 'true')
run: |
if [ "${{ matrix.backport_type }}" == "current" ]; then
echo "branch=${{ needs.prepare.outputs.current_branch }}" >> $GITHUB_OUTPUT
echo "Target branch: ${{ needs.prepare.outputs.current_branch }}"
else
echo "branch=${{ needs.prepare.outputs.previous_branch }}" >> $GITHUB_OUTPUT
echo "Target branch: ${{ needs.prepare.outputs.previous_branch }}"
fi
# 2. Checkout (required by backportaction)
- name: Checkout repository
if: steps.target.outcome == 'success'
uses: actions/checkout@v4
# 3. Create the backport pull request
- name: Create backport PR
uses: korthout/backport-action@v3
id: backport
if: steps.target.outcome == 'success'
uses: korthout/backport-action@v3.2.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
label_pattern: '' # don't read labels for targets
target_branches: ${{ steps.target.outputs.branch }}
merge_commits: skip
conflict_resolution: draft_commit_conflicts

View File

@@ -28,7 +28,7 @@ jobs:
- name: Install generate
run: |
curl -sSL https://github.com/cozystack/cozyvalues-gen/releases/download/v1.0.5/cozyvalues-gen-linux-amd64.tar.gz | tar -xzvf- -C /usr/local/bin/ cozyvalues-gen
curl -sSL https://github.com/cozystack/cozyvalues-gen/releases/download/v1.0.6/cozyvalues-gen-linux-amd64.tar.gz | tar -xzvf- -C /usr/local/bin/ cozyvalues-gen
- name: Run pre-commit hooks
run: |

View File

@@ -15,9 +15,9 @@ 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/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,123 @@
//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
}

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,102 @@
// 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"
)
// 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"`
}
// +kubebuilder:object:root=true
// 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,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"
)
// 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"`
}
// +kubebuilder:object:root=true
// 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,53 @@
/*
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"
)
var (
// GroupVersion is group version used to register these objects.
GroupVersion = schema.GroupVersion{Group: "backups.cozystack.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(
GroupVersion,
&Plan{},
&PlanList{},
&BackupJob{},
&BackupJobList{},
&Backup{},
&BackupList{},
&RestoreJob{},
&RestoreJobList{},
)
metav1.AddToGroupVersion(scheme, GroupVersion)
return nil
}

View File

@@ -0,0 +1,82 @@
// 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"
)
type PlanScheduleType string
const (
PlanScheduleTypeEmpty PlanScheduleType = ""
PlanScheduleTypeCron PlanScheduleType = "cron"
)
// Condtions
const (
PlanConditionError = "Error"
)
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// 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,80 @@
// 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"
)
// 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
// 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,478 @@
//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)
}
// 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 *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,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: "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.BackupJobStrategyReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Job")
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)
}
}

9
go.mod
View File

@@ -6,11 +6,16 @@ go 1.23.0
require (
github.com/fluxcd/helm-controller/api v1.1.0
github.com/go-logr/logr v1.4.2
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/prometheus/client_golang v1.19.1
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
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
@@ -44,9 +49,7 @@ require (
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/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
@@ -74,7 +77,6 @@ require (
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/procfs v0.15.1 // indirect
@@ -94,7 +96,6 @@ require (
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // 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
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.33.0 // indirect

2
go.sum
View File

@@ -145,6 +145,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
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/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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

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,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,184 @@
package backupcontroller
import (
"context"
"fmt"
"strings"
batchv1 "k8s.io/api/batch/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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
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"
"github.com/cozystack/cozystack/internal/template"
appscozystackio "github.com/cozystack/cozystack/pkg/apis/apps"
)
// BackupJobStrategyReconciler reconciles BackupJob with a strategy referencing
// Job.strategy.backups.cozystack.io objects.
type BackupJobStrategyReconciler struct {
client.Client
Scheme *runtime.Scheme
dynClient dynamic.Interface
mapper meta.RESTMapper
}
func (r *BackupJobStrategyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
log.V(2).Info("reconciling")
j := &backupsv1alpha1.BackupJob{}
if err := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, j); err != nil {
if apierrors.IsNotFound(err) {
log.V(3).Info("BackupJob not found")
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
var applicationRefAPIGroup string
var strategyRefAPIGroup string
var storageRefAPIGroup string
if j.Spec.ApplicationRef.APIGroup != nil {
applicationRefAPIGroup = *j.Spec.ApplicationRef.APIGroup
}
if j.Spec.StrategyRef.APIGroup != nil {
strategyRefAPIGroup = *j.Spec.StrategyRef.APIGroup
}
if j.Spec.StorageRef.APIGroup != nil {
storageRefAPIGroup = *j.Spec.StorageRef.APIGroup
}
if strategyRefAPIGroup != strategyv1alpha1.GroupVersion.Group {
return ctrl.Result{}, nil
}
if j.Spec.StrategyRef.Kind != strategyv1alpha1.JobStrategyKind {
return ctrl.Result{}, nil
}
app, err := r.getUnstructured(ctx, applicationRefAPIGroup, j.Spec.ApplicationRef.Kind, j.Namespace, j.Spec.ApplicationRef.Name)
if err != nil {
// TODO: we should handle not-found errors separately, but it's not
// clear, how to trigger a reconcile if the application is created
// later, so we just rely on the default exponential backoff.
return ctrl.Result{}, err
}
strategy := &strategyv1alpha1.Job{}
err = r.Get(ctx, types.NamespacedName{Name: j.Spec.StrategyRef.Name}, strategy)
if err != nil {
// TODO: as with the app, not-found errors for strategies are pointless
// to retry, but a reconcile should be triggered if a strategy is later
// created.
return ctrl.Result{}, err
}
// TODO: we should use the storage in a more generic way, but since the
// storage part of the backups API is not implemented at all, we skip this
// for now and revert to a default implementation: only Bucket is supported
if storageRefAPIGroup != appscozystackio.GroupName {
return ctrl.Result{}, nil
}
if j.Spec.StorageRef.Kind != "Bucket" {
return ctrl.Result{}, nil
}
_, err = r.getUnstructured(ctx, storageRefAPIGroup, j.Spec.StorageRef.Kind, j.Namespace, j.Spec.StorageRef.Name)
if err != nil {
// TODO: same not-found caveat as before
return ctrl.Result{}, err
}
values, ok := app.Object["spec"].(map[string]any)
if !ok {
values = map[string]any{}
}
release := map[string]any{
"Name": fmt.Sprintf("%s-%s", strings.ToLower(j.Spec.ApplicationRef.Kind), j.Spec.ApplicationRef.Name),
"Namespace": j.Namespace,
}
templateContext := map[string]any{
"Release": release,
"Values": values,
"Storage": map[string]any{
"APIGroup": storageRefAPIGroup,
"Kind": j.Spec.StorageRef.Kind,
"Name": fmt.Sprintf("%s-%s", strings.ToLower(j.Spec.StorageRef.Kind), j.Spec.StorageRef.Name),
},
}
podTemplate, err := template.Template(&strategy.Spec.Template, templateContext)
if err != nil {
return ctrl.Result{}, err
}
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: j.Name,
Namespace: j.Namespace,
},
Spec: batchv1.JobSpec{
Template: *podTemplate,
},
}
if err := r.Create(ctx, job); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
// SetupWithManager registers our controller with the Manager and sets up watches.
func (r *BackupJobStrategyReconciler) SetupWithManager(mgr ctrl.Manager) error {
cfg := rest.CopyConfig(mgr.GetConfig())
var err error
r.dynClient, err = dynamic.NewForConfig(cfg)
if err != nil {
return err
}
httpClient, err := rest.HTTPClientFor(cfg)
if err != nil {
return err
}
r.mapper, err = apiutil.NewDynamicRESTMapper(cfg, httpClient)
if err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&backupsv1alpha1.BackupJob{}).
Complete(r)
}
func (r *BackupJobStrategyReconciler) getUnstructured(ctx context.Context, apiGroup, kind, namespace, name string) (*unstructured.Unstructured, error) {
mapping, err := r.mapper.RESTMapping(schema.GroupKind{Group: apiGroup, Kind: kind})
if err != nil {
return nil, err
}
ns := namespace
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
ns = ""
}
obj, err := r.dynClient.Resource(mapping.Resource).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, err
}
return obj, 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

@@ -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

@@ -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,3 +1,4 @@
.helmignore
/logos
/Makefile
/hack

View File

@@ -6,9 +6,12 @@ include ../../../scripts/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
yq -o=json -i '.properties.version.enum = (load("files/versions.yaml") | keys)' values.schema.json
../../../hack/update-crd.sh
update:
hack/update-versions.sh
make generate
image: image-ubuntu-container-disk image-kubevirt-cloud-provider image-kubevirt-csi-driver image-cluster-autoscaler
image-ubuntu-container-disk:

View File

@@ -104,7 +104,7 @@ See the reference for components utilized in this service:
| `nodeGroups[name].resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `nodeGroups[name].gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` |
| `nodeGroups[name].gpus[i].name` | Name of GPU, such as "nvidia.com/AD102GL_L40S". | `string` | `""` |
| `version` | Kubernetes version (vMAJOR.MINOR). Supported: 1.281.33. | `string` | `v1.33` |
| `version` | Kubernetes major.minor version to deploy | `string` | `v1.33` |
| `host` | External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty. | `string` | `""` |

View File

@@ -1,6 +1,6 @@
"v1.28": "v1.28.15"
"v1.29": "v1.29.15"
"v1.30": "v1.30.14"
"v1.31": "v1.31.10"
"v1.32": "v1.32.6"
"v1.33": "v1.33.0"
"v1.32": "v1.32.10"
"v1.31": "v1.31.14"
"v1.30": "v1.30.14"
"v1.29": "v1.29.15"
"v1.28": "v1.28.15"

View File

@@ -0,0 +1,260 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
KUBERNETES_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
VALUES_FILE="${KUBERNETES_DIR}/values.yaml"
VERSIONS_FILE="${KUBERNETES_DIR}/files/versions.yaml"
MAKEFILE="${KUBERNETES_DIR}/Makefile"
KAMAJI_DOCKERFILE="${KUBERNETES_DIR}/../../system/kamaji/images/kamaji/Dockerfile"
# Check if skopeo is installed
if ! command -v skopeo &> /dev/null; then
echo "Error: skopeo is not installed. Please install skopeo and try again." >&2
exit 1
fi
# Check if jq is installed
if ! command -v jq &> /dev/null; then
echo "Error: jq is not installed. Please install jq and try again." >&2
exit 1
fi
# Get kamaji version from Dockerfile
echo "Reading kamaji version from Dockerfile..."
if [ ! -f "$KAMAJI_DOCKERFILE" ]; then
echo "Error: Kamaji Dockerfile not found at $KAMAJI_DOCKERFILE" >&2
exit 1
fi
KAMAJI_VERSION=$(grep "^ARG VERSION=" "$KAMAJI_DOCKERFILE" | cut -d= -f2 | tr -d '"')
if [ -z "$KAMAJI_VERSION" ]; then
echo "Error: Could not extract kamaji version from Dockerfile" >&2
exit 1
fi
echo "Kamaji version: $KAMAJI_VERSION"
# Get Kubernetes version from kamaji repository
echo "Fetching Kubernetes version from kamaji repository..."
KUBERNETES_VERSION_FROM_KAMAJI=$(curl -sSL "https://raw.githubusercontent.com/clastix/kamaji/${KAMAJI_VERSION}/internal/upgrade/kubeadm_version.go" | grep "KubeadmVersion" | sed -E 's/.*KubeadmVersion = "([^"]+)".*/\1/')
if [ -z "$KUBERNETES_VERSION_FROM_KAMAJI" ]; then
echo "Error: Could not fetch Kubernetes version from kamaji repository" >&2
exit 1
fi
echo "Kubernetes version from kamaji: $KUBERNETES_VERSION_FROM_KAMAJI"
# Extract major.minor version (e.g., "1.33" from "v1.33.0")
KUBERNETES_MAJOR_MINOR=$(echo "$KUBERNETES_VERSION_FROM_KAMAJI" | sed -E 's/v([0-9]+)\.([0-9]+)\.[0-9]+/\1.\2/')
KUBERNETES_MAJOR=$(echo "$KUBERNETES_MAJOR_MINOR" | cut -d. -f1)
KUBERNETES_MINOR=$(echo "$KUBERNETES_MAJOR_MINOR" | cut -d. -f2)
echo "Kubernetes major.minor: $KUBERNETES_MAJOR_MINOR"
# Get available image tags
echo "Fetching available image tags from registry..."
AVAILABLE_TAGS=$(skopeo list-tags docker://registry.k8s.io/kube-apiserver | jq -r '.Tags[] | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))' | sort -V)
if [ -z "$AVAILABLE_TAGS" ]; then
echo "Error: Could not fetch available image tags" >&2
exit 1
fi
# Filter out versions higher than KUBERNETES_VERSION_FROM_KAMAJI
echo "Filtering versions above ${KUBERNETES_VERSION_FROM_KAMAJI}..."
FILTERED_TAGS=$(echo "$AVAILABLE_TAGS" | while read tag; do
if [ -n "$tag" ]; then
# Compare tag with KUBERNETES_VERSION_FROM_KAMAJI using version sort
# Include tag if it's less than or equal to KUBERNETES_VERSION_FROM_KAMAJI
if [ "$(printf '%s\n%s\n' "$tag" "$KUBERNETES_VERSION_FROM_KAMAJI" | sort -V | head -1)" = "$tag" ] || [ "$tag" = "$KUBERNETES_VERSION_FROM_KAMAJI" ]; then
echo "$tag"
fi
fi
done)
if [ -z "$FILTERED_TAGS" ]; then
echo "Error: No versions found after filtering" >&2
exit 1
fi
AVAILABLE_TAGS="$FILTERED_TAGS"
echo "Filtered to $(echo "$AVAILABLE_TAGS" | wc -l | tr -d ' ') versions"
# Find the latest patch version for the supported major.minor version
echo "Finding latest patch version for ${KUBERNETES_MAJOR_MINOR}..."
SUPPORTED_PATCH_TAGS=$(echo "$AVAILABLE_TAGS" | grep "^v${KUBERNETES_MAJOR}\\.${KUBERNETES_MINOR}\\.")
if [ -z "$SUPPORTED_PATCH_TAGS" ]; then
echo "Error: Could not find any patch versions for ${KUBERNETES_MAJOR_MINOR}" >&2
exit 1
fi
KUBERNETES_VERSION=$(echo "$SUPPORTED_PATCH_TAGS" | tail -n1)
echo "Using latest patch version: $KUBERNETES_VERSION"
# Build versions map: major.minor -> latest patch version
# First, collect all unique major.minor versions from available tags
echo "Collecting all available major.minor versions..."
ALL_MAJOR_MINOR_VERSIONS=$(echo "$AVAILABLE_TAGS" | sed -E 's/v([0-9]+)\.([0-9]+)\.[0-9]+/v\1.\2/' | sort -V -u)
# Find the position of the supported version in the sorted list
SUPPORTED_MAJOR_MINOR="v${KUBERNETES_MAJOR}.${KUBERNETES_MINOR}"
echo "Looking for supported version: $SUPPORTED_MAJOR_MINOR"
# Get all versions that are <= supported version
# Create a temporary file for filtering
TEMP_VERSIONS=$(mktemp)
echo "$ALL_MAJOR_MINOR_VERSIONS" | while read version; do
# Compare versions using sort -V (version sort)
# If version <= supported, include it
if [ "$(printf '%s\n%s\n' "$version" "$SUPPORTED_MAJOR_MINOR" | sort -V | head -1)" = "$version" ] || [ "$version" = "$SUPPORTED_MAJOR_MINOR" ]; then
echo "$version"
fi
done > "$TEMP_VERSIONS"
# Get the supported version and 5 previous versions (total 6 versions)
# First, find the position of supported version
SUPPORTED_POS=$(grep -n "^${SUPPORTED_MAJOR_MINOR}$" "$TEMP_VERSIONS" | cut -d: -f1)
if [ -z "$SUPPORTED_POS" ]; then
echo "Error: Supported version $SUPPORTED_MAJOR_MINOR not found in available versions" >&2
exit 1
fi
# Calculate start position (5 versions before supported, or from beginning if less than 5 available)
TOTAL_LINES=$(wc -l < "$TEMP_VERSIONS" | tr -d ' ')
START_POS=$((SUPPORTED_POS - 5))
if [ $START_POS -lt 1 ]; then
START_POS=1
fi
# Extract versions from START_POS to SUPPORTED_POS (inclusive)
CANDIDATE_VERSIONS=$(sed -n "${START_POS},${SUPPORTED_POS}p" "$TEMP_VERSIONS")
if [ -z "$CANDIDATE_VERSIONS" ]; then
echo "Error: Could not find supported version $SUPPORTED_MAJOR_MINOR in available versions" >&2
exit 1
fi
declare -A VERSION_MAP
VERSIONS=()
# Process each candidate version
for major_minor_key in $CANDIDATE_VERSIONS; do
# Extract major and minor for matching
major=$(echo "$major_minor_key" | sed -E 's/v([0-9]+)\.([0-9]+)/\1/')
minor=$(echo "$major_minor_key" | sed -E 's/v([0-9]+)\.([0-9]+)/\2/')
# Find all tags that match this major.minor version
matching_tags=$(echo "$AVAILABLE_TAGS" | grep "^v${major}\\.${minor}\\.")
if [ -n "$matching_tags" ]; then
# Get the latest patch version for this major.minor version
latest_tag=$(echo "$matching_tags" | tail -n1)
VERSION_MAP["${major_minor_key}"]="${latest_tag}"
VERSIONS+=("${major_minor_key}")
echo "Found version: ${major_minor_key} -> ${latest_tag}"
fi
done
if [ ${#VERSIONS[@]} -eq 0 ]; then
echo "Error: No matching versions found" >&2
exit 1
fi
# Sort versions in descending order (newest first)
IFS=$'\n' VERSIONS=($(printf '%s\n' "${VERSIONS[@]}" | sort -V -r))
unset IFS
echo "Versions to add: ${VERSIONS[*]}"
# Create/update versions.yaml file
echo "Updating $VERSIONS_FILE..."
{
for ver in "${VERSIONS[@]}"; do
echo "\"${ver}\": \"${VERSION_MAP[$ver]}\""
done
} > "$VERSIONS_FILE"
echo "Successfully updated $VERSIONS_FILE"
# Update values.yaml - enum with major.minor versions only
TEMP_FILE=$(mktemp)
trap "rm -f $TEMP_FILE $TEMP_VERSIONS" EXIT
# Build new version section
NEW_VERSION_SECTION="## @enum {string} Version"
for ver in "${VERSIONS[@]}"; do
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @value $ver"
done
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @param {Version} version - Kubernetes major.minor version to deploy
version: \"${VERSIONS[0]}\""
# Check if version section already exists
if grep -q "^## @enum {string} Version" "$VALUES_FILE"; then
# Version section exists, update it using awk
echo "Updating existing version section in $VALUES_FILE..."
# Use awk to replace the section from "## @enum {string} Version" to "version: " (inclusive)
# Delete the old section and insert the new one
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @enum {string} Version/ {
in_section = 1
print new_section
next
}
in_section && /^version: / {
in_section = 0
next
}
in_section {
next
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
else
# Version section doesn't exist, insert it before Application-specific parameters section
echo "Inserting new version section in $VALUES_FILE..."
# Use awk to insert before "## @section Application-specific parameters"
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @section Application-specific parameters/ {
print new_section
print ""
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
fi
echo "Successfully updated $VALUES_FILE with versions: ${VERSIONS[*]}"
# Update KUBERNETES_VERSION in Makefile
# Extract major.minor from KUBERNETES_VERSION (e.g., "v1.33" from "v1.33.4")
KUBERNETES_MAJOR_MINOR_FOR_MAKEFILE=$(echo "$KUBERNETES_VERSION" | sed -E 's/v([0-9]+)\.([0-9]+)\.[0-9]+/v\1.\2/')
if grep -q "^KUBERNETES_VERSION" "$MAKEFILE"; then
# Update existing KUBERNETES_VERSION line using awk
echo "Updating KUBERNETES_VERSION in $MAKEFILE..."
awk -v new_version="${KUBERNETES_MAJOR_MINOR_FOR_MAKEFILE}" '
/^KUBERNETES_VERSION = / {
print "KUBERNETES_VERSION = " new_version
next
}
{ print }
' "$MAKEFILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$MAKEFILE"
echo "Successfully updated KUBERNETES_VERSION in $MAKEFILE to ${KUBERNETES_MAJOR_MINOR_FOR_MAKEFILE}"
else
echo "Warning: KUBERNETES_VERSION not found in $MAKEFILE" >&2
fi

View File

@@ -5,8 +5,8 @@ metadata:
annotations:
"helm.sh/hook": pre-delete
"helm.sh/hook-weight": "10"
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed
name: {{ .Release.Name }}-cleanup
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
name: {{ .Release.Name }}-pre-cleanup
spec:
template:
metadata:
@@ -65,7 +65,6 @@ spec:
echo "Cleanup completed successfully"
---
apiVersion: v1
kind: ServiceAccount
@@ -73,7 +72,7 @@ metadata:
name: {{ .Release.Name }}-cleanup
annotations:
helm.sh/hook: pre-delete
helm.sh/hook-delete-policy: before-hook-creation,hook-failed,hook-succeeded
helm.sh/hook-delete-policy: hook-succeeded,before-hook-creation,hook-failed
helm.sh/hook-weight: "0"
---
apiVersion: rbac.authorization.k8s.io/v1

View File

@@ -45,8 +45,6 @@ spec:
- name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
{{- end }}
- name: {{ .Release.Name }}-cilium
namespace: {{ .Release.Namespace }}
{{- if .Values.addons.certManager.valuesOverride }}
---
apiVersion: v1

View File

@@ -37,6 +37,8 @@ spec:
dependsOn:
- name: {{ .Release.Name }}-vsnap-crd
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-cilium
namespace: {{ .Release.Namespace }}
{{- if lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" .Release.Namespace .Release.Name }}
- name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}

View File

@@ -0,0 +1,42 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: {{ .Release.Name }}-metrics-server
labels:
cozystack.io/repository: system
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: metrics-server
chart:
spec:
chart: cozy-metrics-server
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig
key: super-admin.svc
targetNamespace: cozy-monitoring
storageNamespace: cozy-monitoring
interval: 5m
timeout: 10m
install:
createNamespace: true
remediation:
retries: -1
upgrade:
remediation:
retries: -1
dependsOn:
{{- if lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" .Release.Namespace .Release.Name }}
- name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
{{- end }}
- name: {{ .Release.Name }}-cilium
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-prometheus-operator-crds
namespace: {{ .Release.Namespace }}

View File

@@ -44,6 +44,10 @@ spec:
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-vertical-pod-autoscaler-crds
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-prometheus-operator-crds
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-metrics-server
namespace: {{ .Release.Namespace }}
values:
vmagent:
externalLabels:

View File

@@ -0,0 +1,37 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: {{ .Release.Name }}-prometheus-operator-crds
labels:
cozystack.io/repository: system
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: prometheus-operator-crds
chart:
spec:
chart: cozy-prometheus-operator-crds
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig
key: super-admin.svc
targetNamespace: cozy-victoria-metrics-operator
storageNamespace: cozy-victoria-metrics-operator
interval: 5m
install:
createNamespace: true
remediation:
retries: -1
upgrade:
remediation:
retries: -1
dependsOn:
{{- if lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" .Release.Namespace .Release.Name }}
- name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
{{- end }}

View File

@@ -37,6 +37,4 @@ spec:
- name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
{{- end }}
- name: {{ .Release.Name }}-cilium
namespace: {{ .Release.Namespace }}
{{- end }}

View File

@@ -37,5 +37,3 @@ spec:
- name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
{{- end }}
- name: {{ .Release.Name }}-cilium
namespace: {{ .Release.Namespace }}

View File

@@ -492,7 +492,7 @@
}
},
"host": {
"description": "External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty.",
"description": "External hostname for Kubernetes cluster. Defaults to `\u003ccluster-name\u003e.\u003ctenant-host\u003e` if empty.",
"type": "string",
"default": ""
},
@@ -615,17 +615,17 @@
"default": "replicated"
},
"version": {
"description": "Kubernetes version (vMAJOR.MINOR). Supported: 1.281.33.",
"description": "Kubernetes major.minor version to deploy",
"type": "string",
"default": "v1.33",
"enum": [
"v1.28",
"v1.29",
"v1.30",
"v1.31",
"v1.33",
"v1.32",
"v1.33"
"v1.31",
"v1.30",
"v1.29",
"v1.28"
]
}
}
}
}

View File

@@ -46,9 +46,19 @@ nodeGroups:
resources: {}
gpus: []
## @param {string} version - Kubernetes version (vMAJOR.MINOR). Supported: 1.281.33.
##
## @enum {string} Version
## @value v1.33
## @value v1.32
## @value v1.31
## @value v1.30
## @value v1.29
## @value v1.28
## @param {Version} version - Kubernetes major.minor version to deploy
version: "v1.33"
## @param {string} host - External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty.
host: ""

View File

@@ -1,3 +1,4 @@
.helmignore
/logos
/Makefile
/hack

View File

@@ -7,6 +7,10 @@ generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
../../../hack/update-crd.sh
update:
hack/update-versions.sh
make generate
image:
docker buildx build images/mariadb-backup \
--tag $(REGISTRY)/mariadb-backup:$(call settag,$(MARIADB_BACKUP_TAG)) \

View File

@@ -79,6 +79,7 @@ more details:
| `size` | Persistent Volume Claim size available for application data. | `quantity` | `10Gi` |
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `version` | MariaDB major.minor version to deploy | `string` | `v11.8` |
### Application-specific parameters

View File

@@ -0,0 +1,4 @@
"v11.8": "11.8.5"
"v11.4": "11.4.9"
"v10.11": "10.11.15"
"v10.6": "10.6.24"

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MYSQL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
VALUES_FILE="${MYSQL_DIR}/values.yaml"
VERSIONS_FILE="${MYSQL_DIR}/files/versions.yaml"
MARIADB_API_URL="https://downloads.mariadb.org/rest-api/mariadb/"
# Check if jq is installed
if ! command -v jq &> /dev/null; then
echo "Error: jq is not installed. Please install jq and try again." >&2
exit 1
fi
# Get LTS versions from MariaDB REST API
echo "Fetching LTS versions from MariaDB REST API..."
LTS_VERSIONS_JSON=$(curl -sSL "${MARIADB_API_URL}")
if [ -z "$LTS_VERSIONS_JSON" ]; then
echo "Error: Could not fetch versions from MariaDB REST API" >&2
exit 1
fi
# Extract LTS stable major versions
LTS_MAJOR_VERSIONS=$(echo "$LTS_VERSIONS_JSON" | jq -r '.major_releases[] | select(.release_support_type == "Long Term Support") | select(.release_status == "Stable") | .release_id' | sort -V -r)
if [ -z "$LTS_MAJOR_VERSIONS" ]; then
echo "Error: Could not find any LTS stable versions" >&2
exit 1
fi
echo "Found LTS major versions: $(echo "$LTS_MAJOR_VERSIONS" | tr '\n' ' ')"
# Build versions map: major version -> latest patch version
declare -A VERSION_MAP
MAJOR_VERSIONS=()
for major_version in $LTS_MAJOR_VERSIONS; do
echo "Fetching patch versions for ${major_version}..."
# Get patch versions for this major version
PATCH_VERSIONS_JSON=$(curl -sSL "${MARIADB_API_URL}${major_version}")
if [ -z "$PATCH_VERSIONS_JSON" ]; then
echo "Warning: Could not fetch patch versions for ${major_version}, skipping..." >&2
continue
fi
# Extract all stable patch version IDs (format: MAJOR.MINOR.PATCH)
# Filter only Stable releases
PATCH_VERSIONS=$(echo "$PATCH_VERSIONS_JSON" | jq -r --arg major "$major_version" '.releases | to_entries[] | select(.key | startswith($major + ".")) | select(.value.release_status == "Stable") | .key' | sort -V)
# If no stable releases found, try to get any releases (for backwards compatibility)
if [ -z "$PATCH_VERSIONS" ]; then
PATCH_VERSIONS=$(echo "$PATCH_VERSIONS_JSON" | jq -r '.releases | keys[]' | grep -E "^${major_version}\." | sort -V)
fi
if [ -z "$PATCH_VERSIONS" ]; then
echo "Warning: Could not find any patch versions for ${major_version}, skipping..." >&2
continue
fi
# Get the latest patch version
LATEST_PATCH=$(echo "$PATCH_VERSIONS" | tail -n1)
# major_version already has format MAJOR.MINOR (e.g., "11.8")
VERSION_MAP["v${major_version}"]="${LATEST_PATCH}"
MAJOR_VERSIONS+=("v${major_version}")
echo "Found version: v${major_version} -> ${LATEST_PATCH}"
done
if [ ${#MAJOR_VERSIONS[@]} -eq 0 ]; then
echo "Error: No matching versions found" >&2
exit 1
fi
# Sort major versions in descending order (newest first)
IFS=$'\n' MAJOR_VERSIONS=($(printf '%s\n' "${MAJOR_VERSIONS[@]}" | sort -V -r))
unset IFS
echo "Major versions to add: ${MAJOR_VERSIONS[*]}"
# Create/update versions.yaml file
echo "Updating $VERSIONS_FILE..."
{
for major_ver in "${MAJOR_VERSIONS[@]}"; do
echo "\"${major_ver}\": \"${VERSION_MAP[$major_ver]}\""
done
} > "$VERSIONS_FILE"
echo "Successfully updated $VERSIONS_FILE"
# Update values.yaml - enum with major.minor versions only
TEMP_FILE=$(mktemp)
trap "rm -f $TEMP_FILE" EXIT
# Build new version section
NEW_VERSION_SECTION="## @enum {string} Version"
for major_ver in "${MAJOR_VERSIONS[@]}"; do
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @value $major_ver"
done
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @param {Version} version - MariaDB major.minor version to deploy
version: ${MAJOR_VERSIONS[0]}"
# Check if version section already exists
if grep -q "^## @enum {string} Version" "$VALUES_FILE"; then
# Version section exists, update it using awk
echo "Updating existing version section in $VALUES_FILE..."
# Use awk to replace the section from "## @enum {string} Version" to "version: " (inclusive)
# Delete the old section and insert the new one
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @enum {string} Version/ {
in_section = 1
print new_section
next
}
in_section && /^version: / {
in_section = 0
next
}
in_section {
next
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
else
# Version section doesn't exist, insert it before Application-specific parameters section
echo "Inserting new version section in $VALUES_FILE..."
# Use awk to insert before "## @section Application-specific parameters"
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @section Application-specific parameters/ {
print new_section
print ""
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
fi
echo "Successfully updated $VALUES_FILE with major.minor versions: ${MAJOR_VERSIONS[*]}"

View File

@@ -0,0 +1,8 @@
{{- define "mysql.versionMap" }}
{{- $versionMap := .Files.Get "files/versions.yaml" | fromYaml }}
{{- if not (hasKey $versionMap .Values.version) }}
{{- printf `MariaDB version %s is not supported, allowed versions are %s` $.Values.version (keys $versionMap) | fail }}
{{- end }}
{{- index $versionMap .Values.version }}
{{- end }}

View File

@@ -8,7 +8,7 @@ spec:
name: {{ .Release.Name }}-credentials
key: root
image: "mariadb:11.0.2"
image: "mariadb:{{ include "mysql.versionMap" $ }}"
port: 3306

View File

@@ -186,6 +186,17 @@
}
}
}
},
"version": {
"description": "MariaDB major.minor version to deploy",
"type": "string",
"default": "v11.8",
"enum": [
"v11.8",
"v11.4",
"v10.11",
"v10.6"
]
}
}
}

View File

@@ -33,6 +33,16 @@ storageClass: ""
## @param {bool} external - Enable external access from outside the cluster.
external: false
##
## @enum {string} Version
## @value v11.8
## @value v11.4
## @value v10.11
## @value v10.6
## @param {Version} version - MariaDB major.minor version to deploy
version: v11.8
##
## @section Application-specific parameters
##

View File

@@ -1,3 +1,4 @@
.helmignore
/logos
/Makefile
/hack

View File

@@ -3,3 +3,7 @@ include ../../../scripts/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
../../../hack/update-crd.sh
update:
hack/update-versions.sh
make generate

View File

@@ -76,6 +76,7 @@ See:
| `size` | Persistent Volume Claim size available for application data. | `quantity` | `10Gi` |
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `version` | PostgreSQL major version to deploy | `string` | `v18` |
### Application-specific parameters

View File

@@ -0,0 +1,6 @@
"v18": "v18.1"
"v17": "v17.7"
"v16": "v16.11"
"v15": "v15.15"
"v14": "v14.20"
"v13": "v13.22"

View File

@@ -0,0 +1,138 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
POSTGRES_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
VALUES_FILE="${POSTGRES_DIR}/values.yaml"
VERSIONS_FILE="${POSTGRES_DIR}/files/versions.yaml"
# Get supported major versions from GitHub README
echo "Fetching supported major versions from GitHub..."
SUPPORTED_MAJOR_VERSIONS=$(curl -sSL 'https://raw.githubusercontent.com/cloudnative-pg/postgres-containers/refs/heads/main/README.md' | sed -n '/# CNPG PostgreSQL Container Images/,/#/p' | awk -F' +| +' '$4 ~ /[0-9]+\-[0-9]+\-[0-9]+/ && $6 ~ /[0-9]+\-[0-9]+\-[0-9]+/ {print $2}' | sort -u | xargs)
if [ -z "$SUPPORTED_MAJOR_VERSIONS" ]; then
echo "Error: Could not fetch supported major versions" >&2
exit 1
fi
echo "Supported major versions: $SUPPORTED_MAJOR_VERSIONS"
# Check if skopeo is installed
if ! command -v skopeo &> /dev/null; then
echo "Error: skopeo is not installed. Please install skopeo and try again." >&2
exit 1
fi
# Check if jq is installed
if ! command -v jq &> /dev/null; then
echo "Error: jq is not installed. Please install jq and try again." >&2
exit 1
fi
# Get available image tags
echo "Fetching available image tags from registry..."
AVAILABLE_TAGS=$(skopeo list-tags docker://ghcr.io/cloudnative-pg/postgresql | jq -r '.Tags[] | select(test("^[0-9]+\\.[0-9]+$"))' | sort -V)
if [ -z "$AVAILABLE_TAGS" ]; then
echo "Error: Could not fetch available image tags" >&2
exit 1
fi
# Build versions map: major version -> latest minor version
declare -A VERSION_MAP
MAJOR_VERSIONS=()
for major_version in $SUPPORTED_MAJOR_VERSIONS; do
# Extract major version number (e.g., "18" from "18.1")
major_num=$(echo "$major_version" | cut -d. -f1)
# Find all tags that match this major version
matching_tags=$(echo "$AVAILABLE_TAGS" | grep "^${major_num}\\.")
if [ -n "$matching_tags" ]; then
# Get the latest minor version for this major version
latest_tag=$(echo "$matching_tags" | tail -n1)
VERSION_MAP["v${major_num}"]="v${latest_tag}"
MAJOR_VERSIONS+=("v${major_num}")
echo "Found version: v${major_num} -> v${latest_tag}"
fi
done
if [ ${#MAJOR_VERSIONS[@]} -eq 0 ]; then
echo "Error: No matching versions found" >&2
exit 1
fi
# Sort major versions in descending order (newest first)
IFS=$'\n' MAJOR_VERSIONS=($(printf '%s\n' "${MAJOR_VERSIONS[@]}" | sort -V -r))
unset IFS
echo "Major versions to add: ${MAJOR_VERSIONS[*]}"
# Create/update versions.yaml file
echo "Updating $VERSIONS_FILE..."
{
for major_ver in "${MAJOR_VERSIONS[@]}"; do
echo "\"${major_ver}\": \"${VERSION_MAP[$major_ver]}\""
done
} > "$VERSIONS_FILE"
echo "Successfully updated $VERSIONS_FILE"
# Update values.yaml - enum with major versions only
TEMP_FILE=$(mktemp)
trap "rm -f $TEMP_FILE" EXIT
# Build new version section
NEW_VERSION_SECTION="## @enum {string} Version"
for major_ver in "${MAJOR_VERSIONS[@]}"; do
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @value $major_ver"
done
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @param {Version} version - PostgreSQL major version to deploy
version: ${MAJOR_VERSIONS[0]}"
# Check if version section already exists
if grep -q "^## @enum {string} Version" "$VALUES_FILE"; then
# Version section exists, update it using awk
echo "Updating existing version section in $VALUES_FILE..."
# Use awk to replace the section from "## @enum {string} Version" to "version: " (inclusive)
# Delete the old section and insert the new one
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @enum {string} Version/ {
in_section = 1
print new_section
next
}
in_section && /^version: / {
in_section = 0
next
}
in_section {
next
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
else
# Version section doesn't exist, insert it before Application-specific parameters section
echo "Inserting new version section in $VALUES_FILE..."
# Use awk to insert before "## @section Application-specific parameters"
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @section Application-specific parameters/ {
print new_section
print ""
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
fi
echo "Successfully updated $VALUES_FILE with major versions: ${MAJOR_VERSIONS[*]}"

View File

@@ -0,0 +1,8 @@
{{- define "postgres.versionMap" }}
{{- $versionMap := .Files.Get "files/versions.yaml" | fromYaml }}
{{- if not (hasKey $versionMap .Values.version) }}
{{- printf `PostgreSQL version %s is not supported, allowed versions are %s` $.Values.version (keys $versionMap) | fail }}
{{- end }}
{{- index $versionMap .Values.version }}
{{- end }}

View File

@@ -44,6 +44,7 @@ spec:
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 4 }}
imageName: ghcr.io/cloudnative-pg/postgresql:{{ include "postgres.versionMap" $ | trimPrefix "v" }}
enableSuperuserAccess: true
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- if $configMap }}

View File

@@ -16,7 +16,7 @@ spec:
restartPolicy: Never
containers:
- name: postgres
image: ghcr.io/cloudnative-pg/postgresql:15.3
image: ghcr.io/cloudnative-pg/postgresql:{{ include "postgres.versionMap" $ | trimPrefix "v" }}
command:
- bash
- /scripts/init.sh

View File

@@ -243,6 +243,19 @@
}
}
}
},
"version": {
"description": "PostgreSQL major version to deploy",
"type": "string",
"default": "v18",
"enum": [
"v18",
"v17",
"v16",
"v15",
"v14",
"v13"
]
}
}
}

View File

@@ -34,6 +34,17 @@ storageClass: ""
external: false
##
## @enum {string} Version
## @value v18
## @value v17
## @value v16
## @value v15
## @value v14
## @value v13
## @param {Version} version - PostgreSQL major version to deploy
version: v18
## @section Application-specific parameters
##

View File

@@ -1,3 +1,4 @@
.helmignore
/logos
/Makefile
/hack

View File

@@ -3,3 +3,7 @@ include ../../../scripts/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
../../../hack/update-crd.sh
update:
hack/update-versions.sh
make generate

View File

@@ -23,6 +23,7 @@ Service utilizes the Spotahome Redis Operator for efficient management and orche
| `size` | Persistent Volume Claim size available for application data. | `quantity` | `1Gi` |
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `version` | Redis major version to deploy | `string` | `v8` |
### Application-specific parameters

View File

@@ -0,0 +1,2 @@
"v8": "8.4.0"
"v7": "7.4.7"

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REDIS_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
VALUES_FILE="${REDIS_DIR}/values.yaml"
VERSIONS_FILE="${REDIS_DIR}/files/versions.yaml"
REDIS_IMAGE="docker://docker.io/redis"
# Check if skopeo is installed
if ! command -v skopeo &> /dev/null; then
echo "Error: skopeo is not installed. Please install skopeo and try again." >&2
exit 1
fi
# Check if jq is installed
if ! command -v jq &> /dev/null; then
echo "Error: jq is not installed. Please install jq and try again." >&2
exit 1
fi
# Get available image tags
echo "Fetching available image tags from registry..."
AVAILABLE_TAGS=$(skopeo list-tags "${REDIS_IMAGE}" | jq -r '.Tags[] | select(test("^[0-9]+\\.[0-9]+\\.[0-9]+$"))' | sort -V)
if [ -z "$AVAILABLE_TAGS" ]; then
echo "Error: Could not fetch available image tags" >&2
exit 1
fi
# Get all unique major versions and find Latest and Previous
echo "Finding Latest and Previous major versions..."
ALL_MAJOR_VERSIONS=$(echo "$AVAILABLE_TAGS" | cut -d. -f1 | sort -u -n -r)
MAJOR_VERSIONS_ARRAY=($ALL_MAJOR_VERSIONS)
if [ ${#MAJOR_VERSIONS_ARRAY[@]} -lt 1 ]; then
echo "Error: Could not find any major versions" >&2
exit 1
fi
# Get Latest and Previous major versions
LATEST_MAJOR=${MAJOR_VERSIONS_ARRAY[0]}
PREVIOUS_MAJOR=""
if [ ${#MAJOR_VERSIONS_ARRAY[@]} -ge 2 ]; then
PREVIOUS_MAJOR=${MAJOR_VERSIONS_ARRAY[1]}
fi
if [ -z "$PREVIOUS_MAJOR" ]; then
echo "Warning: Only one major version found (${LATEST_MAJOR}), using it as both Latest and Previous"
PREVIOUS_MAJOR=$LATEST_MAJOR
fi
echo "Latest major version: ${LATEST_MAJOR}"
echo "Previous major version: ${PREVIOUS_MAJOR}"
# Build versions map: major version -> latest patch version
declare -A VERSION_MAP
MAJOR_VERSIONS=()
PROCESSED_MAJORS=()
for major_version in "$LATEST_MAJOR" "$PREVIOUS_MAJOR"; do
# Skip if we already processed this major version
if [[ " ${PROCESSED_MAJORS[@]} " =~ " ${major_version} " ]]; then
continue
fi
PROCESSED_MAJORS+=("${major_version}")
# Find all tags that match this major version
matching_tags=$(echo "$AVAILABLE_TAGS" | grep "^${major_version}\\.")
if [ -n "$matching_tags" ]; then
# Get the latest patch version for this major version
latest_tag=$(echo "$matching_tags" | tail -n1)
VERSION_MAP["v${major_version}"]="${latest_tag}"
MAJOR_VERSIONS+=("v${major_version}")
echo "Found version: v${major_version} -> ${latest_tag}"
else
echo "Warning: Could not find any patch versions for ${major_version}, skipping..." >&2
fi
done
if [ ${#MAJOR_VERSIONS[@]} -eq 0 ]; then
echo "Error: No matching versions found" >&2
exit 1
fi
# Sort major versions in descending order (newest first)
IFS=$'\n' MAJOR_VERSIONS=($(printf '%s\n' "${MAJOR_VERSIONS[@]}" | sort -V -r))
unset IFS
echo "Major versions to add: ${MAJOR_VERSIONS[*]}"
# Create/update versions.yaml file
echo "Updating $VERSIONS_FILE..."
{
for major_ver in "${MAJOR_VERSIONS[@]}"; do
echo "\"${major_ver}\": \"${VERSION_MAP[$major_ver]}\""
done
} > "$VERSIONS_FILE"
echo "Successfully updated $VERSIONS_FILE"
# Update values.yaml - enum with major versions only
TEMP_FILE=$(mktemp)
trap "rm -f $TEMP_FILE" EXIT
# Build new version section
NEW_VERSION_SECTION="## @enum {string} Version"
for major_ver in "${MAJOR_VERSIONS[@]}"; do
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @value $major_ver"
done
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @param {Version} version - Redis major version to deploy
version: ${MAJOR_VERSIONS[0]}"
# Check if version section already exists
if grep -q "^## @enum {string} Version" "$VALUES_FILE"; then
# Version section exists, update it using awk
echo "Updating existing version section in $VALUES_FILE..."
# Use awk to replace the section from "## @enum {string} Version" to "version: " (inclusive)
# Delete the old section and insert the new one
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @enum {string} Version/ {
in_section = 1
print new_section
next
}
in_section && /^version: / {
in_section = 0
next
}
in_section {
next
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
else
# Version section doesn't exist, insert it before Application-specific parameters section
echo "Inserting new version section in $VALUES_FILE..."
# Use awk to insert before "## @section Application-specific parameters"
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @section Application-specific parameters/ {
print new_section
print ""
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
fi
echo "Successfully updated $VALUES_FILE with major versions: ${MAJOR_VERSIONS[*]}"

View File

@@ -0,0 +1,8 @@
{{- define "redis.versionMap" }}
{{- $versionMap := .Files.Get "files/versions.yaml" | fromYaml }}
{{- if not (hasKey $versionMap .Values.version) }}
{{- printf `Redis version %s is not supported, allowed versions are %s` $.Values.version (keys $versionMap) | fail }}
{{- end }}
{{- index $versionMap .Values.version }}
{{- end }}

View File

@@ -27,7 +27,7 @@ spec:
replicas: 3
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 6 }}
redis:
image: "redis:8.2.0"
image: "redis:{{ include "redis.versionMap" $ }}"
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 6 }}
replicas: {{ .Values.replicas }}
{{- with .Values.size }}

View File

@@ -82,6 +82,15 @@
"description": "StorageClass used to store the data.",
"type": "string",
"default": ""
},
"version": {
"description": "Redis major version to deploy",
"type": "string",
"default": "v8",
"enum": [
"v8",
"v7"
]
}
}
}

View File

@@ -33,6 +33,13 @@ storageClass: ""
## @param {bool} external - Enable external access from outside the cluster.
external: false
## @enum {string} Version
## @value v8
## @value v7
## @param {Version} version - Redis major version to deploy
version: v8
##
## @section Application-specific parameters
##

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

@@ -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

@@ -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

@@ -74,6 +74,18 @@ releases:
namespace: cozy-system
dependsOn: [cozystack-controller,cilium,cert-manager]
- name: cozystack-resource-definition-crd
releaseName: cozystack-resource-definition-crd
chart: cozystack-resource-definition-crd
namespace: cozy-system
dependsOn: [cilium]
- name: cozystack-resource-definitions
releaseName: cozystack-resource-definitions
chart: cozystack-resource-definitions
namespace: cozy-system
dependsOn: [cilium,cozystack-controller,cozystack-resource-definition-crd]
- name: cert-manager
releaseName: cert-manager
chart: cozy-cert-manager
@@ -84,15 +96,26 @@ releases:
releaseName: cert-manager-issuers
chart: cozy-cert-manager-issuers
namespace: cozy-cert-manager
optional: true
dependsOn: [cilium,cert-manager]
- name: prometheus-operator-crds
releaseName: prometheus-operator-crds
chart: cozy-prometheus-operator-crds
namespace: cozy-victoria-metrics-operator
dependsOn: []
- name: metrics-server
releaseName: metrics-server
chart: cozy-metrics-server
namespace: cozy-monitoring
dependsOn: [cilium,prometheus-operator-crds]
- name: victoria-metrics-operator
releaseName: victoria-metrics-operator
chart: cozy-victoria-metrics-operator
namespace: cozy-victoria-metrics-operator
optional: true
dependsOn: [cilium,cert-manager]
dependsOn: [cilium,cert-manager,prometheus-operator-crds]
- name: monitoring-agents
releaseName: monitoring-agents
@@ -100,7 +123,7 @@ releases:
namespace: cozy-monitoring
privileged: true
optional: true
dependsOn: [cilium,victoria-metrics-operator]
dependsOn: [cilium,victoria-metrics-operator,metrics-server]
values:
scrapeRules:
etcd:
@@ -192,7 +215,7 @@ releases:
releaseName: snapshot-controller
chart: cozy-snapshot-controller
namespace: cozy-snapshot-controller
dependsOn: [cilium]
dependsOn: [cilium,cert-manager-issuers]
- name: objectstorage-controller
releaseName: objectstorage-controller

View File

@@ -55,12 +55,24 @@ releases:
optional: true
dependsOn: [cert-manager]
- name: prometheus-operator-crds
releaseName: prometheus-operator-crds
chart: cozy-prometheus-operator-crds
namespace: cozy-victoria-metrics-operator
dependsOn: []
- name: metrics-server
releaseName: metrics-server
chart: cozy-metrics-server
namespace: cozy-monitoring
dependsOn: [prometheus-operator-crds]
- name: victoria-metrics-operator
releaseName: victoria-metrics-operator
chart: cozy-victoria-metrics-operator
namespace: cozy-victoria-metrics-operator
optional: true
dependsOn: [cert-manager]
dependsOn: [prometheus-operator-crds,cert-manager]
- name: monitoring-agents
releaseName: monitoring-agents
@@ -68,7 +80,7 @@ releases:
namespace: cozy-monitoring
privileged: true
optional: true
dependsOn: [victoria-metrics-operator]
dependsOn: [victoria-metrics-operator, metrics-server]
values:
scrapeRules:
etcd:

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
@@ -142,18 +148,30 @@ releases:
namespace: cozy-cert-manager
dependsOn: [cilium,kubeovn,cert-manager]
- name: prometheus-operator-crds
releaseName: prometheus-operator-crds
chart: cozy-prometheus-operator-crds
namespace: cozy-victoria-metrics-operator
dependsOn: []
- name: metrics-server
releaseName: metrics-server
chart: cozy-metrics-server
namespace: cozy-monitoring
dependsOn: [cilium,kubeovn,prometheus-operator-crds]
- name: victoria-metrics-operator
releaseName: victoria-metrics-operator
chart: cozy-victoria-metrics-operator
namespace: cozy-victoria-metrics-operator
dependsOn: [cilium,kubeovn,cert-manager]
dependsOn: [cilium,kubeovn,cert-manager,prometheus-operator-crds]
- name: monitoring-agents
releaseName: monitoring-agents
chart: cozy-monitoring-agents
namespace: cozy-monitoring
privileged: true
dependsOn: [victoria-metrics-operator, vertical-pod-autoscaler-crds]
dependsOn: [victoria-metrics-operator, vertical-pod-autoscaler-crds, metrics-server]
values:
scrapeRules:
etcd:

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
@@ -86,18 +91,30 @@ releases:
namespace: cozy-cert-manager
dependsOn: [cert-manager]
- name: prometheus-operator-crds
releaseName: prometheus-operator-crds
chart: cozy-prometheus-operator-crds
namespace: cozy-victoria-metrics-operator
dependsOn: []
- name: metrics-server
releaseName: metrics-server
chart: cozy-metrics-server
namespace: cozy-monitoring
dependsOn: [prometheus-operator-crds]
- name: victoria-metrics-operator
releaseName: victoria-metrics-operator
chart: cozy-victoria-metrics-operator
namespace: cozy-victoria-metrics-operator
dependsOn: [cert-manager]
dependsOn: [cert-manager,prometheus-operator-crds]
- name: monitoring-agents
releaseName: monitoring-agents
chart: cozy-monitoring-agents
namespace: cozy-monitoring
privileged: true
dependsOn: [victoria-metrics-operator, vertical-pod-autoscaler-crds]
dependsOn: [victoria-metrics-operator, vertical-pod-autoscaler-crds, metrics-server]
values:
scrapeRules:
etcd:

View File

@@ -91,3 +91,12 @@
| `grafana.resources.limits.cpu` | CPU limit. | `quantity` | `1` |
| `grafana.resources.limits.memory` | Memory limit. | `quantity` | `1Gi` |
### Vmagent configuration
| Name | Description | Type | Value |
| ------------------------ | ---------------------------------------- | -------- | ----- |
| `vmagent` | Configuration for VictoriaMetrics Agent. | `object` | `{}` |
| `vmagent.externalLabels` | External labels applied to all metrics. | `object` | `{}` |
| `vmagent.remoteWrite` | Remote write configuration. | `object` | `{}` |

View File

@@ -0,0 +1,30 @@
{{- if ne .Release.Namespace "tenant-root" }}
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMAgent
metadata:
name: vmagent
spec:
podMetadata:
labels:
policy.cozystack.io/allow-to-apiserver: "true"
shardCount: 1
externalLabels:
cluster: {{ .Values.vmagent.externalLabels.cluster }}
tenant: {{ .Release.Namespace }}
extraArgs:
promscrape.maxScrapeSize: "32MB"
promscrape.streamParse: "true"
remoteWrite:
{{- range .Values.vmagent.remoteWrite.urls }}
- url: {{ . | quote }}
{{- end }}
scrapeInterval: 30s
selectAllByDefault: false
podScrapeNamespaceSelector:
matchLabels:
namespace.cozystack.io/monitoring: {{ .Release.Namespace }}
serviceScrapeNamespaceSelector:
matchLabels:
namespace.cozystack.io/monitoring: {{ .Release.Namespace }}
{{- end }}

View File

@@ -560,6 +560,28 @@
}
}
}
},
"vmagent": {
"description": "Configuration for VictoriaMetrics Agent.",
"type": "object",
"default": {},
"properties": {
"externalLabels": {
"description": "External labels applied to all metrics.",
"default": {
"cluster": "cozystack"
}
},
"remoteWrite": {
"description": "Remote write configuration.",
"default": {
"urls": [
"http://vminsert-shortterm:8480/insert/0/prometheus",
"http://vminsert-longterm:8480/insert/0/prometheus"
]
}
}
}
}
}
}

View File

@@ -154,3 +154,25 @@ grafana:
requests:
cpu: 100m
memory: 256Mi
##
## @section Vmagent configuration
##
## @typedef {struct} VmagentRemoteWrite - Remote write configuration.
## @typedef {stringSlice} VmagentRemoteWriteURLs - List of remoteWrite endpoint URLs.
## @typedef {struct} VmagentExternalLabels - External labels for metrics.
## @field {string} [<key>] - Label value for the given key.
## @typedef {struct} Vmagent - VictoriaMetrics Agent configuration.
## @field {VmagentExternalLabels} [externalLabels] - External labels applied to all metrics.
## @field {VmagentRemoteWrite} [remoteWrite] - Remote write configuration.
## @param {Vmagent} vmagent - Configuration for VictoriaMetrics Agent.
vmagent:
externalLabels:
cluster: cozystack
remoteWrite:
urls:
- http://vminsert-shortterm:8480/insert/0/prometheus
- http://vminsert-longterm:8480/insert/0/prometheus

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 @@
* linguist-generated

View File

@@ -0,0 +1,231 @@
---
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:
- 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
served: true
storage: true

View File

@@ -0,0 +1,234 @@
---
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
served: true
storage: true

View File

@@ -0,0 +1,135 @@
---
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
type: object
served: true
storage: true

View File

@@ -0,0 +1,168 @@
---
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:
- 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

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,11 @@
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"]

View File

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

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"]

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