Compare commits

..

92 Commits

Author SHA1 Message Date
Andrei Kvapil
37e84be573 Release v0.40.5 (#1925)
This PR prepares the release `v0.40.5`.
2026-01-29 10:08:30 +01:00
cozystack-bot
7e58850fb1 Prepare release v0.40.5
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-01-29 01:39:21 +00:00
Andrei Kvapil
ef00445322 Update cozyhr v1.6.1
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-27 23:10:58 +01:00
Andrei Kvapil
8bd3861164 [Backport release-0.40] [dashboard] Improve dashboard session params (#1919)
# Description
Backport of #1913 to `release-0.40`.
2026-01-27 18:56:31 +01:00
Timofei Larkin
d11dc983b8 [dashboard] Improve dashboard session params
## What this PR does

This patch enables the `offline_access` scope for the dashbord keycloak
client, so that users get a refresh token which gatekeeper can use to
automatically refresh an expiring access token. Also session timeouts
were increased.

### Release note

```release-note
[dashboard] Increase session timeouts, add the offline_access scope,
enable refresh tokens to improve the overall user experience when
working with the dashboard.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
(cherry picked from commit 7cebafbafd)
2026-01-27 17:18:42 +00:00
Andrei Kvapil
5e9bff0e50 Release v0.40.4 (#1890)
This PR prepares the release `v0.40.4`.
2026-01-20 03:31:21 +01:00
cozystack-bot
cfbe41d8e0 Prepare release v0.40.4
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-01-20 01:34:53 +00:00
Andrei Kvapil
d13a45c8d6 Update Talos Linux v1.11.6 (#1879)
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 PR updates Talos Linux which includes DRBD fixes:
- https://github.com/LINBIT/drbd/releases/tag/drbd-9.2.15
- https://github.com/LINBIT/drbd/releases/tag/drbd-9.2.16

### 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
Update Talos Linux v1.11.
```
2026-01-20 01:54:40 +01:00
Andrei Kvapil
6b3cd40ba5 [Backport release-0.40] [fix] Fix view of loadbalancer ip in services window (#1887)
# Description
Backport of #1884 to `release-0.40`.
2026-01-20 01:54:17 +01:00
IvanHunters
838d1abff6 [fix] Fix view of loadbalancer ip in services window
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
(cherry picked from commit 5c7c3359b3)
2026-01-20 00:48:39 +00:00
Andrei Kvapil
4cdd1113d1 [Backport release-0.40] fix(kubernetes): increase kube-apiserver startup probe threshold (#1883)
# Description
Backport of #1876 to `release-0.40`.
2026-01-19 20:13:03 +01:00
Andrei Kvapil
4d7ee809e5 [Backport release-0.40] [kubernetes] Increase default apiServer resourcesPreset to large (#1882)
# Description
Backport of #1875 to `release-0.40`.
2026-01-19 20:12:53 +01:00
Andrei Kvapil
125a57eb97 fix(kubernetes): increase kube-apiserver startup probe threshold
Increase startupProbe.failureThreshold from 3 to 30 for kube-apiserver
container. The default 30 seconds (3 failures * 10s period) is often
insufficient for apiserver to complete RBAC bootstrap, especially under
load or with slower etcd connections.

This change gives kube-apiserver up to 300 seconds to initialize,
preventing unnecessary CrashLoopBackOff cycles.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 8261ea4fcf)
2026-01-19 19:12:43 +00:00
Andrei Kvapil
d33a2357ed fix(kubernetes): increase default apiServer resourcesPreset to large
Change the default resourcesPreset for kube-apiserver from "medium" to
"large" to prevent OOM kills during normal operation. The "medium"
preset provides insufficient memory for clusters with moderate workloads.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 6256893040)
2026-01-19 19:12:33 +00:00
Andrei Kvapil
a8a7e510ac [Backport release-0.40] [etcd] Increase probe thresholds for better recovery (#1878)
# Description
Backport of #1874 to `release-0.40`.
2026-01-19 13:13:49 +01:00
Andrei Kvapil
cf0598bcf1 Update Talos Linux v1.11.6
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-19 11:01:48 +01:00
Andrei Kvapil
6169220317 fix(etcd): increase probe thresholds for better recovery
Increase startup probe failureThreshold to 300 (25 minutes) to allow
etcd members more time to sync with the cluster after restart or
recovery. This prevents pods from being killed during initial
synchronization when VPA assigns minimal resources.

Also increase liveness probe failureThreshold to 10 to reduce
unnecessary restarts during temporary network issues.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit c58c959df6)
2026-01-19 09:56:16 +00:00
Andrei Kvapil
fbe33e1191 Release v0.40.3 (#1871)
This PR prepares the release `v0.40.3`.
2026-01-19 09:04:37 +01:00
cozystack-bot
9507f24332 Prepare release v0.40.3
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-01-19 01:34:56 +00:00
Andrei Kvapil
2aa839b68f [Backport release-0.40] [cilium] Update cilium to v1.18.6 (#1870)
# Description
Backport of #1868 to `release-0.40`.
2026-01-16 20:49:18 +01:00
Kirill Ilin
40683e28a7 [cilium] Update cilium to v1.18.6
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
(cherry picked from commit 464b6b3bb6)
2026-01-16 19:15:21 +00:00
Andrei Kvapil
4ddb374680 [apiserver] Fix Watch resourceVersion and bookmark handling (#1860)
Fixes aggregated API server Watch implementation to properly work with
controller-runtime informers. External controllers watching Tenant
resources via the aggregated API were experiencing issues with cache
sync timeouts and missing reconciliations on startup.

**ResourceVersion handling in List:**
- Compute ResourceVersion from items when the cached client doesn't set
it on the list itself

**Bookmark handling:**
- Pass through bookmark events with converted types for proper informer
sync

**ADDED event filtering (main fix):**
- Simplified the filtering logic that was incorrectly skipping initial
events
- Only skip ADDED events when `startingRV > 0` AND `objRV <= startingRV`
(client already has from List)
- When `startingRV == 0`, always send ADDED events (client wants full
state)
- Removed the complex `initialSyncComplete` tracking that had inverted
logic

When a controller starts watching resources, controller-runtime may call
Watch with `resourceVersion=""`. The server should send all existing
objects as ADDED events. The previous `initialSyncComplete` logic was
inverted and could skip these events, causing objects (like Tenants with
lock annotations) to not be reconciled on controller startup.

```release-note
[apiserver] Fix Watch resourceVersion and bookmark handling for controller-runtime compatibility
```

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

* **Improvements**
* Enhanced resource watching and synchronization with Kubernetes 1.27+
compatibility, including proper handling of initial events and
bookmarks.
* Optimized event filtering and resource version tracking for
applications, tenant modules, tenant namespaces, and tenant secrets to
reduce unnecessary event noise.
* Improved list metadata consistency by deriving accurate resource
versions when unavailable in responses.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-16 17:23:38 +01:00
Andrei Kvapil
e07fafb393 Release v0.40.2 (#1858)
This PR prepares the release `v0.40.2`.
2026-01-13 17:22:13 +01:00
cozystack-bot
7b932a400d Prepare release v0.40.2
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-01-13 12:32:18 +00:00
Andrei Kvapil
8f317c9065 [Backport release-0.40] [linstor] Refactor node-level RWX validation (#1857)
# Description
Backport of #1856 to `release-0.40`.
2026-01-13 13:28:11 +01:00
Andrei Kvapil
e73bf9905d [linstor] Refactor node-level RWX validation
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 3c4f0cd952)
2026-01-13 12:27:52 +00:00
Andrei Kvapil
c5222aae97 [linstor] Remove node-level RWX validation (#1851)
## What this PR does

Removes node-level RWX block validation from linstor-csi as
controller-level check is sufficient. The controller already validates
that all pods attached to RWX block volume belong to the same VM by
extracting vmName from pod owner references (VirtualMachineInstance).

This simplifies the validation logic and fixes VM live migration issues.

### Release note

```release-note
[linstor] Remove node-level RWX block validation to fix VM live migration
```

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

## Summary by CodeRabbit

* **Improvements**
* Enhanced RWX (read-write-many) block validation with VM-aware checks
across node and controller flows, including support for hotplug-disk
pods and stricter prevention of cross-VM block sharing.
* Improved propagation and resolution of VM identity for attachments to
ensure consistent validation.

* **Tests**
* Added comprehensive unit tests covering single/multiple pod scenarios,
VM ownership, hotplug disks, upgrade paths, and legacy volumes.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-13 11:28:03 +01:00
Andrei Kvapil
e176cdec87 feat(linstor): add linstor-csi image build with RWX validation
Add custom linstor-csi image build to packages/system/linstor:

- Add Dockerfile based on upstream linstor-csi
- Import patch from upstream PR #403 for RWX block volume validation
  (prevents misuse of allow-two-primaries in KubeVirt live migration)
- Update Makefile to build both piraeus-server and linstor-csi images
- Configure LinstorCluster CR to use custom linstor-csi image in
  CSI controller and node pods

The RWX validation patch ensures that RWX block volumes with
allow-two-primaries are only used by pods belonging to the same
KubeVirt VM during live migration.

Upstream PR: https://github.com/piraeusdatastore/linstor-csi/pull/403

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-13 11:27:55 +01:00
Andrei Kvapil
477d391440 Release v0.40.1 (#1855)
This PR prepares the release `v0.40.1`.
2026-01-13 08:04:35 +01:00
cozystack-bot
9ba76d4839 Prepare release v0.40.1
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-01-13 01:39:20 +00:00
Andrei Kvapil
f5ccbe94f9 [Backport release-0.40] [linstor] Update piraeus-server patches with critical fixes (#1852)
# Description
Backport of #1850 to `release-0.40`.
2026-01-12 23:23:59 +01:00
Andrei Kvapil
f900db6338 [linstor] Update piraeus-server patches with critical fixes
Update piraeus-server patches to address critical production issues:

- Add fix-duplicate-tcp-ports.diff to prevent duplicate TCP ports
  after toggle-disk operations (upstream PR #476)

- Update skip-adjust-when-device-inaccessible.diff with comprehensive
  fixes for resources stuck in StandAlone after reboot, Unknown state
  race condition, and encrypted LUKS resource deletion (upstream PR #477)

```release-note
[linstor] Fix DRBD resources stuck in StandAlone state after reboot and encrypted resource deletion issues
```

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit dc2773ba26)
2026-01-12 22:23:42 +00:00
Andrei Kvapil
3bcc0e5cc0 Release v0.40.0 (#1840)
This PR prepares the release `v0.40.0`.
2026-01-10 03:06:55 +01:00
cozystack-bot
2a8317291d Prepare release v0.40.0
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-01-10 00:57:35 +00:00
Andrei Kvapil
acc6ed26bb fix(platform): fix migrations for v0.40 release (#1846)
## What this PR does

Fixes migration scripts for v0.40 release to ensure smooth upgrade
process.

Changes:
- **Migration 20**: Replace `helm upgrade --install` with `cozyhr apply`
for cozystack-controller and lineage-controller-webhook
- **Migration 21**: Improve Flux instance removal process:
  - Disable reconciliation on FluxInstance before deletion
  - Delete Flux deployments before HelmReleases
  - Add `--wait=false` flags to prevent hanging
  - Fix CRD grep pattern to properly match `fluxcd.io` domains
- **Migration 22**: Add new migration tasks:
- Migrate Victoria Metrics Operator CRDs to prometheus-operator-crds
Helm release
  - Remove CozyStack Resource Definitions HelmRelease
- Add default version v17 to PostgreSQL HelmReleases without explicit
version

### Release note

```release-note
[platform] Fix migration scripts for v0.40 release: improve Flux removal, add VictoriaMetrics CRD migration, set default PostgreSQL version
```
2026-01-10 01:52:12 +01:00
Andrei Kvapil
3af7430074 fix(registry): implement field selector filtering for label-based resources (#1845)
## What this PR does

Fixes field selector filtering for registry resources (Applications,
TenantModules, TenantSecrets) when using kubectl with field selectors
like `--field-selector=metadata.namespace=tenant-kvaps` or
`metadata.name=test`.

Controller-runtime cache doesn't support field selectors natively, which
caused incorrect filtering behavior. This PR implements manual filtering
for `metadata.name` and `metadata.namespace` field selectors in List()
and Watch() methods.

Changes:
- Created `pkg/registry/fields` package with `ParseFieldSelector`
utility for common field selector parsing
- Refactored field selector logic in application, tenantmodule, and
tenantsecret registries to use the common implementation
- Implemented manual post-processing filtering after label-based queries
- Removed `Raw` field usage and field selectors from
`client.ListOptions`

### Release note

```release-note
[registry] Fix field selector filtering for kubectl queries with metadata.name and metadata.namespace selectors
```
2026-01-10 01:52:08 +01:00
Andrei Kvapil
800ca3b3d2 [main][paas-full] Add multus dependencies similar to other CNIs (#1842)
Adds multus as a dependency as other CNIs

```release-note
Adds multus as a dependency as other CNIs
```

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

* **Chores**
* Updated component initialization ordering to ensure more consistent
and reliable platform startup sequencing.
* Streamlined dependency requirements for several core system components
to improve deployment reliability and reduce startup complexity.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-10 01:50:06 +01:00
Andrei Kvapil
4a83d2c7c8 [platform] fix migration for removing fluxcd-operator
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-09 16:24:42 +01:00
Andrei Kvapil
c0b1539d3e [kube-ovn] Update to v1.14.25 (#1819)
## What this PR does

Update kube-ovn from v1.14.11 to v1.14.25.

Changes synced from upstream include:
- Updated chart templates
- New configuration options in values

### Release note

```release-note
[kube-ovn] Update to v1.14.25
```

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

## Summary by CodeRabbit

* **New Features**
  * Added tolerations support for VpcNatGateway resources
* Added automatic VLAN subinterface creation capability for provider
networks
* Enhanced installation documentation with OCI Registry and Talos Linux
deployment examples

* **Security Improvements**
  * Applied runtime-default seccomp profiles across deployments
* Configured service account token mounting behavior for improved pod
security

* **Chores**
  * Updated Kube-OVN to v1.14.25
  * Updated Helm chart metadata and dependencies

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:03 +01:00
Andrei Kvapil
1c42f8bd10 [linstor] fix: prevent DRBD device race condition in updateDiscGran (#1829)
## Problem

After satellite restart with patched linstor-server, some DRBD resources
ended up in **Unknown** state. Investigation revealed a race condition
between `drbdadm adjust` completion and `updateDiscGran()` lsblk check:

1. `drbdadm adjust` completes successfully (21:53:49.791) - brings
devices up
2. `updateDiscGran()` immediately tries to check discard granularity
(21:53:49.799)
3. `/dev/drbd*` device node doesn't exist yet - kernel hasn't created it
4. `lsblk` fails with exit code 32: "not a block device"  
5. `StorageException` interrupts DeviceManager cycle (21:53:50.811)
6. DRBD device remains in incomplete state → **Unknown**

### Timeline from logs

```
21:53:49.791 - [DeviceManager] Resource 'pvc-aafbd92a' [DRBD] adjusted ✓
21:53:49.799 - [lsblk_parser] ERROR: /dev/drbd1169 not a block device
21:53:49.804-50.807 - [lsblk_parser] 10+ retry errors (every ~100ms)
21:53:50.811 - [DeviceManager] ERROR: Error executing lsblk
21:53:50.878 - [DeviceManager] End cycle 9 (WITH ERROR!)
```

## Solution

Update `skip-adjust-when-device-inaccessible.diff` patch to add physical
device path existence check before calling `lsblk`:

- Check `Files.exists(devicePath)` in `updateDiscGran()` before lsblk
- If device doesn't exist yet → skip check, set `discGran =
UNINITIALIZED_SIZE`
- Next DeviceManager cycle (few seconds later) → device node available →
lsblk succeeds

This complements the existing patch which checks **child layer** devices
(LUKS/Storage) for deletion scenarios, while this fix addresses the
**DRBD device itself** during adjust operations.

## Testing

Manual `drbdadm up` on affected devices confirmed they were in down
state and brought them back to UpToDate, proving the issue was
incomplete device initialization.

## Related

- Upstream linstor-server PR:
https://github.com/LINBIT/linstor-server/pull/471#issuecomment-3723392917

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

## Summary by CodeRabbit

* **Bug Fixes**
* Enhanced robustness for storage operations by adding device
accessibility validation. Operations now gracefully skip when device
paths are unavailable or invalid, preventing unnecessary failures and
improving system resilience during device access issues.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:03 +01:00
Timofei Larkin
de2bbd35c2 [keycloak] Make kubernetes client public (#1802)
## What this PR does

By its nature, the kubernetes client provisioned in keycloak is public:
anyone can read the client secret. Therefore the secret is not necessary
and the client should be explicitly provisioned as a public client, not
as a confidential client.

### Release note

```release-note
[keycloak] Change the kubernetes client to be public, without a client
secret.
```

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

* **New Features**
* Add OIDC enablement flag and optional host derivation from tenant
release; allow explicit management kubeconfig endpoint override.

* **Refactor**
* Convert Keycloak client to public mode and remove reliance on a
Kubernetes client secret, simplifying authentication configuration.
* Remove secret-based credential plumbing from deployment templates,
streamlining kubeconfig generation.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:03 +01:00
Andrei Kvapil
fe82f66330 [linstor] Enable auto-diskful for diskless nodes (#1826)
## What this PR does

Enable DRBD auto-diskful options to automatically convert diskless nodes
to diskful when they remain in Primary state for an extended period.

### How it works

When LINSTOR is integrated with Kubernetes, the platform may schedule
workloads on nodes that don't have local storage replicas. In such
cases, DRBD operates in "diskless" mode, accessing data over the network
from nodes that have actual disk replicas.

The `auto-diskful` feature addresses this by:

1. **Monitoring Primary state duration**: When a diskless node holds a
DRBD resource in Primary state (actively using the volume) for more than
the configured time (30 minutes), LINSTOR automatically creates a local
disk replica on that node.

2. **Automatic cleanup**: With `auto-diskful-allow-cleanup` enabled,
when the resource is no longer in Primary state on that node, LINSTOR
can automatically remove the disk replica that was created, freeing up
storage space.

### Configuration

- `DrbdOptions/auto-diskful: 30` — Convert diskless to diskful after 30
minutes in Primary state
- `DrbdOptions/auto-diskful-allow-cleanup: true` — Allow automatic
removal of auto-created replicas when no longer needed

### Benefits

- Improves I/O performance for long-running workloads by creating local
replicas
- Reduces network traffic for frequently accessed data
- Automatic cleanup prevents storage waste from temporary replicas

### Release note

```release-note
[linstor] Enable auto-diskful to automatically create local replicas on diskless nodes that hold volumes in Primary state for more than 30 minutes, improving I/O performance for long-running workloads.
```

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

* **New Features**
* Introduced an "auto diskful" option to automatically manage diskful
resources with configurable interval and optional cleanup.
* **Chores**
* Added default configuration values to enable and time the auto-diskful
behavior and to control automatic 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 -->
2026-01-09 14:54:03 +01:00
Andrei Kvapil
c4bf72b44f [linstor] Add linstor-scheduler package (#1824)
## What this PR does

Add linstor-scheduler-extender for optimal pod placement on nodes with
LINSTOR storage.

### How it works

The linstor-scheduler consists of two components:

#### 1. Scheduler Extender
A custom Kubernetes scheduler that works alongside the default
kube-scheduler. When a pod requests LINSTOR-backed storage, the
scheduler extender communicates with the LINSTOR controller to find
nodes that have local replicas of the requested volumes, prioritizing
placement on nodes with existing data to minimize network traffic.

#### 2. Admission Webhook
A MutatingWebhookConfiguration that automatically sets `schedulerName:
linstor-scheduler` on pods that use LINSTOR CSI driver volumes. This
ensures that only pods with LINSTOR PVCs are routed through the custom
scheduler, while all other pods continue using the default scheduler.

The webhook inspects pod creation requests and checks if any of the
pod's PVCs use a StorageClass with the LINSTOR CSI provisioner
(`linstor.csi.linbit.com`). If so, it mutates the pod spec to use the
linstor-scheduler.

### Components

- `linstor-scheduler` package in `packages/system/linstor-scheduler/`
- PackageSource definition in
`packages/core/platform/sources/linstor-scheduler.yaml`
- Integration into `paas-full` and `distro-full` bundles

### Upstream PRs

- https://github.com/piraeusdatastore/helm-charts/pull/67 — fix:
KubeSchedulerConfiguration v1 support for Kubernetes 1.25+
- https://github.com/piraeusdatastore/helm-charts/pull/68 — feat:
admission webhook support with cert-manager
- https://github.com/piraeusdatastore/helm-charts/pull/69 — fix chart
template issues

### Release note

```release-note
[linstor] Add linstor-scheduler for optimal pod placement on nodes with local LINSTOR replicas. Includes admission webhook that automatically routes pods using LINSTOR CSI volumes to the custom scheduler.
```

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

* **New Features**
* Added LINSTOR scheduler component to the platform, packaged as a Helm
chart and installable package.
* Includes admission webhook, scheduler extender, autoscaling support,
pod disruption budget, RBAC and service account integration, and Helm
test hooks.
* **Documentation**
* Added chart README and default configuration values for easy
deployment and customization.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:02 +01:00
Nikita
33452d65e5 [seaweedfs] Traffic locality (#1748)
## What this PR does
Addes traffic locality capabilities to Seaweedfs.

### Release note
```release-note
Added traffic locality capabilities to Seaweedfs
2026-01-09 14:54:02 +01:00
Andrei Kvapil
9039011252 [api, lineage] Tolerate all taints (#1781)
## What this PR does
Cozystack-api and lineage-webhook tolerate all taints, so they are able
to run on masters no matter what.
Needed for unschedulable control-plane setup, quorum nodes, etc.

### Release note
```release-note
Cozystack-api and lineage-webhook tolerate all taints.
```

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

## Summary by CodeRabbit

* **Chores**
* Updated pod scheduling tolerations to improve deployment flexibility
and compatibility across diverse cluster configurations.
* Enhanced workload distribution by making pods more resilient to
cluster taints, enabling better resource utilization in various
environments.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:02 +01:00
Andrei Kvapil
2bbeb1b474 fix(linstor): prevent orphaned DRBD devices during toggle-disk retry (#1823)
## Summary

Fix bug in toggle-disk retry logic that left orphaned DRBD devices in
kernel.

## Problem

When toggle-disk retry was triggered (e.g., user retries after a failed
operation), the code called `removeLayerData()` to clean up and recreate
the layer stack. However, `removeLayerData()` only removes data from the
controller's database — it does NOT call `drbdadm down` on the
satellite.

This caused DRBD devices to remain in the kernel (visible in `drbdsetup`
but not managed by LINSTOR), occupying ports and blocking subsequent
operations.

## Solution

Changed retry logic to simply repeat the operation with existing layer
data intact. The satellite handles this idempotently without creating
orphaned resources.

## Upstream

- https://github.com/LINBIT/linstor-server/pull/475 (updated)

```release-note
[linstor] Fix orphaned DRBD devices during toggle-disk retry
```

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

## Summary by CodeRabbit

* **New Features**
  * Added cancel and retry capabilities for disk addition operations
  * Added cancel and retry capabilities for disk removal operations
* Improved cleanup handling for diskless resources with orphaned storage
layers

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:02 +01:00
Andrei Kvapil
a51e2078a8 [linstor] Build linstor-server with custom patches (#1726)
## What this PR does

Build piraeus-server (linstor-server) from source with custom patches:

- **adjust-on-resfile-change.diff** — Use actual device path in res file
during toggle-disk; fix LUKS data offset
- Upstream: [#473](https://github.com/LINBIT/linstor-server/pull/473),
[#472](https://github.com/LINBIT/linstor-server/pull/472)
- **allow-toggle-disk-retry.diff** — Allow retry and cancellation of
failed toggle-disk operations
  - Upstream: [#475](https://github.com/LINBIT/linstor-server/pull/475)
- **force-metadata-check-on-disk-add.diff** — Create metadata during
toggle-disk from diskless to diskful
  - Upstream: [#474](https://github.com/LINBIT/linstor-server/pull/474)
- **skip-adjust-when-device-inaccessible.diff** — Skip DRBD adjust/res
file regeneration when child layer device is inaccessible
  - Upstream: [#471](https://github.com/LINBIT/linstor-server/pull/471)

Also updates plunger-satellite script and values.yaml for the new build.

### Release note

```release-note
[linstor] Build linstor-server with custom patches for improved disk handling
```

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

## Summary by CodeRabbit

* **New Features**
* Added automatic DRBD stall detection and recovery, improving storage
resync resilience without manual intervention.
* Introduced configurable container image references via Helm values for
streamlined deployment.

* **Bug Fixes**
* Enhanced disk toggle operations with retry and cancellation support
for better error handling.
  * Improved metadata creation during disk state transitions.
* Added device accessibility checks to prevent errors when underlying
storage devices are unavailable.
* Fixed LUKS encryption header sizing for consistent deployment across
nodes.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:02 +01:00
Timofei Larkin
e40a95849d [testing] Add aliases and autocomplete (#1803)
## What this PR does

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

### Release note

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

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

* **Chores**
* Enhanced the e2e sandbox image to enable shell bash-completion and
kubectl command completion.
* Added an alias (k) and completion wiring for kubectl to improve
interactive command use.
* These changes augment the test environment shell during image build to
provide a smoother developer/testing experience.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:02 +01:00
Andrei Kvapil
31bcbbd5bf [kubernetes] Fix endpoints for cilium-gateway (#1729)
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

Integrate fix
- https://github.com/kubevirt/cloud-provider-kubevirt/pull/379

### 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
[kubernetes] Fix endpoints for cilium-gateway
```

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

* **Bug Fixes**
* Improved handling of incomplete endpoint data by introducing fallback
detection mechanisms.
* Enhanced service discovery to gather endpoints from all available
resources when standard detection fails.
* Updated logging to provide better visibility into fallback operations
and current resource status.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:01 +01:00
Andrei Kvapil
e7b62ca3c8 [platform] Replace Helm lookup with valuesFrom mechanism (#1787)
## What this PR does

Replaces Helm lookup functions with FluxCD valuesFrom mechanism for
passing configuration to HelmReleases. This provides cleaner config
propagation and eliminates the need for force reconcile controllers.

### Changes:

**Platform/Tenant charts:**
- Add Secret `cozystack-values` creation in platform chart (for
tenant-root and system namespaces)
- Add Secret `cozystack-values` creation in tenant chart (for child
namespaces)

**cozystack-api:**
- Add `valuesFrom` references to HelmRelease when creating applications
- Filter keys starting with `_` when returning Application specs
- Validate that user values don't contain `_` prefixed keys

**cozystack-controller:**
- Add validation that HelmRelease contains correct valuesFrom
configuration
- Remove `CozystackConfigReconciler` (no longer needed)
- Remove `TenantHelmReconciler` (no longer needed)

**Helm charts (40+ files):**
- Add helper templates in cozy-lib for `_cluster`/`_namespace` access
- Replace ConfigMap lookups with `.Values._cluster.*`
- Replace Namespace annotation lookups with `.Values._namespace.*`

### Architecture:

```
Secret cozystack-values (in each namespace)
├── _cluster: YAML with data from ConfigMaps (cozystack, cozystack-branding, cozystack-scheduling)
└── _namespace: YAML with namespace service references (etcd, host, ingress, monitoring, seaweedfs)

HelmRelease
└── spec.valuesFrom:
    ├── Secret/cozystack-values → _namespace → .Values._namespace
    └── Secret/cozystack-values → _cluster → .Values._cluster
```

### Release note

```release-note
[platform] Replace Helm lookup functions with FluxCD valuesFrom mechanism for configuration propagation
```

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

* **New Features**
* Helm releases and namespaces now source centralized cluster/namespace
configuration via a new secret (cozystack-values), and many templates
read values from chart-provided _cluster/_namespace entries.

* **Bug Fixes**
* API now rejects application specs containing reserved keys prefixed
with "_" to prevent invalid configurations.

* **Refactor**
* Two background reconciler controllers were removed from startup,
simplifying controller initialization.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:01 +01:00
Andrei Kvapil
868a7497d0 fix(ci): ensure correct latest release after backport publishing (#1800)
## What this PR does

Fixes an issue where backport releases incorrectly became marked as
"Latest" despite passing `make_latest: 'false'` to the GitHub API.

**Root cause:** The `getLatestRelease()` API returns the release with
the "Latest" flag, not the highest semver version. Combined with race
conditions during parallel release publishing and GitHub API potentially
ignoring `make_latest: 'false'`, backport releases were incorrectly
marked as latest.

**Solution:**
- Replace `getLatestRelease()` with semver-based max version detection
across all published releases
- After publishing a backport release, explicitly restore the latest
flag on the highest semver release
- Remove unused dead code from `tags.yaml` workflow

### Release note

```release-note
[ci] Fix latest release detection to use semver comparison instead of GitHub's "Latest" flag
```
2026-01-09 14:54:01 +01:00
Aleksei Sviridkin
666eeffa47 [kubevirt-operator] Revert incorrect case change in VM alerts (#1804)
## What this PR does

Reverts PR #1770 which incorrectly changed status/phase checks from
lowercase to uppercase.

The actual KubeVirt metrics use **lowercase**:
- `kubevirt_vm_info` uses `status="running"` (not `"Running"`)
- `kubevirt_vmi_info` uses `phase="running"` (not `"Running"`)

### Verification

Queried virt-controller metrics directly in the instories cluster:

```
kubevirt_vm_info{...,status="running",status_group="running",...} 1
kubevirt_vmi_info{...,phase="running",...} 1
```

Note: `kubectl get vm` shows `STATUS: Running` with capital R, but this
is display formatting — the actual metric labels use lowercase.

### Release note

```release-note
[kubevirt-operator] Fix VM alert rules to use correct lowercase status values
```
2026-01-09 14:54:01 +01:00
Andrei Kvapil
8d166e0754 [platform] refactor: split cozystack-resource-definitions into separate packages (#1778)
## What this PR does

This PR splits the monolithic `cozystack-resource-definitions` package
into 25 individual resource definition packages (`*-rd`) for better
modularity and independent versioning.

**Changes:**
- Created 25 separate `*-rd` packages (bootbox-rd, bucket-rd,
clickhouse-rd, etcd-rd, ferretdb-rd, foundationdb-rd, http-cache-rd,
info-rd, ingress-rd, kafka-rd, kubernetes-rd, monitoring-rd, mysql-rd,
nats-rd, postgres-rd, rabbitmq-rd, redis-rd, seaweedfs-rd,
tcp-balancer-rd, tenant-rd, virtual-machine-rd, virtualprivatecloud-rd,
vm-disk-rd, vm-instance-rd, vpn-rd)
- Removed `packages/system/cozystack-resource-definitions`
- Updated platform bundles (paas-hosted, paas-full, distro-full) to
reference individual -rd packages
- Updated `hack/update-crd.sh` to use package-specific directories

Each `*-rd` package contains:
- `Chart.yaml` - package metadata
- `values.yaml` - default values
- `Makefile` - build instructions
- `cozyrds/<name>.yaml` - CRD definition
- `templates/cozyrd.yaml` - Helm template

**Benefits:**
- **Modularity**: Each resource definition is now a standalone package
- **Independent versioning**: Resources can be versioned independently
- **Maintainability**: Easier to update individual resources
- **Build efficiency**: Parallel building of resource packages

### Release note

```release-note
[platform] Split cozystack-resource-definitions into 25 separate *-rd packages for better modularity and independent versioning. Each resource definition is now a standalone package.

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

* **Refactor**
  * Split the monolithic resource-definitions into many independent resource-definition packages (e.g., bootbox-rd, bucket-rd, clickhouse-rd, etc.) for modular deployment and finer-grained management
  * Updated deployment bundles to reference the new per-resource releases with uniform cozy-system namespace and CRD dependency

* **Chores**
  * Added packaging/Helm stubs (Chart.yaml, Makefile, values, templates) for each new resource-definition
* **Bug Fixes**
  * Made CRD path resolution dynamic (NAME validated before assignment)

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:01 +01:00
Andrei Kvapil
967f0bdc9f [kubernetes] Add lb tests for tenant k8s (#1783)
<!-- 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
[kubernetes] Add lb tests for tenant k8s
```

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

* **Tests**
  * Increased readiness and port-forward timeouts to improve stability.
* Added full end-to-end provisioning and validation: automated namespace
and backend deployment, load balancer provisioning, health checks with
retries, reachability validation, and cleanup.
* Provisioning sequence now runs earlier and is duplicated within the
test flow, altering execution order and adding extra validation/cleanup
steps.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:00 +01:00
Andrei Kvapil
30c1041e7a feat(ci): add /retest command to rerun tests from Prepare environment
Allows maintainers to trigger test rerun by commenting /retest on a PR.
The workflow finds the latest run for the PR and reruns starting from
the "Prepare environment" job, skipping the build step.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-09 14:54:00 +01:00
Andrei Kvapil
68a639b32c fix(ci): remove GITHUB_TOKEN extraheader to trigger workflows
actions/checkout configures http.extraheader with GITHUB_TOKEN which
takes priority over URL credentials. This caused tag pushes to
authenticate as github-actions instead of cozystack-bot, preventing
the Versioned Tag workflow from being triggered.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-09 14:54:00 +01:00
Andrei Kvapil
2bf1168dcf [system] Add resource requests and limits to etcd-defrag (#1785)
## What this PR does
Added resource requests and limits for etcd-defrag container.

### 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
[system] Add resource requests and limits to etcd-defrag
```

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

## Summary by CodeRabbit

* **Chores**
* Enhanced resource configurations for the etcd defragmentation process
to ensure optimal performance and system stability.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:54:00 +01:00
Andrei Kvapil
526af29459 [ci] Fix auto-release workflow
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-09 14:54:00 +01:00
Andrei Kvapil
b65fc64e17 [tenant] Run cleanup job from system namespace (#1774)
## What this PR does

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

### Release note

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

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

## Summary by CodeRabbit

* **Infrastructure Updates**
* Updated cleanup job namespace targeting to use a fixed system
configuration
  * Adjusted cleanup job execution priority level

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:59 +01:00
Andrei Kvapil
9a2d39a4b0 [tenant] Allow egress to parent ingress pods (#1765)
## What this PR does

Fixes egress policy for nested Kubernetes clusters using `exposeMethod:
Proxied`.

The clusterwide egress policy blocks traffic from tenant pods to ingress
pods in parent namespaces. This breaks:
- cert-manager HTTP-01 self-check
- Any scenario where pods need to access services exposed through parent
ingress

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

### Release note

```release-note
[tenant] Fixed tenant egress policy to allow traffic to parent ingress pods, enabling cert-manager HTTP-01 challenges and external domain access for nested clusters with exposeMethod: Proxied
```

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

## Summary by CodeRabbit

* **New Features**
* Enhanced network policies for multi-tenant environments with improved
traffic routing based on namespace hierarchies, enabling more granular
egress control.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:59 +01:00
Andrei Kvapil
fe565aff4a [kubevirt-operator] Fix typo in VMNotRunningFor10Minutes alert (#1770)
## What this PR does

Fix case sensitivity in status check for VMNotRunningFor10Minutes alert
rule.

The metric `kubevirt_vm_info` uses `status="Running"` (capital R), but
the alert rule was checking for `status!="running"` (lowercase), causing
false alerts for running VMs.

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

### Release note

```release-note
[kubevirt-operator] Fix VMNotRunningFor10Minutes alert false positives
```

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

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed alert rule condition for virtual machine status detection to
ensure proper matching of running VMs.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:59 +01:00
Andrei Kvapil
29bd12d52d [installer,dx] Rename cozypkg to cozyhr (#1763)
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 PR renames cozypkg to cozyhr
https://github.com/cozystack/cozyhr

### 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
[installer,dx] Rename cozypkg to cozyhr
```
2026-01-09 14:53:59 +01:00
Nikita
12023b7f02 [multus] Increase memory limit (#1773)
## What this PR does
Increases multus memory limit.
Based on multiple community reports stating that multus tend to consume
a lot of memory during startup after a node reboot.

### Release note
```release-note
Multus memory limit increased.
```
2026-01-09 14:53:59 +01:00
Aleksei Sviridkin
f3c178e30d [cilium] Update Cilium to v1.18.5 (#1769)
## What this PR does

- Update Cilium from v1.17.8 to v1.18.5
- Add `SED_INPLACE` variable to `scripts/common-envs.mk` for macOS
compatibility
- Remove deprecated `enableRuntimeDeviceDetection` option (now default
behavior in 1.18)

Cilium 1.18 requires Linux kernel 5.10+ (compatible with Talos).

References:
- [Cilium 1.18 Upgrade
Guide](https://docs.cilium.io/en/stable/operations/upgrade/)
- [Cilium 1.18 Release
Blog](https://isovalent.com/blog/post/cilium-1-18/)

### Release note

```release-note
[cilium] Update Cilium to v1.18.5
```

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

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added configurable Kubernetes service discovery with ConfigMap-based
endpoint source
* Introduced exponential backoff settings for Kubernetes API client
connections
* Added metrics sampling interval configuration for internal agent
metrics
* Implemented identity management mode options for endpoint slices and
DNS proxy pre-allocation
* Enhanced Prometheus scrape timeout configuration across monitoring
components

* **Improvements**
  * Upgraded core components to v1.18.5
* Strengthened security defaults by disabling privilege escalation
across pods
* Added startup and liveness probe configurations for improved health
monitoring
  * Extended network policy correlation capabilities in Hubble

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:59 +01:00
Andrei Kvapil
654eb7c3f9 [cozystack-controller] Fix: move crds to definitions (#1759)
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
[cozystack-controller] Fix: move crds to definitions
```

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

## Summary by CodeRabbit

* **Chores**
* Updated the Custom Resource Definition generation template to source
files from a new location, ensuring consistent configuration management.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:58 +01:00
Timofei Larkin
ffd10e99f1 [platform] Add alphabetical sorting to registry resource lists (#1764)
## What this PR does

Adds deterministic alphabetical sorting to all registry resource lists:
- TenantNamespace (sorted by name)
- TenantSecret (sorted by namespace/name)
- TenantModule (sorted by namespace/name)
- Application (sorted by namespace/name)

Introduces a new `pkg/registry/sorting` package with generic helper
functions
to avoid code duplication.

Also fixes pre-existing linter errors:
- Unused `helmReleaseGVR` variables
- Non-constant format strings in `klog.Errorf` calls
- Redundant embedded field selectors

### Release note

```release-note
[platform] Registry resource lists are now returned in alphabetical order
```

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

* **New Features**
* Added a reusable alphabetical sorting utility and applied consistent
sorting across list endpoints.

* **Bug Fixes**
* Ensured ResourceVersion/UID are sourced correctly for accurate
list/table responses.
* Simplified and improved error logging and table ResourceVersion
handling.
  * Made schema defaulting behavior more consistent.

* **Tests**
* Added unit tests validating alphabetical sorting for multiple resource
types.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:58 +01:00
Andrei Kvapil
f400310f28 [agents] Add instructions for working with unresolved code review comments (#1710)
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
[agents] Add instructions for working with unresolved code review comments
```
2026-01-09 14:53:58 +01:00
Andrei Kvapil
9ab7430cec [workflow] Push tags as cozystack-bot to trigger GitHub workflows
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-09 14:53:58 +01:00
Andrei Kvapil
87fa8c93fc [workflows] Add auto patch release workflow (#1754)
This PR adds a new GitHub workflow that automatically creates patch
releases for maintenance branches.

## What it does

- Runs daily at 2:00 AM CET (1:00 UTC)
- Checks all `release-X.Y` branches
- For each branch, finds the latest published release with tag `vX.Y.Z`
(without suffixes like -rc, -alpha, -beta)
- If new commits exist after the release, creates a new tag `vX.Y.Z+1`
and force-pushes it
- This triggers the existing tag workflow to build and create a release
PR

## Why releases instead of tags

The workflow checks published releases (not just tags) because if a
release PR hasn't been merged yet, the workflow should run again the
next day and move the tag to newer commits if they exist.

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

## Summary by CodeRabbit

* **Chores**
* Automated patch release workflow configured for release branches,
enabling automatic version tagging when new commits are detected.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:58 +01:00
Andrei Kvapil
d1c0af1db4 [workflow] Add GitHub Action to update release notes from changelogs (#1752)
This PR adds a GitHub Actions workflow that automatically updates
release notes from changelog files when changes are merged to main.

The workflow:
- Triggers on push to main branch
- Processes up to 30 releases from the first page
- For each release, checks if a corresponding changelog file exists in
`docs/changelogs/`
- Updates the release notes with the changelog content if it exists and
differs from the current content
- Skips releases that are already up to date to avoid unnecessary API
calls

This automates the process of keeping release notes synchronized with
changelog files.

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

* **Chores**
* Automated workflow added to synchronize GitHub releases with local
changelog files, updating release notes only when content differs.

* **Documentation**
* Changelogs and release notes reorganized and expanded with improved
sectioning and new tooling/documentation entries.

* **New Features**
  * Added VM disk SVG icon entry under Features/Improvements.

* **Bug Fixes**
  * Pinned CoreDNS image tag for reproducible deployments.
* Fixed a documentation typo that prevented a component from displaying
in the web UI.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:57 +01:00
Andrei Kvapil
eb042b4ef1 [core] Extract Talos package from installer (#1724)
## What this PR does

Extract Talos-related functionality from `packages/core/installer` into
a separate `packages/core/talos` package.

This refactoring separates concerns:
- The `installer` package now focuses solely on the Cozystack installer
- The `talos` package handles all Talos Linux image building and assets
generation

Changes:
- Created new `packages/core/talos` package with Chart.yaml, Makefile,
and values.yaml
- Moved Talos profiles (initramfs, kernel, iso, installer, metal,
nocloud) to talos package
- Moved matchbox configuration and Dockerfile to talos package
- Moved `hack/gen-profiles.sh` and `hack/gen-versions.sh` scripts to
talos package
- Updated installer Makefile to remove all Talos-related targets
- Updated root Makefile to build talos package separately
- Updated matchbox Dockerfile paths to reference talos package

### Release note

```release-note
[core] Extract Talos package from installer into separate packages/core/talos package
```



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

* **Chores**
* Reorganized build system to add a dedicated Talos package with its own
image and asset build workflow
* Switched asset generation to the new Talos packaging path and
simplified build dependency chain
* Added Helm chart manifest and streamlined image build/publishing steps
for Talos-related artifacts

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:57 +01:00
Andrei Kvapil
ecb9a223aa Add changelogs to v.0.39.1 (#1750)
<!-- 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
Add changelogs for v.0.39.0 and v.0.39.1
```

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

## Summary by CodeRabbit

* **New Features**
  * Topology-aware routing for services
  * Automatic pod rollouts on configuration changes
  * Windows VM scheduling support
  * SLACK_SEVERITY_FILTER for notification filtering
  * VMAgent resource for metrics scraping

* **Improvements**
  * Enhanced networking and storage capabilities
  * Configuration management enhancements
  * Updated dependencies

* **Bug Fixes**
  * Schema nesting and resizing validation
  * Namespace deletion regression

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:57 +01:00
Andrei Kvapil
77de2bdda0 Update go modules (#1736)
This change is extracted from
- https://github.com/cozystack/cozystack/pull/1641

and reworked to work standalone

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

<!--  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
[cozystack] Update go modules
```

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

* **Chores**
* Updated Go toolchain to 1.25.0 and upgraded core Kubernetes libraries,
OpenTelemetry, Prometheus, gRPC/protobuf and many indirect dependencies.
Bumped builder base images to golang:1.25-alpine across multiple
components.

* **Refactor**
* Removed legacy component versioning/emulation and simplified server
startup and configuration paths.

* **Tests**
  * Removed tests related to the legacy versioning/emulation behavior.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:53:08 +01:00
Timofei Larkin
b3179d032a [vm] Always expose VMs with a service (#1738)
## What this PR does

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

### Release note

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

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

* **Improvements**
* Service templates now conditionally enable external-specific
annotations and settings when external mode is enabled.
* External LoadBalancer deployments support richer port configuration
(including whole-IP fallback), while internal services retain a single
default port (65535).
* External traffic policy and node port allocation are applied only in
external mode to preserve internal behavior.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:52:42 +01:00
Andrei Kvapil
a939b2bbd0 [registry] Add application labels and update filtering mechanism (#1707)
This change is extracted from
- https://github.com/cozystack/cozystack/pull/1641

and reworked to work standalone

## What this PR does

This PR extracts changes from the
https://github.com/cozystack/cozystack/pull/1641. It adds application
metadata labels to HelmReleases and updates the filtering mechanism to
use labels instead of chart/sourceRef matching.

Changes:
- Add three application metadata labels
(`apps.cozystack.io/application.kind`,
`apps.cozystack.io/application.group`,
`apps.cozystack.io/application.name`) when creating/updating HelmRelease
via Cozystack-API
- Replace `shouldIncludeHelmRelease` filtering with label-based
filtering in Get, List, and Update methods
- Always add kind and group label requirements in List for precise
filtering
- Update CozystackResourceDefinitionController to watch only
HelmReleases with `cozystack.io/ui=true` label
- Update LineageControllerWebhook to extract metadata directly from
HelmRelease labels instead of using chart mapping configuration
- Add functionality to update HelmRelease chart from
CozystackResourceDefinition using label selectors

### Release note

```release-note
[registry] Add application labels and update filtering mechanism to use label-based filtering instead of chart/sourceRef matching
```

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

* **New Features**
* Added application metadata labels (kind, group, name) and exported
label keys for HelmRelease identification.
* New reconciler keeps HelmRelease charts in sync with application CRDs.

* **Refactor**
* Mapping, listing and selection moved to label-driven logic;
reconciliation responsibilities split so core reconciler focuses on
restart/debounce while a separate reconciler updates HelmReleases.

* **Chores**
* Migration script to backfill application labels on existing
HelmReleases.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:52:42 +01:00
Nikita
682f83e71c [system/cilium] Enable topology-aware routing for services (#1734) 2026-01-09 14:52:41 +01:00
Timofei Larkin
66cfbe7c0e Add changelogs for v0.38.3 and v.0.38.4 (#1743) 2026-01-09 14:52:41 +01:00
Nikita
67109c33b1 [cilium] Enable automatic pod rollout on configmap updates (#1728)
Enable automatic restart of cilium and cilium-operator pods when
cilium-config ConfigMap is updated.

This change adds:
- `rollOutCiliumPods: true` - enables automatic rollout of cilium-agent
pods
- `operator.rollOutPods: true` - enables automatic rollout of
cilium-operator pods

When the ConfigMap is updated, pods will automatically restart due to
the `cilium.io/cilium-configmap-checksum` annotation that contains the
SHA256 hash of the configmap.

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

## Summary by CodeRabbit

* **Chores**
* Updated Cilium configuration settings to enable pod rollout behavior
for Cilium and operator pods by default.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:52:41 +01:00
Nikita
f9bfcfd125 [virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config (#1693)
This PR adds nodeAffinity configuration to virtual-machine and
vm-instance charts to support dedicated nodes for Windows VMs.

## Changes
- Added logic to check `cozystack-scheduling` ConfigMap in `cozy-system`
namespace
- If `dedicatedNodesForWindowsVMs` is enabled, adds appropriate
nodeAffinity:
- **Windows VMs**: Strong affinity (required) to nodes with label
`scheduling.cozystack.io/vm-windows=true`
- **Non-Windows VMs**: Soft affinity (preferred) to nodes without the
Windows label

## Implementation
- Windows detection based on `instanceProfile` value starting with
"windows"
- Strong affinity uses `requiredDuringSchedulingIgnoredDuringExecution`
- Soft affinity uses `preferredDuringSchedulingIgnoredDuringExecution`
with weight 100

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

* **New Features**
* Added configurable node-affinity for VM scheduling: when the cluster
scheduling flag is enabled, Windows VMs are placed on dedicated Windows
nodes (required rule), while non-Windows VMs are preferred to avoid
those nodes (soft preference).
* Change is gated by the cluster scheduling configuration and only
affects placement rules; no other VM specs were altered.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:52:41 +01:00
Andrei Kvapil
1beb11a6a9 Add Cloupard to ADOPTERS.md (#1733)
<!-- 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 PR adds Cloupard as new adopter to the list.

### 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
Add Cloupard to ADOPTERS.md
```

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

## Summary by CodeRabbit

* **Documentation**
  * Updated the list of project adopters with recent entries.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:52:41 +01:00
Nikita
f7c6a54b0c Update SeaweedFS v4.02 (#1725)
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 PR includes new seaweedfs with improved perfomance for S3 daemon

and fixes issue https://github.com/seaweedfs/seaweedfs/issues/7757

### 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
Update SeaweedFS v4.02
```

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

## Summary by CodeRabbit

* **New Features**
* Added all-in-one deployment mode with configurable replicas and update
strategy
* Expanded storage configuration supporting PersistentVolumeClaims with
customizable access modes and size
  * Introduced configurable certificate duration and renewal periods
* Enhanced monitoring configuration with gateway host/port and
additional labels

* **Bug Fixes**
  * Fixed probe endpoint scheme references across components

* **Chores**
  * Updated to SeaweedFS 4.02
  * Updated default ingress class configuration
  * S3 disabled by default

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:52:40 +01:00
Andrei Kvapil
35c57798a6 [monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant monitoring (#1712)
[monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant
monitoring
What this PR does
This PR introduces the SLACK_SEVERITY_FILTER environment variable in the
Alerta deployment to enable
filtering of alert severities for Slack notifications based on the
disabledSeverity configuration.
Additionally, it adds a VMAgent resource template for scraping metrics
within tenant namespaces, improving
monitoring granularity and control.

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

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

## Summary by CodeRabbit

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

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

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:52:40 +01:00
Andrei Kvapil
aba147571b [fluxcd] Add flux-aio module and migration (#1698)
This change is extracted from
- https://github.com/cozystack/cozystack/pull/1641

and reworked to work standalone

requires:

- https://github.com/cozystack/cozystack/pull/1705


## What this PR does

Adds a new `flux-aio` module and migration script to upgrade FluxCD to
version 22. This introduces a new modular approach to FluxCD
installation using the flux-aio OCI module.

Changes:
- Created new `flux-aio` package with Chart.yaml, Makefile, and CUE
configuration
- Added flux-aio module configuration using OCI module from
`ghcr.io/stefanprodan/modules/flux-aio`
- Generated large fluxcd.yaml template (11956+ lines) for FluxCD
resources
- Added migration script (migrations/21) to handle upgrade from version
21 to 22
- Updated installer to include flux-aio module
- Added script `issue-flux-certificates.sh` for managing TLS
certificates for cozystack-assets
- Updated platform templates to support flux-aio module
- Updated cozystack-assets service references

### Release note

```release-note
[fluxcd] Add flux-aio module and migration
```



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

## Release Notes

* **New Features**
* Added TLS certificate support for Helm package repositories with
automatic certificate provisioning.

* **Chores**
  * Refactored FluxCD integration using Helm chart-based deployment.
  * Updated system to version 22 with automatic migration support.
  * Enhanced security dependencies (OpenSSL).

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:52:40 +01:00
Andrei Kvapil
17138d825d [fluxcd] Enable source-watcher (#1706)
This change is extracted from
- https://github.com/cozystack/cozystack/pull/1641

and reworked to work standalone

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
[fluxcd] Enable source-watcher
```
2026-01-09 14:52:40 +01:00
Andrei Kvapil
f5eb211312 [platform] Separate assets server into dedicated deployment (#1705)
## What this PR does

Separates the assets server from the main cozystack installer into a
dedicated StatefulSet deployment. This improves separation of concerns
and allows the assets server to run independently from the installer.

Changes:
- Created new `cozystack-assets` StatefulSet in the platform package
- Added dedicated Dockerfile for assets server image
(`packages/core/platform/images/cozystack-assets/Dockerfile`)
- Removed assets server container and Service from installer deployment
- Updated HelmRepository URLs to point to new `cozystack-assets` service
- Updated dashboard URLs in monitoring package to use new service
- Added image build target to platform Makefile
- Configured assets server with hostNetwork and proper RBAC permissions

### Release note

```release-note
[platform] Separate assets server into dedicated StatefulSet deployment
```
2026-01-09 14:52:40 +01:00
Andrei Kvapil
c44b198a60 [apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK (#1679)
## What this PR does

This PR refactors the apiserver REST handlers to use typed objects
(`appsv1alpha1.Application`) instead of `unstructured.Unstructured`,
eliminating
the need for runtime conversions and simplifying the codebase.

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

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

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

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

### Release note

```release-note
[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK handling

This change refactors the apiserver REST handlers to use typed Application objects instead of unstructured.Unstructured, eliminating runtime conversions and simplifying the codebase. Additionally, it fixes an issue where UnstructuredList objects were incorrectly using the first registered kind from typeToGVK instead of the kind from the object's field when multiple kinds are registered with the same Go type. This fix includes the upstream fix from kubernetes/kubernetes#135537.
```



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

* **Chores**
* Added a module replacement in dependency configuration to ensure
reproducible builds.

* **Refactor**
* REST internals now use strongly-typed Application and TenantModule
resources and return typed API objects with consistent metadata.

* **Tests**
* Strengthened end-to-end checks for resource kinds and added a
create/delete namespace scenario.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-09 14:52:39 +01:00
Andrei Kvapil
9b5f3726b6 [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 -->
2026-01-09 14:52:39 +01:00
Andrei Kvapil
66b68e3798 [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 -->
2026-01-09 14:52:39 +01:00
Andrei Kvapil
bd443bd578 [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
```
2026-01-09 14:52:39 +01:00
Andrei Kvapil
2c37611136 [ci] Update korthout/backport-action@v3.2.1
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-09 14:52:39 +01:00
Andrei Kvapil
a4a432e2aa [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 -->
2026-01-09 14:52:38 +01:00
Andrei Kvapil
da6c053d3f 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 -->
2026-01-09 14:52:37 +01:00
299 changed files with 5928 additions and 33265 deletions

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/linstor image
make -C packages/system/kubeovn-webhook image
make -C packages/system/kubeovn-plunger image
make -C packages/system/dashboard image

1
api/.gitattributes vendored
View File

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

View File

@@ -1,37 +0,0 @@
/*
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

@@ -1,63 +0,0 @@
// 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

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

View File

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

View File

@@ -1,421 +0,0 @@
# 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

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -92,8 +92,7 @@ type CozystackResourceDefinitionApplication struct {
type CozystackResourceDefinitionRelease struct {
// Helm chart configuration
// +optional
Chart CozystackResourceDefinitionChart `json:"chart,omitempty"`
Chart CozystackResourceDefinitionChart `json:"chart"`
// Labels for the release
Labels map[string]string `json:"labels,omitempty"`
// Prefix for the release name
@@ -111,18 +110,17 @@ type CozystackResourceDefinitionRelease struct {
// - {{ .namespace }}: The namespace of the resource being processed
//
// Example YAML:
//
// secrets:
// include:
// - matchExpressions:
// - key: badlabel
// operator: DoesNotExist
// matchLabels:
// goodlabel: goodvalue
// resourceNames:
// - "{{ .name }}-secret"
// - "{{ .kind }}-{{ .name }}-tls"
// - "specificname"
// secrets:
// include:
// - matchExpressions:
// - key: badlabel
// operator: DoesNotExist
// matchLabels:
// goodlabel: goodvalue
// resourceNames:
// - "{{ .name }}-secret"
// - "{{ .kind }}-{{ .name }}-tls"
// - "specificname"
type CozystackResourceDefinitionResourceSelector struct {
metav1.LabelSelector `json:",inline"`
// ResourceNames is a list of resource names to match

View File

@@ -1,100 +0,0 @@
/*
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
import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster,shortName={pkg,pkgs}
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Variant",type="string",JSONPath=".spec.variant",description="Selected variant"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message",description="Ready message"
// Package is the Schema for the packages API
type Package struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PackageSpec `json:"spec,omitempty"`
Status PackageStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// PackageList contains a list of Packages
type PackageList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Package `json:"items"`
}
func init() {
SchemeBuilder.Register(&Package{}, &PackageList{})
}
// PackageSpec defines the desired state of Package
type PackageSpec struct {
// Variant is the name of the variant to use from the PackageSource
// If not specified, defaults to "default"
// +optional
Variant string `json:"variant,omitempty"`
// IgnoreDependencies is a list of package source dependencies to ignore
// Dependencies listed here will not be installed even if they are specified in the PackageSource
// +optional
IgnoreDependencies []string `json:"ignoreDependencies,omitempty"`
// Components is a map of release name to component overrides
// Allows overriding values and enabling/disabling specific components from the PackageSource
// +optional
Components map[string]PackageComponent `json:"components,omitempty"`
}
// PackageComponent defines overrides for a specific component
type PackageComponent struct {
// Enabled indicates whether this component should be installed
// If false, the component will be disabled even if it's defined in the PackageSource
// +optional
Enabled *bool `json:"enabled,omitempty"`
// Values contains Helm chart values as a JSON object
// These values will be merged with the default values from the PackageSource
// +optional
Values *apiextensionsv1.JSON `json:"values,omitempty"`
}
// PackageStatus defines the observed state of Package
type PackageStatus struct {
// Conditions represents the latest available observations of a Package's state
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Dependencies tracks the readiness status of each dependency
// Key is the dependency package name, value indicates if the dependency is ready
// +optional
Dependencies map[string]DependencyStatus `json:"dependencies,omitempty"`
}
// DependencyStatus represents the readiness status of a dependency
type DependencyStatus struct {
// Ready indicates whether the dependency is ready
Ready bool `json:"ready"`
}

View File

@@ -1,171 +0,0 @@
/*
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
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster,shortName={pks}
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Variants",type="string",JSONPath=".status.variants",description="Package variants (comma-separated)"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message",description="Ready message"
// PackageSource is the Schema for the packagesources API
type PackageSource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PackageSourceSpec `json:"spec,omitempty"`
Status PackageSourceStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// PackageSourceList contains a list of PackageSources
type PackageSourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []PackageSource `json:"items"`
}
func init() {
SchemeBuilder.Register(&PackageSource{}, &PackageSourceList{})
}
// PackageSourceSpec defines the desired state of PackageSource
type PackageSourceSpec struct {
// SourceRef is the source reference for the package source charts
// +optional
SourceRef *PackageSourceRef `json:"sourceRef,omitempty"`
// Variants is a list of package source variants
// Each variant defines components, applications, dependencies, and libraries for a specific configuration
// +optional
Variants []Variant `json:"variants,omitempty"`
}
// Variant defines a single variant configuration
type Variant struct {
// Name is the unique identifier for this variant
// +required
Name string `json:"name"`
// DependsOn is a list of package source dependencies
// For example: "cozystack.networking"
// +optional
DependsOn []string `json:"dependsOn,omitempty"`
// Libraries is a list of Helm library charts used by components in this variant
// +optional
Libraries []Library `json:"libraries,omitempty"`
// Components is a list of Helm releases to be installed as part of this variant
// +optional
Components []Component `json:"components,omitempty"`
}
// Library defines a Helm library chart
type Library struct {
// Name is the optional name for library placed in charts
// +optional
Name string `json:"name,omitempty"`
// Path is the path to the library chart directory
// +required
Path string `json:"path"`
}
// PackageSourceRef defines the source reference for package source charts
type PackageSourceRef struct {
// Kind of the source reference
// +kubebuilder:validation:Enum=GitRepository;OCIRepository
// +required
Kind string `json:"kind"`
// Name of the source reference
// +required
Name string `json:"name"`
// Namespace of the source reference
// +required
Namespace string `json:"namespace"`
// Path is the base path where packages are located in the source.
// For GitRepository, defaults to "packages" if not specified.
// For OCIRepository, defaults to empty string (root) if not specified.
// +optional
Path string `json:"path,omitempty"`
}
// ComponentInstall defines installation parameters for a component
type ComponentInstall struct {
// ReleaseName is the name of the HelmRelease resource that will be created
// If not specified, defaults to the component Name field
// +optional
ReleaseName string `json:"releaseName,omitempty"`
// Namespace is the Kubernetes namespace where the release will be installed
// +optional
Namespace string `json:"namespace,omitempty"`
// Privileged indicates whether this release requires privileged access
// +optional
Privileged bool `json:"privileged,omitempty"`
// DependsOn is a list of component names that must be installed before this component
// +optional
DependsOn []string `json:"dependsOn,omitempty"`
}
// Component defines a single Helm release component within a package source
type Component struct {
// Name is the unique identifier for this component within the package source
// +required
Name string `json:"name"`
// Path is the path to the Helm chart directory
// +required
Path string `json:"path"`
// Install defines installation parameters for this component
// +optional
Install *ComponentInstall `json:"install,omitempty"`
// Libraries is a list of library names that this component depends on
// These libraries must be defined at the variant level
// +optional
Libraries []string `json:"libraries,omitempty"`
// ValuesFiles is a list of values file names to use
// +optional
ValuesFiles []string `json:"valuesFiles,omitempty"`
}
// PackageSourceStatus defines the observed state of PackageSource
type PackageSourceStatus struct {
// Variants is a comma-separated list of package variant names
// This field is populated by the controller based on spec.variants keys
// +optional
Variants string `json:"variants,omitempty"`
// Conditions represents the latest available observations of a PackageSource's state
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

View File

@@ -21,62 +21,10 @@ limitations under the License.
package v1alpha1
import (
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Component) DeepCopyInto(out *Component) {
*out = *in
if in.Install != nil {
in, out := &in.Install, &out.Install
*out = new(ComponentInstall)
(*in).DeepCopyInto(*out)
}
if in.Libraries != nil {
in, out := &in.Libraries, &out.Libraries
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ValuesFiles != nil {
in, out := &in.ValuesFiles, &out.ValuesFiles
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Component.
func (in *Component) DeepCopy() *Component {
if in == nil {
return nil
}
out := new(Component)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ComponentInstall) DeepCopyInto(out *ComponentInstall) {
*out = *in
if in.DependsOn != nil {
in, out := &in.DependsOn, &out.DependsOn
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentInstall.
func (in *ComponentInstall) DeepCopy() *ComponentInstall {
if in == nil {
return nil
}
out := new(ComponentInstall)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CozystackResourceDefinition) DeepCopyInto(out *CozystackResourceDefinition) {
*out = *in
@@ -308,299 +256,6 @@ func (in *CozystackResourceDefinitionSpec) DeepCopy() *CozystackResourceDefiniti
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DependencyStatus) DeepCopyInto(out *DependencyStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyStatus.
func (in *DependencyStatus) DeepCopy() *DependencyStatus {
if in == nil {
return nil
}
out := new(DependencyStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Library) DeepCopyInto(out *Library) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Library.
func (in *Library) DeepCopy() *Library {
if in == nil {
return nil
}
out := new(Library)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Package) DeepCopyInto(out *Package) {
*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 Package.
func (in *Package) DeepCopy() *Package {
if in == nil {
return nil
}
out := new(Package)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Package) 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 *PackageComponent) DeepCopyInto(out *PackageComponent) {
*out = *in
if in.Enabled != nil {
in, out := &in.Enabled, &out.Enabled
*out = new(bool)
**out = **in
}
if in.Values != nil {
in, out := &in.Values, &out.Values
*out = new(v1.JSON)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageComponent.
func (in *PackageComponent) DeepCopy() *PackageComponent {
if in == nil {
return nil
}
out := new(PackageComponent)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageList) DeepCopyInto(out *PackageList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Package, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageList.
func (in *PackageList) DeepCopy() *PackageList {
if in == nil {
return nil
}
out := new(PackageList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PackageList) 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 *PackageSource) DeepCopyInto(out *PackageSource) {
*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 PackageSource.
func (in *PackageSource) DeepCopy() *PackageSource {
if in == nil {
return nil
}
out := new(PackageSource)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PackageSource) 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 *PackageSourceList) DeepCopyInto(out *PackageSourceList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]PackageSource, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceList.
func (in *PackageSourceList) DeepCopy() *PackageSourceList {
if in == nil {
return nil
}
out := new(PackageSourceList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PackageSourceList) 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 *PackageSourceRef) DeepCopyInto(out *PackageSourceRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceRef.
func (in *PackageSourceRef) DeepCopy() *PackageSourceRef {
if in == nil {
return nil
}
out := new(PackageSourceRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageSourceSpec) DeepCopyInto(out *PackageSourceSpec) {
*out = *in
if in.SourceRef != nil {
in, out := &in.SourceRef, &out.SourceRef
*out = new(PackageSourceRef)
**out = **in
}
if in.Variants != nil {
in, out := &in.Variants, &out.Variants
*out = make([]Variant, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceSpec.
func (in *PackageSourceSpec) DeepCopy() *PackageSourceSpec {
if in == nil {
return nil
}
out := new(PackageSourceSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageSourceStatus) DeepCopyInto(out *PackageSourceStatus) {
*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 PackageSourceStatus.
func (in *PackageSourceStatus) DeepCopy() *PackageSourceStatus {
if in == nil {
return nil
}
out := new(PackageSourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageSpec) DeepCopyInto(out *PackageSpec) {
*out = *in
if in.IgnoreDependencies != nil {
in, out := &in.IgnoreDependencies, &out.IgnoreDependencies
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Components != nil {
in, out := &in.Components, &out.Components
*out = make(map[string]PackageComponent, len(*in))
for key, val := range *in {
(*out)[key] = *val.DeepCopy()
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSpec.
func (in *PackageSpec) DeepCopy() *PackageSpec {
if in == nil {
return nil
}
out := new(PackageSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageStatus) DeepCopyInto(out *PackageStatus) {
*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])
}
}
if in.Dependencies != nil {
in, out := &in.Dependencies, &out.Dependencies
*out = make(map[string]DependencyStatus, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageStatus.
func (in *PackageStatus) DeepCopy() *PackageStatus {
if in == nil {
return nil
}
out := new(PackageStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in Selector) DeepCopyInto(out *Selector) {
{
@@ -637,38 +292,6 @@ func (in *SourceRef) DeepCopy() *SourceRef {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Variant) DeepCopyInto(out *Variant) {
*out = *in
if in.DependsOn != nil {
in, out := &in.DependsOn, &out.DependsOn
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Libraries != nil {
in, out := &in.Libraries, &out.Libraries
*out = make([]Library, len(*in))
copy(*out, *in)
}
if in.Components != nil {
in, out := &in.Components, &out.Components
*out = make([]Component, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Variant.
func (in *Variant) DeepCopy() *Variant {
if in == nil {
return nil
}
out := new(Variant)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Workload) DeepCopyInto(out *Workload) {
*out = *in

View File

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

@@ -1,174 +0,0 @@
/*
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.BackupJobReconciler{
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)
}
}

View File

@@ -1,549 +0,0 @@
/*
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.
*/
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/spf13/cobra"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
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/serializer/yaml"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
_ "k8s.io/client-go/plugin/pkg/client/auth"
)
var addCmdFlags struct {
files []string
kubeconfig string
}
var addCmd = &cobra.Command{
Use: "add [package]...",
Short: "Install PackageSource and its dependencies interactively",
Long: `Install PackageSource and its dependencies interactively.
You can specify packages as arguments or use -f flag to read from files.
Multiple -f flags can be specified, and they can point to files or directories.`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Collect package names from arguments and files
packageNames := make(map[string]bool)
packagesFromFiles := make(map[string]string) // packageName -> filePath
for _, arg := range args {
packageNames[arg] = true
}
// Read packages from files
for _, filePath := range addCmdFlags.files {
packages, err := readPackagesFromFile(filePath)
if err != nil {
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
}
for _, pkg := range packages {
packageNames[pkg] = true
if oldPath, ok := packagesFromFiles[pkg]; ok {
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
}
packagesFromFiles[pkg] = filePath
}
}
if len(packageNames) == 0 {
return fmt.Errorf("no packages specified")
}
// Create Kubernetes client config
var config *rest.Config
var err error
if addCmdFlags.kubeconfig != "" {
config, err = clientcmd.BuildConfigFromFlags("", addCmdFlags.kubeconfig)
if err != nil {
return fmt.Errorf("failed to load kubeconfig from %s: %w", addCmdFlags.kubeconfig, err)
}
} else {
config, err = ctrl.GetConfig()
if err != nil {
return fmt.Errorf("failed to get kubeconfig: %w", err)
}
}
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to create k8s client: %w", err)
}
// Process each package
for packageName := range packageNames {
// Check if package comes from a file
if filePath, fromFile := packagesFromFiles[packageName]; fromFile {
// Try to create Package directly from file
if err := createPackageFromFile(ctx, k8sClient, filePath, packageName); err == nil {
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", packageName)
continue
}
// If failed, fall back to interactive installation
}
// Interactive installation from PackageSource
if err := installPackage(ctx, k8sClient, packageName); err != nil {
return err
}
}
return nil
},
}
func readPackagesFromFile(filePath string) ([]string, error) {
info, err := os.Stat(filePath)
if err != nil {
return nil, err
}
var packages []string
if info.IsDir() {
// Read all YAML files from directory
err := filepath.Walk(filePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || !strings.HasSuffix(path, ".yaml") && !strings.HasSuffix(path, ".yml") {
return nil
}
pkgs, err := readPackagesFromYAMLFile(path)
if err != nil {
return fmt.Errorf("failed to read %s: %w", path, err)
}
packages = append(packages, pkgs...)
return nil
})
if err != nil {
return nil, err
}
} else {
packages, err = readPackagesFromYAMLFile(filePath)
if err != nil {
return nil, err
}
}
return packages, nil
}
func readPackagesFromYAMLFile(filePath string) ([]string, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
var packages []string
// Split YAML documents (in case of multiple resources)
documents := strings.Split(string(data), "---")
for _, doc := range documents {
doc = strings.TrimSpace(doc)
if doc == "" {
continue
}
// Parse using Kubernetes decoder
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
obj := &unstructured.Unstructured{}
_, _, err := decoder.Decode([]byte(doc), nil, obj)
if err != nil {
continue
}
// Check if it's a Package
if obj.GetKind() == "Package" {
name := obj.GetName()
if name != "" {
packages = append(packages, name)
}
continue
}
// Check if it's a PackageSource
if obj.GetKind() == "PackageSource" {
name := obj.GetName()
if name != "" {
packages = append(packages, name)
}
continue
}
// Try to parse as PackageList or PackageSourceList
if obj.GetKind() == "PackageList" || obj.GetKind() == "PackageSourceList" {
items, found, err := unstructured.NestedSlice(obj.Object, "items")
if err == nil && found {
for _, item := range items {
if itemMap, ok := item.(map[string]interface{}); ok {
if metadata, ok := itemMap["metadata"].(map[string]interface{}); ok {
if name, ok := metadata["name"].(string); ok && name != "" {
packages = append(packages, name)
}
}
}
}
}
continue
}
}
// Return empty list if no packages found - don't error out
// The check for whether any packages were specified at all is handled later in RunE
return packages, nil
}
// buildDependencyTree builds a dependency tree starting from the root PackageSource
// Returns both the dependency tree and a map of dependencies to their requesters
func buildDependencyTree(ctx context.Context, k8sClient client.Client, rootName string) (map[string][]string, map[string]string, error) {
tree := make(map[string][]string)
dependencyRequesters := make(map[string]string) // dep -> requester
visited := make(map[string]bool)
// Ensure root is in tree even if it has no dependencies
tree[rootName] = []string{}
var buildTree func(string) error
buildTree = func(pkgName string) error {
if visited[pkgName] {
return nil
}
visited[pkgName] = true
// Get PackageSource
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(ctx, client.ObjectKey{Name: pkgName}, ps); err != nil {
// If PackageSource doesn't exist, just skip it
return nil
}
// Collect all dependencies from all variants
deps := make(map[string]bool)
for _, variant := range ps.Spec.Variants {
for _, dep := range variant.DependsOn {
deps[dep] = true
}
}
// Add dependencies to tree
for dep := range deps {
if _, exists := tree[pkgName]; !exists {
tree[pkgName] = []string{}
}
tree[pkgName] = append(tree[pkgName], dep)
// Track who requested this dependency
dependencyRequesters[dep] = pkgName
// Recursively build tree for dependencies
if err := buildTree(dep); err != nil {
return err
}
}
return nil
}
if err := buildTree(rootName); err != nil {
return nil, nil, err
}
return tree, dependencyRequesters, nil
}
// topologicalSort performs topological sort on the dependency tree
// Returns order from root to leaves (dependencies first)
func topologicalSort(tree map[string][]string) ([]string, error) {
// Build reverse graph (dependencies -> dependents)
reverseGraph := make(map[string][]string)
allNodes := make(map[string]bool)
for node, deps := range tree {
allNodes[node] = true
for _, dep := range deps {
allNodes[dep] = true
reverseGraph[dep] = append(reverseGraph[dep], node)
}
}
// Calculate in-degrees (how many dependencies a node has)
inDegree := make(map[string]int)
for node := range allNodes {
inDegree[node] = 0
}
for node, deps := range tree {
inDegree[node] = len(deps)
}
// Kahn's algorithm - start with nodes that have no dependencies
var queue []string
for node, degree := range inDegree {
if degree == 0 {
queue = append(queue, node)
}
}
var result []string
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
// Process dependents (nodes that depend on this node)
for _, dependent := range reverseGraph[node] {
inDegree[dependent]--
if inDegree[dependent] == 0 {
queue = append(queue, dependent)
}
}
}
// Check for cycles
if len(result) != len(allNodes) {
return nil, fmt.Errorf("dependency cycle detected")
}
return result, nil
}
// createPackageFromFile creates a Package resource directly from a YAML file
func createPackageFromFile(ctx context.Context, k8sClient client.Client, filePath string, packageName string) error {
data, err := os.ReadFile(filePath)
if err != nil {
return err
}
// Split YAML documents
documents := strings.Split(string(data), "---")
for _, doc := range documents {
doc = strings.TrimSpace(doc)
if doc == "" {
continue
}
// Parse using Kubernetes decoder
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
obj := &unstructured.Unstructured{}
_, _, err := decoder.Decode([]byte(doc), nil, obj)
if err != nil {
continue
}
// Check if it's a Package with matching name
if obj.GetKind() == "Package" && obj.GetName() == packageName {
// Convert to Package
var pkg cozyv1alpha1.Package
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &pkg); err != nil {
return fmt.Errorf("failed to convert Package: %w", err)
}
// Create Package
if err := k8sClient.Create(ctx, &pkg); err != nil {
return fmt.Errorf("failed to create Package: %w", err)
}
return nil
}
}
return fmt.Errorf("Package %s not found in file", packageName)
}
func installPackage(ctx context.Context, k8sClient client.Client, packageSourceName string) error {
// Get PackageSource
packageSource := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(ctx, client.ObjectKey{Name: packageSourceName}, packageSource); err != nil {
return fmt.Errorf("failed to get PackageSource %s: %w", packageSourceName, err)
}
// Build dependency tree
dependencyTree, dependencyRequesters, err := buildDependencyTree(ctx, k8sClient, packageSourceName)
if err != nil {
return fmt.Errorf("failed to build dependency tree: %w", err)
}
// Topological sort (install from root to leaves)
installOrder, err := topologicalSort(dependencyTree)
if err != nil {
return fmt.Errorf("failed to sort dependencies: %w", err)
}
// Get all PackageSources for variant selection
var allPackageSources cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &allPackageSources); err != nil {
return fmt.Errorf("failed to list PackageSources: %w", err)
}
packageSourceMap := make(map[string]*cozyv1alpha1.PackageSource)
for i := range allPackageSources.Items {
packageSourceMap[allPackageSources.Items[i].Name] = &allPackageSources.Items[i]
}
// Get all installed Packages
var installedPackages cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &installedPackages); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
installedMap := make(map[string]*cozyv1alpha1.Package)
for i := range installedPackages.Items {
installedMap[installedPackages.Items[i].Name] = &installedPackages.Items[i]
}
// First, collect all variant selections
fmt.Fprintf(os.Stderr, "Installing %s and its dependencies...\n\n", packageSourceName)
packageVariants := make(map[string]string) // packageName -> variant
for _, pkgName := range installOrder {
// Check if already installed
if installed, exists := installedMap[pkgName]; exists {
variant := installed.Spec.Variant
if variant == "" {
variant = "default"
}
fmt.Fprintf(os.Stderr, "✓ %s (already installed, variant: %s)\n", pkgName, variant)
packageVariants[pkgName] = variant
continue
}
// Get PackageSource for this dependency
ps, exists := packageSourceMap[pkgName]
if !exists {
requester := dependencyRequesters[pkgName]
if requester != "" {
return fmt.Errorf("PackageSource %s not found (required by %s)", pkgName, requester)
}
return fmt.Errorf("PackageSource %s not found", pkgName)
}
// Select variant interactively
variant, err := selectVariantInteractive(ps)
if err != nil {
return fmt.Errorf("failed to select variant for %s: %w", pkgName, err)
}
packageVariants[pkgName] = variant
}
// Now create all Package resources
for _, pkgName := range installOrder {
// Skip if already installed
if _, exists := installedMap[pkgName]; exists {
continue
}
variant := packageVariants[pkgName]
// Create Package
pkg := &cozyv1alpha1.Package{
ObjectMeta: metav1.ObjectMeta{
Name: pkgName,
},
Spec: cozyv1alpha1.PackageSpec{
Variant: variant,
},
}
if err := k8sClient.Create(ctx, pkg); err != nil {
return fmt.Errorf("failed to create Package %s: %w", pkgName, err)
}
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", pkgName)
}
return nil
}
// selectVariantInteractive prompts user to select a variant
func selectVariantInteractive(ps *cozyv1alpha1.PackageSource) (string, error) {
if len(ps.Spec.Variants) == 0 {
return "", fmt.Errorf("no variants available for PackageSource %s", ps.Name)
}
reader := bufio.NewReader(os.Stdin)
fmt.Fprintf(os.Stderr, "\nPackageSource: %s\n", ps.Name)
fmt.Fprintf(os.Stderr, "Available variants:\n")
for i, variant := range ps.Spec.Variants {
fmt.Fprintf(os.Stderr, " %d. %s\n", i+1, variant.Name)
}
// If only one variant, use it as default
defaultVariant := ps.Spec.Variants[0].Name
var prompt string
if len(ps.Spec.Variants) == 1 {
prompt = "Select variant [1]: "
} else {
prompt = fmt.Sprintf("Select variant (1-%d): ", len(ps.Spec.Variants))
}
for {
fmt.Fprintf(os.Stderr, prompt)
input, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("failed to read input: %w", err)
}
input = strings.TrimSpace(input)
// If input is empty and there's a default variant, use it
if input == "" && len(ps.Spec.Variants) == 1 {
return defaultVariant, nil
}
choice, err := strconv.Atoi(input)
if err != nil || choice < 1 || choice > len(ps.Spec.Variants) {
fmt.Fprintf(os.Stderr, "Invalid choice. Please enter a number between 1 and %d.\n", len(ps.Spec.Variants))
continue
}
return ps.Spec.Variants[choice-1].Name, nil
}
}
func init() {
rootCmd.AddCommand(addCmd)
addCmd.Flags().StringArrayVarP(&addCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
addCmd.Flags().StringVar(&addCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
}

View File

@@ -1,396 +0,0 @@
/*
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.
*/
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
_ "k8s.io/client-go/plugin/pkg/client/auth"
)
var delCmdFlags struct {
files []string
kubeconfig string
}
var delCmd = &cobra.Command{
Use: "del [package]...",
Short: "Delete Package resources",
Long: `Delete Package resources.
You can specify packages as arguments or use -f flag to read from files.
Multiple -f flags can be specified, and they can point to files or directories.`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Collect package names from arguments and files
packageNames := make(map[string]bool)
packagesFromFiles := make(map[string]string) // packageName -> filePath
for _, arg := range args {
packageNames[arg] = true
}
// Read packages from files (reuse function from add.go)
for _, filePath := range delCmdFlags.files {
packages, err := readPackagesFromFile(filePath)
if err != nil {
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
}
for _, pkg := range packages {
packageNames[pkg] = true
if oldPath, ok := packagesFromFiles[pkg]; ok {
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
}
packagesFromFiles[pkg] = filePath
}
}
if len(packageNames) == 0 {
return fmt.Errorf("no packages specified")
}
// Create Kubernetes client config
var config *rest.Config
var err error
if delCmdFlags.kubeconfig != "" {
config, err = clientcmd.BuildConfigFromFlags("", delCmdFlags.kubeconfig)
if err != nil {
return fmt.Errorf("failed to load kubeconfig from %s: %w", delCmdFlags.kubeconfig, err)
}
} else {
config, err = ctrl.GetConfig()
if err != nil {
return fmt.Errorf("failed to get kubeconfig: %w", err)
}
}
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to create k8s client: %w", err)
}
// Check which requested packages are installed
var installedPackages cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &installedPackages); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
installedMap := make(map[string]bool)
for _, pkg := range installedPackages.Items {
installedMap[pkg.Name] = true
}
// Warn about requested packages that are not installed
for pkgName := range packageNames {
if !installedMap[pkgName] {
fmt.Fprintf(os.Stderr, "⚠ Package %s is not installed, skipping\n", pkgName)
}
}
// Find all packages to delete (including dependents)
packagesToDelete, err := findPackagesToDelete(ctx, k8sClient, packageNames)
if err != nil {
return fmt.Errorf("failed to analyze dependencies: %w", err)
}
if len(packagesToDelete) == 0 {
fmt.Fprintf(os.Stderr, "No packages found to delete\n")
return nil
}
// Show packages to be deleted and ask for confirmation
if err := confirmDeletion(packagesToDelete, packageNames); err != nil {
return err
}
// Delete packages in reverse topological order (dependents first, then dependencies)
deleteOrder, err := getDeleteOrder(ctx, k8sClient, packagesToDelete)
if err != nil {
return fmt.Errorf("failed to determine delete order: %w", err)
}
// Delete each package
for _, packageName := range deleteOrder {
pkg := &cozyv1alpha1.Package{}
pkg.Name = packageName
if err := k8sClient.Delete(ctx, pkg); err != nil {
if apierrors.IsNotFound(err) {
fmt.Fprintf(os.Stderr, "⚠ Package %s not found, skipping\n", packageName)
continue
}
return fmt.Errorf("failed to delete Package %s: %w", packageName, err)
}
fmt.Fprintf(os.Stderr, "✓ Deleted Package %s\n", packageName)
}
return nil
},
}
// findPackagesToDelete finds all packages that need to be deleted, including dependents
func findPackagesToDelete(ctx context.Context, k8sClient client.Client, requestedPackages map[string]bool) (map[string]bool, error) {
// Get all installed Packages
var installedPackages cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &installedPackages); err != nil {
return nil, fmt.Errorf("failed to list Packages: %w", err)
}
installedMap := make(map[string]bool)
for _, pkg := range installedPackages.Items {
installedMap[pkg.Name] = true
}
// Get all PackageSources to build dependency graph
var packageSources cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &packageSources); err != nil {
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
}
// Build reverse dependency graph (dependents -> dependencies)
// This tells us: for each package, which packages depend on it
reverseDeps := make(map[string][]string)
for _, ps := range packageSources.Items {
// Only consider installed packages
if !installedMap[ps.Name] {
continue
}
for _, variant := range ps.Spec.Variants {
for _, dep := range variant.DependsOn {
// Only consider installed dependencies
if installedMap[dep] {
reverseDeps[dep] = append(reverseDeps[dep], ps.Name)
}
}
}
}
// Find all packages to delete (requested + their dependents)
packagesToDelete := make(map[string]bool)
visited := make(map[string]bool)
var findDependents func(string)
findDependents = func(pkgName string) {
if visited[pkgName] {
return
}
visited[pkgName] = true
// Only add if it's installed
if installedMap[pkgName] {
packagesToDelete[pkgName] = true
}
// Recursively find all dependents
for _, dependent := range reverseDeps[pkgName] {
if installedMap[dependent] {
findDependents(dependent)
}
}
}
// Start from requested packages
for pkgName := range requestedPackages {
if !installedMap[pkgName] {
continue
}
findDependents(pkgName)
}
return packagesToDelete, nil
}
// confirmDeletion shows the list of packages to be deleted and asks for user confirmation
func confirmDeletion(packagesToDelete map[string]bool, requestedPackages map[string]bool) error {
// Separate requested packages from dependents
var requested []string
var dependents []string
for pkg := range packagesToDelete {
if requestedPackages[pkg] {
requested = append(requested, pkg)
} else {
dependents = append(dependents, pkg)
}
}
fmt.Fprintf(os.Stderr, "\nThe following packages will be deleted:\n\n")
if len(requested) > 0 {
fmt.Fprintf(os.Stderr, "Requested packages:\n")
for _, pkg := range requested {
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
}
fmt.Fprintf(os.Stderr, "\n")
}
if len(dependents) > 0 {
fmt.Fprintf(os.Stderr, "Dependent packages (will also be deleted):\n")
for _, pkg := range dependents {
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
}
fmt.Fprintf(os.Stderr, "\n")
}
fmt.Fprintf(os.Stderr, "Total: %d package(s)\n\n", len(packagesToDelete))
fmt.Fprintf(os.Stderr, "Do you want to continue? [y/N]: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read input: %w", err)
}
input = strings.TrimSpace(strings.ToLower(input))
if input != "y" && input != "yes" {
return fmt.Errorf("deletion cancelled")
}
return nil
}
// getDeleteOrder returns packages in reverse topological order (dependents first, then dependencies)
// This ensures we delete dependents before their dependencies
func getDeleteOrder(ctx context.Context, k8sClient client.Client, packagesToDelete map[string]bool) ([]string, error) {
// Get all PackageSources to build dependency graph
var packageSources cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &packageSources); err != nil {
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
}
// Build forward dependency graph (package -> dependencies)
dependencyGraph := make(map[string][]string)
for _, ps := range packageSources.Items {
if !packagesToDelete[ps.Name] {
continue
}
deps := make(map[string]bool)
for _, variant := range ps.Spec.Variants {
for _, dep := range variant.DependsOn {
if packagesToDelete[dep] {
deps[dep] = true
}
}
}
var depList []string
for dep := range deps {
depList = append(depList, dep)
}
dependencyGraph[ps.Name] = depList
}
// Build reverse graph for topological sort
reverseGraph := make(map[string][]string)
allNodes := make(map[string]bool)
for node, deps := range dependencyGraph {
allNodes[node] = true
for _, dep := range deps {
allNodes[dep] = true
reverseGraph[dep] = append(reverseGraph[dep], node)
}
}
// Add nodes that have no dependencies
for pkg := range packagesToDelete {
if !allNodes[pkg] {
allNodes[pkg] = true
dependencyGraph[pkg] = []string{}
}
}
// Calculate in-degrees
inDegree := make(map[string]int)
for node := range allNodes {
inDegree[node] = 0
}
for node, deps := range dependencyGraph {
inDegree[node] = len(deps)
}
// Kahn's algorithm - start with nodes that have no dependencies
var queue []string
for node, degree := range inDegree {
if degree == 0 {
queue = append(queue, node)
}
}
var result []string
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
// Process dependents
for _, dependent := range reverseGraph[node] {
inDegree[dependent]--
if inDegree[dependent] == 0 {
queue = append(queue, dependent)
}
}
}
// Check for cycles: if not all nodes were processed, there's a cycle
if len(result) != len(allNodes) {
// Find unprocessed nodes
processed := make(map[string]bool)
for _, node := range result {
processed[node] = true
}
var unprocessed []string
for node := range allNodes {
if !processed[node] {
unprocessed = append(unprocessed, node)
}
}
return nil, fmt.Errorf("dependency cycle detected: the following packages form a cycle and cannot be deleted: %v", unprocessed)
}
// Reverse the result to get dependents first, then dependencies
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return result, nil
}
func init() {
rootCmd.AddCommand(delCmd)
delCmd.Flags().StringArrayVarP(&delCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
delCmd.Flags().StringVar(&delCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
}

View File

@@ -1,564 +0,0 @@
/*
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.
*/
package cmd
import (
"context"
"fmt"
"os"
"strings"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"github.com/emicklei/dot"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var dotCmdFlags struct {
installed bool
components bool
files []string
kubeconfig string
}
var dotCmd = &cobra.Command{
Use: "dot [package]...",
Short: "Generate dependency graph as graphviz DOT format",
Long: `Generate dependency graph as graphviz DOT format.
Pipe the output through the "dot" program (part of graphviz package) to render the graph:
cozypkg dot | dot -Tpng > graph.png
By default, shows dependencies for all PackageSource resources.
Use --installed to show only installed Package resources.
Specify packages as arguments or use -f flag to read from files.`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Collect package names from arguments and files
packageNames := make(map[string]bool)
for _, arg := range args {
packageNames[arg] = true
}
// Read packages from files (reuse function from add.go)
for _, filePath := range dotCmdFlags.files {
packages, err := readPackagesFromFile(filePath)
if err != nil {
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
}
for _, pkg := range packages {
packageNames[pkg] = true
}
}
// Convert to slice, empty means all packages
var selectedPackages []string
if len(packageNames) > 0 {
for pkg := range packageNames {
selectedPackages = append(selectedPackages, pkg)
}
}
// If multiple packages specified, show graph for all of them
// If single package, use packageName for backward compatibility
var packageName string
if len(selectedPackages) == 1 {
packageName = selectedPackages[0]
} else if len(selectedPackages) > 1 {
// Multiple packages - pass empty string to packageName, use selectedPackages
packageName = ""
}
// packagesOnly is inverse of components flag (if components=false, then packagesOnly=true)
packagesOnly := !dotCmdFlags.components
graph, allNodes, edgeVariants, packageNames, err := buildGraphFromCluster(ctx, dotCmdFlags.kubeconfig, packagesOnly, dotCmdFlags.installed, packageName, selectedPackages)
if err != nil {
return fmt.Errorf("error getting PackageSource dependencies: %w", err)
}
dotGraph := generateDOTGraph(graph, allNodes, packagesOnly, edgeVariants, packageNames)
dotGraph.Write(os.Stdout)
return nil
},
}
func init() {
rootCmd.AddCommand(dotCmd)
dotCmd.Flags().BoolVarP(&dotCmdFlags.installed, "installed", "i", false, "show dependencies only for installed Package resources")
dotCmd.Flags().BoolVar(&dotCmdFlags.components, "components", false, "show component-level dependencies")
dotCmd.Flags().StringArrayVarP(&dotCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
dotCmd.Flags().StringVar(&dotCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
}
var (
dependenciesScheme = runtime.NewScheme()
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(dependenciesScheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(dependenciesScheme))
}
// buildGraphFromCluster builds a dependency graph from PackageSource resources in the cluster.
// Returns: graph, allNodes, edgeVariants (map[edgeKey]variants), packageNames, error
func buildGraphFromCluster(ctx context.Context, kubeconfig string, packagesOnly bool, installedOnly bool, packageName string, selectedPackages []string) (map[string][]string, map[string]bool, map[string][]string, map[string]bool, error) {
// Create Kubernetes client config
var config *rest.Config
var err error
if kubeconfig != "" {
// Load kubeconfig from explicit path
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to load kubeconfig from %s: %w", kubeconfig, err)
}
} else {
// Use default kubeconfig loading (from env var or ~/.kube/config)
config, err = ctrl.GetConfig()
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to get kubeconfig: %w", err)
}
}
k8sClient, err := client.New(config, client.Options{Scheme: dependenciesScheme})
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to create k8s client: %w", err)
}
// Get installed Packages if needed
installedPackages := make(map[string]bool)
if installedOnly {
var packageList cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &packageList); err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to list Packages: %w", err)
}
for _, pkg := range packageList.Items {
installedPackages[pkg.Name] = true
}
}
// List all PackageSource resources
var packageSourceList cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &packageSourceList); err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to list PackageSources: %w", err)
}
// Build map of existing packages and components
packageNames := make(map[string]bool)
allExistingComponents := make(map[string]bool) // "package.component" -> true
for _, ps := range packageSourceList.Items {
if ps.Name != "" {
packageNames[ps.Name] = true
for _, variant := range ps.Spec.Variants {
for _, component := range variant.Components {
if component.Install != nil {
componentFullName := fmt.Sprintf("%s.%s", ps.Name, component.Name)
allExistingComponents[componentFullName] = true
}
}
}
}
}
graph := make(map[string][]string)
allNodes := make(map[string]bool)
edgeVariants := make(map[string][]string) // key: "source->target", value: list of variant names
existingEdges := make(map[string]bool) // key: "source->target" to avoid duplicates
componentHasLocalDeps := make(map[string]bool) // componentName -> has local component dependencies
// Process each PackageSource
for _, ps := range packageSourceList.Items {
psName := ps.Name
if psName == "" {
continue
}
// Filter by package name if specified
if packageName != "" && psName != packageName {
continue
}
// Filter by selected packages if specified
if len(selectedPackages) > 0 {
found := false
for _, selected := range selectedPackages {
if psName == selected {
found = true
break
}
}
if !found {
continue
}
}
// Filter by installed packages if flag is set
if installedOnly && !installedPackages[psName] {
continue
}
allNodes[psName] = true
// Track package dependencies per variant
packageDepVariants := make(map[string]map[string]bool) // dep -> variant -> true
allVariantNames := make(map[string]bool)
for _, v := range ps.Spec.Variants {
allVariantNames[v.Name] = true
}
// Track component dependencies per variant
componentDepVariants := make(map[string]map[string]map[string]bool) // componentName -> dep -> variant -> true
componentVariants := make(map[string]map[string]bool) // componentName -> variant -> true
// Extract dependencies from variants
for _, variant := range ps.Spec.Variants {
// Variant-level dependencies (package-level)
for _, dep := range variant.DependsOn {
// If installedOnly is set, only include dependencies that are installed
if installedOnly && !installedPackages[dep] {
continue
}
// Track which variant this dependency comes from
if packageDepVariants[dep] == nil {
packageDepVariants[dep] = make(map[string]bool)
}
packageDepVariants[dep][variant.Name] = true
edgeKey := fmt.Sprintf("%s->%s", psName, dep)
if !existingEdges[edgeKey] {
graph[psName] = append(graph[psName], dep)
existingEdges[edgeKey] = true
}
// Add to allNodes only if package exists
if packageNames[dep] {
allNodes[dep] = true
}
// If package doesn't exist, don't add to allNodes - it will be shown as missing (red)
}
// Component-level dependencies
if !packagesOnly {
for _, component := range variant.Components {
// Skip components without install section
if component.Install == nil {
continue
}
componentName := fmt.Sprintf("%s.%s", psName, component.Name)
allNodes[componentName] = true
// Track which variants this component appears in
if componentVariants[componentName] == nil {
componentVariants[componentName] = make(map[string]bool)
}
componentVariants[componentName][variant.Name] = true
if component.Install != nil {
if componentDepVariants[componentName] == nil {
componentDepVariants[componentName] = make(map[string]map[string]bool)
}
for _, dep := range component.Install.DependsOn {
// Track which variant this dependency comes from
if componentDepVariants[componentName][dep] == nil {
componentDepVariants[componentName][dep] = make(map[string]bool)
}
componentDepVariants[componentName][dep][variant.Name] = true
// Check if it's a local component dependency or external
if strings.Contains(dep, ".") {
// External component dependency (package.component format)
// Mark that this component has local dependencies (for edge to package logic)
componentHasLocalDeps[componentName] = true
// Check if target component exists
if allExistingComponents[dep] {
// Component exists
edgeKey := fmt.Sprintf("%s->%s", componentName, dep)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], dep)
existingEdges[edgeKey] = true
}
allNodes[dep] = true
} else {
// Component doesn't exist - create missing component node
edgeKey := fmt.Sprintf("%s->%s", componentName, dep)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], dep)
existingEdges[edgeKey] = true
}
// Don't add to allNodes - will be shown as missing (red)
// Add edge from missing component to its package
parts := strings.SplitN(dep, ".", 2)
if len(parts) == 2 {
depPackageName := parts[0]
missingEdgeKey := fmt.Sprintf("%s->%s", dep, depPackageName)
if !existingEdges[missingEdgeKey] {
graph[dep] = append(graph[dep], depPackageName)
existingEdges[missingEdgeKey] = true
}
// Add package to allNodes only if it exists
if packageNames[depPackageName] {
allNodes[depPackageName] = true
}
// If package doesn't exist, it will be shown as missing (red)
}
}
} else {
// Local component dependency (same package)
// Mark that this component has local dependencies
componentHasLocalDeps[componentName] = true
localDep := fmt.Sprintf("%s.%s", psName, dep)
// Check if target component exists
if allExistingComponents[localDep] {
// Component exists
edgeKey := fmt.Sprintf("%s->%s", componentName, localDep)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], localDep)
existingEdges[edgeKey] = true
}
allNodes[localDep] = true
} else {
// Component doesn't exist - create missing component node
edgeKey := fmt.Sprintf("%s->%s", componentName, localDep)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], localDep)
existingEdges[edgeKey] = true
}
// Don't add to allNodes - will be shown as missing (red)
// Add edge from missing component to its package
missingEdgeKey := fmt.Sprintf("%s->%s", localDep, psName)
if !existingEdges[missingEdgeKey] {
graph[localDep] = append(graph[localDep], psName)
existingEdges[missingEdgeKey] = true
}
}
}
}
}
}
}
}
// Store variant information for package dependencies that are not in all variants
for dep, variants := range packageDepVariants {
if len(variants) < len(allVariantNames) {
var variantList []string
for v := range variants {
variantList = append(variantList, v)
}
edgeKey := fmt.Sprintf("%s->%s", psName, dep)
edgeVariants[edgeKey] = variantList
}
}
// Add component->package edges for components without local dependencies
if !packagesOnly {
for componentName := range componentVariants {
// Only add edge to package if component has no local component dependencies
if !componentHasLocalDeps[componentName] {
edgeKey := fmt.Sprintf("%s->%s", componentName, psName)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], psName)
existingEdges[edgeKey] = true
}
// If component is not in all variants, store variant info for component->package edge
componentAllVariants := componentVariants[componentName]
if len(componentAllVariants) < len(allVariantNames) {
var variantList []string
for v := range componentAllVariants {
variantList = append(variantList, v)
}
edgeVariants[edgeKey] = variantList
}
}
}
}
// Store variant information for component dependencies that are not in all variants
for componentName, deps := range componentDepVariants {
componentAllVariants := componentVariants[componentName]
for dep, variants := range deps {
if len(variants) < len(componentAllVariants) {
var variantList []string
for v := range variants {
variantList = append(variantList, v)
}
// Determine the actual target name
var targetName string
if strings.Contains(dep, ".") {
targetName = dep
} else {
targetName = fmt.Sprintf("%s.%s", psName, dep)
}
edgeKey := fmt.Sprintf("%s->%s", componentName, targetName)
edgeVariants[edgeKey] = variantList
}
}
}
}
return graph, allNodes, edgeVariants, packageNames, nil
}
// generateDOTGraph generates a DOT graph from the dependency graph.
func generateDOTGraph(graph map[string][]string, allNodes map[string]bool, packagesOnly bool, edgeVariants map[string][]string, packageNames map[string]bool) *dot.Graph {
g := dot.NewGraph(dot.Directed)
g.Attr("rankdir", "RL")
g.Attr("nodesep", "0.5")
g.Attr("ranksep", "1.0")
// Helper function to check if a node is a package
// A node is a package if:
// 1. It's directly in packageNames
// 2. It doesn't contain a dot (simple package name)
// 3. It contains a dot but the part before the first dot is a package name
isPackage := func(nodeName string) bool {
if packageNames[nodeName] {
return true
}
if !strings.Contains(nodeName, ".") {
return true
}
// If it contains a dot, check if the part before the first dot is a package
parts := strings.SplitN(nodeName, ".", 2)
if len(parts) > 0 {
return packageNames[parts[0]]
}
return false
}
// Add nodes
for node := range allNodes {
if packagesOnly && !isPackage(node) {
// Skip component nodes when packages-only is enabled
continue
}
n := g.Node(node)
// Style nodes based on type
if isPackage(node) {
// Package node
n.Attr("shape", "box")
n.Attr("style", "rounded,filled")
n.Attr("fillcolor", "lightblue")
n.Attr("label", node)
} else {
// Component node
n.Attr("shape", "box")
n.Attr("style", "rounded,filled")
n.Attr("fillcolor", "lightyellow")
// Extract component name (part after last dot)
parts := strings.Split(node, ".")
if len(parts) > 0 {
n.Attr("label", parts[len(parts)-1])
} else {
n.Attr("label", node)
}
}
}
// Add edges
for source, targets := range graph {
if packagesOnly && !isPackage(source) {
// Skip component edges when packages-only is enabled
continue
}
for _, target := range targets {
if packagesOnly && !isPackage(target) {
// Skip component edges when packages-only is enabled
continue
}
// Check if target exists
targetExists := allNodes[target]
// Determine edge type for coloring
sourceIsPackage := isPackage(source)
targetIsPackage := isPackage(target)
// Add edge
edge := g.Edge(g.Node(source), g.Node(target))
// Set edge color based on type (if target exists)
if targetExists {
if sourceIsPackage && targetIsPackage {
// Package -> Package: black (default)
edge.Attr("color", "black")
} else {
// Component -> Package or Component -> Component: green
edge.Attr("color", "green")
}
}
// If target doesn't exist, mark it as missing (red color)
if !targetExists {
edge.Attr("color", "red")
edge.Attr("style", "dashed")
// Also add the missing node with red color
missingNode := g.Node(target)
missingNode.Attr("shape", "box")
missingNode.Attr("style", "rounded,filled,dashed")
missingNode.Attr("fillcolor", "lightcoral")
// Determine label based on node type
if isPackage(target) {
// Package node
missingNode.Attr("label", target)
} else {
// Component node - extract component name
parts := strings.Split(target, ".")
if len(parts) > 0 {
missingNode.Attr("label", parts[len(parts)-1])
} else {
missingNode.Attr("label", target)
}
}
} else {
// Check if this edge has variant information (dependency not in all variants)
edgeKey := fmt.Sprintf("%s->%s", source, target)
if variants, hasVariants := edgeVariants[edgeKey]; hasVariants {
// Add label with variant names
edge.Attr("label", strings.Join(variants, ","))
}
}
}
}
return g
}

View File

@@ -1,220 +0,0 @@
/*
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.
*/
package cmd
import (
"context"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
_ "k8s.io/client-go/plugin/pkg/client/auth"
)
var listCmdFlags struct {
installed bool
components bool
kubeconfig string
}
var listCmd = &cobra.Command{
Use: "list",
Short: "List PackageSource or Package resources",
Long: `List PackageSource or Package resources in table format.
By default, lists PackageSource resources. Use --installed flag to list installed Package resources.
Use --components flag to show components on separate lines.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Create Kubernetes client config
var config *rest.Config
var err error
if listCmdFlags.kubeconfig != "" {
config, err = clientcmd.BuildConfigFromFlags("", listCmdFlags.kubeconfig)
if err != nil {
return fmt.Errorf("failed to load kubeconfig from %s: %w", listCmdFlags.kubeconfig, err)
}
} else {
config, err = ctrl.GetConfig()
if err != nil {
return fmt.Errorf("failed to get kubeconfig: %w", err)
}
}
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to create k8s client: %w", err)
}
if listCmdFlags.installed {
return listPackages(ctx, k8sClient, listCmdFlags.components)
}
return listPackageSources(ctx, k8sClient, listCmdFlags.components)
},
}
func listPackageSources(ctx context.Context, k8sClient client.Client, showComponents bool) error {
var psList cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &psList); err != nil {
return fmt.Errorf("failed to list PackageSources: %w", err)
}
// Use tabwriter for better column alignment
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
defer w.Flush()
// Print header
fmt.Fprintln(w, "NAME\tVARIANTS\tREADY\tSTATUS")
// Print rows
for _, ps := range psList.Items {
// Get variants
var variants []string
for _, variant := range ps.Spec.Variants {
variants = append(variants, variant.Name)
}
variantsStr := strings.Join(variants, ",")
if len(variantsStr) > 28 {
variantsStr = variantsStr[:25] + "..."
}
// Get Ready condition
ready := "Unknown"
status := ""
for _, condition := range ps.Status.Conditions {
if condition.Type == "Ready" {
ready = string(condition.Status)
status = condition.Message
if len(status) > 48 {
status = status[:45] + "..."
}
break
}
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", ps.Name, variantsStr, ready, status)
// Show components if requested
if showComponents {
for _, variant := range ps.Spec.Variants {
for _, component := range variant.Components {
fmt.Fprintf(w, " %s\t%s\t\t\n",
fmt.Sprintf("%s.%s", ps.Name, component.Name),
variant.Name)
}
}
}
}
return nil
}
func listPackages(ctx context.Context, k8sClient client.Client, showComponents bool) error {
var pkgList cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &pkgList); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
// Fetch all PackageSource resources once if components are requested
var psMap map[string]*cozyv1alpha1.PackageSource
if showComponents {
var psList cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &psList); err != nil {
return fmt.Errorf("failed to list PackageSources: %w", err)
}
psMap = make(map[string]*cozyv1alpha1.PackageSource)
for i := range psList.Items {
psMap[psList.Items[i].Name] = &psList.Items[i]
}
}
// Use tabwriter for better column alignment
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
defer w.Flush()
// Print header
fmt.Fprintln(w, "NAME\tVARIANT\tREADY\tSTATUS")
// Print rows
for _, pkg := range pkgList.Items {
variant := pkg.Spec.Variant
if variant == "" {
variant = "default"
}
// Get Ready condition
ready := "Unknown"
status := ""
for _, condition := range pkg.Status.Conditions {
if condition.Type == "Ready" {
ready = string(condition.Status)
status = condition.Message
if len(status) > 48 {
status = status[:45] + "..."
}
break
}
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", pkg.Name, variant, ready, status)
// Show components if requested
if showComponents {
// Look up PackageSource from map instead of making API call
if ps, exists := psMap[pkg.Name]; exists {
// Find the variant
for _, v := range ps.Spec.Variants {
if v.Name == variant {
for _, component := range v.Components {
fmt.Fprintf(w, " %s\t%s\t\t\n",
fmt.Sprintf("%s.%s", pkg.Name, component.Name),
variant)
}
break
}
}
}
}
}
return nil
}
func init() {
rootCmd.AddCommand(listCmd)
listCmd.Flags().BoolVarP(&listCmdFlags.installed, "installed", "i", false, "list installed Package resources instead of PackageSource resources")
listCmd.Flags().BoolVar(&listCmdFlags.components, "components", false, "show components on separate lines")
listCmd.Flags().StringVar(&listCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
}

View File

@@ -1,49 +0,0 @@
/*
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.
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands.
var rootCmd = &cobra.Command{
Use: "cozypkg",
Short: "A CLI for managing Cozystack packages",
Long: ``,
SilenceErrors: true,
SilenceUsage: true,
DisableAutoGenTag: true,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() error {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
return err
}
return nil
}
func init() {
// Commands are registered in their respective init() functions
}

View File

@@ -1,30 +0,0 @@
/*
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.
*/
package main
import (
"os"
"github.com/cozystack/cozystack/cmd/cozypkg/cmd"
)
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}

View File

@@ -1,500 +0,0 @@
/*
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.
*/
package main
import (
"context"
"flag"
"fmt"
"os"
"strings"
"time"
// 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"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"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/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"github.com/cozystack/cozystack/internal/cozyvaluesreplicator"
"github.com/cozystack/cozystack/internal/fluxinstall"
"github.com/cozystack/cozystack/internal/operator"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
utilruntime.Must(helmv2.AddToScheme(scheme))
utilruntime.Must(sourcev1.AddToScheme(scheme))
utilruntime.Must(sourcewatcherv1beta1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var installFlux bool
var cozystackVersion string
var cozyValuesSecretName string
var cozyValuesSecretNamespace string
var cozyValuesNamespaceSelector string
var platformSourceURL string
var platformSourceName string
var platformSourceRef string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
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", false,
"If set the metrics endpoint is served securely")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop")
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
"Version of Cozystack")
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
flag.StringVar(&cozyValuesSecretName, "cozy-values-secret-name", "cozystack-values", "The name of the secret containing cluster-wide configuration values.")
flag.StringVar(&cozyValuesSecretNamespace, "cozy-values-secret-namespace", "cozy-system", "The namespace of the secret containing cluster-wide configuration values.")
flag.StringVar(&cozyValuesNamespaceSelector, "cozy-values-namespace-selector", "cozystack.io/system=true", "The label selector for namespaces where the cluster-wide configuration values must be replicated.")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
config := ctrl.GetConfigOrDie()
// Create a direct client (without cache) for pre-start operations
directClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
setupLog.Error(err, "unable to create direct client")
os.Exit(1)
}
targetNSSelector, err := labels.Parse(cozyValuesNamespaceSelector)
if err != nil {
setupLog.Error(err, "could not parse namespace label selector")
os.Exit(1)
}
// Start the controller manager
setupLog.Info("Starting controller manager")
mgr, err := ctrl.NewManager(config, ctrl.Options{
Scheme: scheme,
Cache: cache.Options{
ByObject: map[client.Object]cache.ByObject{
// Cache only Secrets named <secretName> (in any namespace)
&corev1.Secret{}: {
Field: fields.OneTermEqualSelector("metadata.name", cozyValuesSecretName),
},
// Cache only Namespaces that match a label selector
&corev1.Namespace{}: {
Label: targetNSSelector,
},
},
},
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
},
WebhookServer: webhook.NewServer(webhook.Options{
Port: 9443,
}),
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "cozystack-operator.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, 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)
}
// Install Flux before starting reconcile loop
if installFlux {
setupLog.Info("Installing Flux components before starting reconcile loop")
installCtx, installCancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer installCancel()
// Use direct client for pre-start operations (cache is not ready yet)
if err := fluxinstall.Install(installCtx, directClient, fluxinstall.WriteEmbeddedManifests); err != nil {
setupLog.Error(err, "failed to install Flux")
os.Exit(1)
}
setupLog.Info("Flux installation completed successfully")
}
// Generate and install platform source resource if specified
if platformSourceURL != "" {
setupLog.Info("Generating platform source resource", "url", platformSourceURL, "name", platformSourceName, "ref", platformSourceRef)
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer installCancel()
// Use direct client for pre-start operations (cache is not ready yet)
if err := installPlatformSourceResource(installCtx, directClient, platformSourceURL, platformSourceName, platformSourceRef); err != nil {
setupLog.Error(err, "failed to install platform source resource")
os.Exit(1)
} else {
setupLog.Info("Platform source resource installation completed successfully")
}
}
if err := (&cozyvaluesreplicator.SecretReplicatorReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
SourceNamespace: cozyValuesSecretNamespace,
SecretName: cozyValuesSecretName,
TargetNamespaceSelector: targetNSSelector,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CozyValuesReplicator")
os.Exit(1)
}
// Setup PackageSource reconciler
if err := (&operator.PackageSourceReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "PackageSource")
os.Exit(1)
}
// Setup Package reconciler
if err := (&operator.PackageReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Package")
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 controller manager")
mgrCtx := ctrl.SetupSignalHandler()
if err := mgr.Start(mgrCtx); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
// installPlatformSourceResource generates and installs a Flux source resource (OCIRepository or GitRepository)
// based on the platform source URL
func installPlatformSourceResource(ctx context.Context, k8sClient client.Client, sourceURL, resourceName, refSpec string) error {
logger := log.FromContext(ctx)
// Parse the source URL to determine type
sourceType, repoURL, err := parsePlatformSourceURL(sourceURL)
if err != nil {
return fmt.Errorf("failed to parse platform source URL: %w", err)
}
// Parse reference specification
refMap, err := parseRefSpec(refSpec)
if err != nil {
return fmt.Errorf("failed to parse reference specification: %w", err)
}
var obj client.Object
switch sourceType {
case "oci":
obj, err = generateOCIRepository(resourceName, repoURL, refMap)
if err != nil {
return fmt.Errorf("failed to generate OCIRepository: %w", err)
}
case "git":
obj, err = generateGitRepository(resourceName, repoURL, refMap)
if err != nil {
return fmt.Errorf("failed to generate GitRepository: %w", err)
}
default:
return fmt.Errorf("unsupported source type: %s (expected oci:// or https://)", sourceType)
}
// Apply the resource (create or update)
logger.Info("Applying platform source resource",
"apiVersion", obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
"kind", obj.GetObjectKind().GroupVersionKind().Kind,
"name", obj.GetName(),
"namespace", obj.GetNamespace(),
)
existing := obj.DeepCopyObject().(client.Object)
key := client.ObjectKeyFromObject(obj)
err = k8sClient.Get(ctx, key, existing)
if err != nil {
if client.IgnoreNotFound(err) == nil {
// Resource doesn't exist, create it
if err := k8sClient.Create(ctx, obj); err != nil {
return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
}
logger.Info("Created platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
} else {
return fmt.Errorf("failed to check if resource exists: %w", err)
}
} else {
// Resource exists, update it
obj.SetResourceVersion(existing.GetResourceVersion())
if err := k8sClient.Update(ctx, obj); err != nil {
return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
}
logger.Info("Updated platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
}
return nil
}
// parsePlatformSourceURL parses the source URL and returns the source type and repository URL.
// Supports formats:
// - oci://registry.example.com/repo
// - https://github.com/user/repo
// - http://github.com/user/repo
// - ssh://git@github.com/user/repo
func parsePlatformSourceURL(sourceURL string) (sourceType, repoURL string, err error) {
sourceURL = strings.TrimSpace(sourceURL)
if strings.HasPrefix(sourceURL, "oci://") {
return "oci", sourceURL, nil
}
if strings.HasPrefix(sourceURL, "https://") || strings.HasPrefix(sourceURL, "http://") || strings.HasPrefix(sourceURL, "ssh://") {
return "git", sourceURL, nil
}
return "", "", fmt.Errorf("unsupported source URL scheme (expected oci://, https://, http://, or ssh://): %s", sourceURL)
}
// parseRefSpec parses a reference specification string in the format "key1=value1,key2=value2".
// Returns a map of key-value pairs.
func parseRefSpec(refSpec string) (map[string]string, error) {
result := make(map[string]string)
refSpec = strings.TrimSpace(refSpec)
if refSpec == "" {
return result, nil
}
pairs := strings.Split(refSpec, ",")
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
// Split on first '=' only to allow '=' in values (e.g., digest=sha256:...)
idx := strings.Index(pair, "=")
if idx == -1 {
return nil, fmt.Errorf("invalid reference specification format: %q (expected key=value)", pair)
}
key := strings.TrimSpace(pair[:idx])
value := strings.TrimSpace(pair[idx+1:])
if key == "" {
return nil, fmt.Errorf("empty key in reference specification: %q", pair)
}
if value == "" {
return nil, fmt.Errorf("empty value for key %q in reference specification", key)
}
result[key] = value
}
return result, nil
}
// Valid reference keys for OCI repositories
var validOCIRefKeys = map[string]bool{
"digest": true,
"semver": true,
"semverFilter": true,
"tag": true,
}
// Valid reference keys for Git repositories
var validGitRefKeys = map[string]bool{
"branch": true,
"tag": true,
"semver": true,
"name": true,
"commit": true,
}
// validateOCIRef validates reference keys for OCI repositories
func validateOCIRef(refMap map[string]string) error {
for key := range refMap {
if !validOCIRefKeys[key] {
return fmt.Errorf("invalid OCI reference key %q (valid keys: digest, semver, semverFilter, tag)", key)
}
}
// Validate digest format if provided
if digest, ok := refMap["digest"]; ok {
if !strings.HasPrefix(digest, "sha256:") {
return fmt.Errorf("digest must be in format 'sha256:<hash>', got: %s", digest)
}
}
return nil
}
// validateGitRef validates reference keys for Git repositories
func validateGitRef(refMap map[string]string) error {
for key := range refMap {
if !validGitRefKeys[key] {
return fmt.Errorf("invalid Git reference key %q (valid keys: branch, tag, semver, name, commit)", key)
}
}
// Validate commit format if provided (should be a hex string)
if commit, ok := refMap["commit"]; ok {
if len(commit) < 7 {
return fmt.Errorf("commit SHA should be at least 7 characters, got: %s", commit)
}
for _, c := range commit {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return fmt.Errorf("commit SHA should be a hexadecimal string, got: %s", commit)
}
}
}
return nil
}
// generateOCIRepository creates an OCIRepository resource
func generateOCIRepository(name, repoURL string, refMap map[string]string) (*sourcev1.OCIRepository, error) {
if err := validateOCIRef(refMap); err != nil {
return nil, err
}
obj := &sourcev1.OCIRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1.GroupVersion.String(),
Kind: sourcev1.OCIRepositoryKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "cozy-system",
},
Spec: sourcev1.OCIRepositorySpec{
URL: repoURL,
Interval: metav1.Duration{Duration: 5 * time.Minute},
},
}
// Set reference if any ref options are provided
if len(refMap) > 0 {
obj.Spec.Reference = &sourcev1.OCIRepositoryRef{
Digest: refMap["digest"],
SemVer: refMap["semver"],
SemverFilter: refMap["semverFilter"],
Tag: refMap["tag"],
}
}
return obj, nil
}
// generateGitRepository creates a GitRepository resource
func generateGitRepository(name, repoURL string, refMap map[string]string) (*sourcev1.GitRepository, error) {
if err := validateGitRef(refMap); err != nil {
return nil, err
}
obj := &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1.GroupVersion.String(),
Kind: sourcev1.GitRepositoryKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "cozy-system",
},
Spec: sourcev1.GitRepositorySpec{
URL: repoURL,
Interval: metav1.Duration{Duration: 5 * time.Minute},
},
}
// Set reference if any ref options are provided
if len(refMap) > 0 {
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
Branch: refMap["branch"],
Tag: refMap["tag"],
SemVer: refMap["semver"],
Name: refMap["name"],
Commit: refMap["commit"],
}
}
return obj, nil
}

View File

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

39
go.mod
View File

@@ -5,10 +5,7 @@ module github.com/cozystack/cozystack
go 1.25.0
require (
github.com/emicklei/dot v1.10.0
github.com/fluxcd/helm-controller/api v1.4.3
github.com/fluxcd/source-controller/api v1.7.4
github.com/fluxcd/source-watcher/api/v2 v2.0.3
github.com/go-logr/logr v1.4.3
github.com/go-logr/zapr v1.3.0
github.com/google/gofuzz v1.2.0
@@ -17,19 +14,18 @@ require (
github.com/prometheus/client_golang v1.22.0
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.9.1
github.com/vmware-tanzu/velero v1.17.1
go.uber.org/zap v1.27.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1
k8s.io/apimachinery v0.34.2
k8s.io/apimachinery v0.34.1
k8s.io/apiserver v0.34.1
k8s.io/client-go v0.34.1
k8s.io/component-base v0.34.1
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/controller-runtime v0.22.2
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
)
@@ -48,9 +44,8 @@ require (
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
github.com/fluxcd/pkg/apis/kustomize v1.13.0 // indirect
github.com/fluxcd/pkg/apis/meta v1.23.0 // indirect
github.com/fluxcd/pkg/apis/meta v1.22.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -81,8 +76,8 @@ 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_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
@@ -91,14 +86,14 @@ require (
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
@@ -106,18 +101,18 @@ require (
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.37.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect

78
go.sum
View File

@@ -27,8 +27,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/dot v1.10.0 h1:z17n0ce/FBMz3QbShSzVGhiW447Qhu7fljzvp3Gs6ig=
github.com/emicklei/dot v1.10.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
@@ -39,16 +37,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/helm-controller/api v1.4.3 h1:CdZwjL1liXmYCWyk2jscmFEB59tICIlnWB9PfDDW5q4=
github.com/fluxcd/helm-controller/api v1.4.3/go.mod h1:0XrBhKEaqvxyDj/FziG1Q8Fmx2UATdaqLgYqmZh6wW4=
github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
github.com/fluxcd/pkg/apis/kustomize v1.13.0 h1:GGf0UBVRIku+gebY944icVeEIhyg1P/KE3IrhOyJJnE=
github.com/fluxcd/pkg/apis/kustomize v1.13.0/go.mod h1:TLKVqbtnzkhDuhWnAsN35977HvRfIjs+lgMuNro/LEc=
github.com/fluxcd/pkg/apis/meta v1.23.0 h1:fLis5YcHnOsyKYptzBtituBm5EWNx13I0bXQsy0FG4s=
github.com/fluxcd/pkg/apis/meta v1.23.0/go.mod h1:UWsIbBPCxYvoVklr2mV2uLFBf/n17dNAmKFjRfApdDo=
github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs=
github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY=
github.com/fluxcd/source-watcher/api/v2 v2.0.3 h1:SsVGAaMBxzvcgrOz/Kl6c2ybMHVqoiEFwtI+bDuSeSs=
github.com/fluxcd/source-watcher/api/v2 v2.0.3/go.mod h1:Nx3QZweVyuhaOtSNrw+oxifG+qrakPvjgNAN9qlUTb0=
github.com/fluxcd/pkg/apis/meta v1.22.0 h1:EHWQH5ZWml7i8eZ/AMjm1jxid3j/PQ31p+hIwCt6crM=
github.com/fluxcd/pkg/apis/meta v1.22.0/go.mod h1:Kc1+bWe5p0doROzuV9XiTfV/oL3ddsemYXt8ZYWdVVg=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
@@ -144,10 +136,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
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=
@@ -179,8 +171,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/vmware-tanzu/velero v1.17.1 h1:ldKeiTuUwkThOw7zrUucNA1NwnLG66zl13YetWAoE0I=
github.com/vmware-tanzu/velero v1.17.1/go.mod h1:3KTxuUN6Un38JzmYAX+8U6j2k6EexGoNNxa8jrJML8U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=
@@ -203,24 +193,24 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -248,8 +238,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -266,8 +256,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -280,14 +270,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -322,8 +312,8 @@ k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPG
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4=
sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=

View File

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

View File

@@ -23,14 +23,6 @@ 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)
OPERATOR_CRDDIR=packages/core/installer/definitions
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions
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"
@@ -61,15 +53,6 @@ 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=${TMPDIR}
mv ${TMPDIR}/cozystack.io_packages.yaml ${OPERATOR_CRDDIR}/cozystack.io_packages.yaml
mv ${TMPDIR}/cozystack.io_packagesources.yaml ${OPERATOR_CRDDIR}/cozystack.io_packagesources.yaml
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}/
$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

View File

@@ -1,93 +0,0 @@
package backupcontroller
import (
"context"
"net/http"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/log"
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
)
// BackupVeleroStrategyReconciler reconciles BackupJob with a strategy referencing
// Velero.strategy.backups.cozystack.io objects.
type BackupJobReconciler struct {
client.Client
dynamic.Interface
meta.RESTMapper
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
func (r *BackupJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("reconciling BackupJob", "namespace", req.Namespace, "name", req.Name)
j := &backupsv1alpha1.BackupJob{}
err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, j)
if err != nil {
if apierrors.IsNotFound(err) {
logger.V(1).Info("BackupJob not found, skipping")
return ctrl.Result{}, nil
}
logger.Error(err, "failed to get BackupJob")
return ctrl.Result{}, err
}
if j.Spec.StrategyRef.APIGroup == nil {
logger.V(1).Info("BackupJob has nil StrategyRef.APIGroup, skipping", "backupjob", j.Name)
return ctrl.Result{}, nil
}
if *j.Spec.StrategyRef.APIGroup != strategyv1alpha1.GroupVersion.Group {
logger.V(1).Info("BackupJob StrategyRef.APIGroup doesn't match, skipping",
"backupjob", j.Name,
"expected", strategyv1alpha1.GroupVersion.Group,
"got", *j.Spec.StrategyRef.APIGroup)
return ctrl.Result{}, nil
}
logger.Info("processing BackupJob", "backupjob", j.Name, "strategyKind", j.Spec.StrategyRef.Kind)
switch j.Spec.StrategyRef.Kind {
case strategyv1alpha1.JobStrategyKind:
return r.reconcileJob(ctx, j)
case strategyv1alpha1.VeleroStrategyKind:
return r.reconcileVelero(ctx, j)
default:
logger.V(1).Info("BackupJob StrategyRef.Kind not supported, skipping",
"backupjob", j.Name,
"kind", j.Spec.StrategyRef.Kind,
"supported", []string{strategyv1alpha1.JobStrategyKind, strategyv1alpha1.VeleroStrategyKind})
return ctrl.Result{}, nil
}
}
// SetupWithManager registers our controller with the Manager and sets up watches.
func (r *BackupJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
cfg := mgr.GetConfig()
var err error
if r.Interface, err = dynamic.NewForConfig(cfg); err != nil {
return err
}
var h *http.Client
if h, err = rest.HTTPClientFor(cfg); err != nil {
return err
}
if r.RESTMapper, err = apiutil.NewDynamicRESTMapper(cfg, h); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&backupsv1alpha1.BackupJob{}).
Complete(r)
}

View File

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

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

View File

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

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

View File

@@ -134,7 +134,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid
createCustomColumnsOverride("factory-details-v1.services", []any{
createCustomColumnWithSpecificColor("Name", "Service", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
createStringColumn("ClusterIP", ".spec.clusterIP"),
createStringColumn("LoadbalancerIP", ".spec.loadBalancerIP"),
createStringColumn("LoadbalancerIP", ".status.loadBalancer.ingress[0].ip"),
createTimestampColumn("Created", ".metadata.creationTimestamp"),
}),

View File

@@ -1,160 +0,0 @@
package cozyvaluesreplicator
import (
"context"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// Reconciler fields this setup relies on.
type SecretReplicatorReconciler struct {
client.Client
Scheme *runtime.Scheme
// Source of truth:
SourceNamespace string
SecretName string
// Namespaces to replicate into:
// (e.g. labels.SelectorFromSet(labels.Set{"tenant":"true"}), or metav1.LabelSelectorAsSelector(...))
TargetNamespaceSelector labels.Selector
}
func (r *SecretReplicatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
// 1) Primary watch for requirement (b):
// Reconcile any Secret named r.SecretName in any namespace (includes source too).
// This keeps Secrets in cache and causes “copy changed -> reconcile it” to happen.
secretNameOnly := predicate.NewPredicateFuncs(func(obj client.Object) bool {
return obj.GetName() == r.SecretName
})
// 2) Secondary watch for requirement (c):
// When the *source* Secret changes, fan-out reconcile requests to every matching namespace.
onlySourceSecret := predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool { return isSourceSecret(e.Object, r) },
UpdateFunc: func(e event.UpdateEvent) bool { return isSourceSecret(e.ObjectNew, r) },
DeleteFunc: func(e event.DeleteEvent) bool { return isSourceSecret(e.Object, r) },
GenericFunc: func(e event.GenericEvent) bool {
return isSourceSecret(e.Object, r)
},
}
// Fan-out mapper for source Secret events -> one request per matching target namespace.
fanOutOnSourceSecret := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, _ client.Object) []reconcile.Request {
// List namespaces *from the cache* (because we also watch Namespaces below).
var nsList corev1.NamespaceList
if err := r.List(ctx, &nsList); err != nil {
// If list fails, best-effort: return nothing; reconcile will be retried by next event.
return nil
}
reqs := make([]reconcile.Request, 0, len(nsList.Items))
for i := range nsList.Items {
ns := &nsList.Items[i]
if ns.Name == r.SourceNamespace {
continue
}
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
continue
}
reqs = append(reqs, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: ns.Name,
Name: r.SecretName,
},
})
}
return reqs
})
// 3) Namespace watch for requirement (a):
// When a namespace is created/updated to match selector, enqueue reconcile for the Secret copy in that namespace.
enqueueOnNamespaceMatch := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
ns, ok := obj.(*corev1.Namespace)
if !ok {
return nil
}
if ns.Name == r.SourceNamespace {
return nil
}
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
return nil
}
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Namespace: ns.Name,
Name: r.SecretName,
},
}}
})
// Only trigger from namespace events where the label match may be (or become) true.
// (You can keep this simple; its fine if it fires on any update—your Reconcile should be idempotent.)
namespaceMayMatter := predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
ns, ok := e.Object.(*corev1.Namespace)
return ok && (r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)))
},
UpdateFunc: func(e event.UpdateEvent) bool {
oldNS, okOld := e.ObjectOld.(*corev1.Namespace)
newNS, okNew := e.ObjectNew.(*corev1.Namespace)
if !okOld || !okNew {
return false
}
// Fire if it matches now OR matched before (covers transitions both ways; reconcile can decide what to do).
oldMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(oldNS.Labels))
newMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(newNS.Labels))
return oldMatch || newMatch
},
DeleteFunc: func(event.DeleteEvent) bool { return false }, // nothing to do on namespace delete
GenericFunc: func(event.GenericEvent) bool { return false },
}
return ctrl.NewControllerManagedBy(mgr).
// (b) Watch all Secrets with the chosen name; this also ensures Secret objects are cached.
For(&corev1.Secret{}, builder.WithPredicates(secretNameOnly)).
// (c) Add a second watch on Secret, but only for the source secret, and fan-out to all namespaces.
Watches(
&corev1.Secret{},
fanOutOnSourceSecret,
builder.WithPredicates(onlySourceSecret),
).
// (a) Watch Namespaces so theyre cached and so “namespace appears / starts matching” enqueues reconcile.
Watches(
&corev1.Namespace{},
enqueueOnNamespaceMatch,
builder.WithPredicates(namespaceMayMatter),
).
Complete(r)
}
func isSourceSecret(obj client.Object, r *SecretReplicatorReconciler) bool {
if obj == nil {
return false
}
return obj.GetNamespace() == r.SourceNamespace && obj.GetName() == r.SecretName
}
func (r *SecretReplicatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
if req.Name != r.SecretName || req.Namespace == r.SourceNamespace {
return ctrl.Result{}, nil
}
originalSecret := &corev1.Secret{}
r.Get(ctx, types.NamespacedName{Namespace: r.SourceNamespace, Name: r.SecretName}, originalSecret)
replicatedSecret := originalSecret.DeepCopy()
replicatedSecret.Namespace = req.Namespace
r.Update(ctx, replicatedSecret)
return ctrl.Result{}, nil
}

View File

@@ -1,378 +0,0 @@
/*
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.
*/
package fluxinstall
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// Install installs Flux components using embedded manifests.
// It extracts the manifests and applies them to the cluster.
// The namespace is automatically determined from the Namespace object in the manifests.
func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifests func(string) error) error {
logger := log.FromContext(ctx)
// Create temporary directory for manifests
tmpDir, err := os.MkdirTemp("", "flux-install-*")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
// Extract embedded manifests (generated by cozypkg)
manifestsDir := filepath.Join(tmpDir, "manifests")
if err := os.MkdirAll(manifestsDir, 0755); err != nil {
return fmt.Errorf("failed to create manifests directory: %w", err)
}
if err := writeEmbeddedManifests(manifestsDir); err != nil {
return fmt.Errorf("failed to extract embedded manifests: %w", err)
}
// Find the manifest file (should be fluxcd.yaml from cozypkg)
manifestPath := filepath.Join(manifestsDir, "fluxcd.yaml")
if _, err := os.Stat(manifestPath); err != nil {
// Try to find any YAML file if fluxcd.yaml doesn't exist
entries, err := os.ReadDir(manifestsDir)
if err != nil {
return fmt.Errorf("failed to read manifests directory: %w", err)
}
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".yaml") {
manifestPath = filepath.Join(manifestsDir, entry.Name())
break
}
}
}
// Parse and apply manifests
objects, err := parseManifests(manifestPath)
if err != nil {
return fmt.Errorf("failed to parse manifests: %w", err)
}
if len(objects) == 0 {
return fmt.Errorf("no objects found in manifests")
}
// Inject KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT if set in operator environment
if err := injectKubernetesServiceEnv(objects); err != nil {
logger.Info("Failed to inject KUBERNETES_SERVICE_* env vars, continuing anyway", "error", err)
}
// Extract namespace from Namespace object in manifests
namespace, err := extractNamespace(objects)
if err != nil {
return fmt.Errorf("failed to extract namespace from manifests: %w", err)
}
logger.Info("Installing Flux components", "namespace", namespace)
// Apply manifests using server-side apply
logger.Info("Applying Flux manifests", "count", len(objects), "manifest", manifestPath, "namespace", namespace)
if err := applyManifests(ctx, k8sClient, objects); err != nil {
return fmt.Errorf("failed to apply manifests: %w", err)
}
logger.Info("Flux installation completed successfully")
return nil
}
// parseManifests parses YAML manifests into unstructured objects.
func parseManifests(manifestPath string) ([]*unstructured.Unstructured, error) {
data, err := os.ReadFile(manifestPath)
if err != nil {
return nil, fmt.Errorf("failed to read manifest file: %w", err)
}
return readYAMLObjects(bytes.NewReader(data))
}
// readYAMLObjects parses multi-document YAML into unstructured objects.
func readYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) {
var objects []*unstructured.Unstructured
yamlReader := k8syaml.NewYAMLReader(bufio.NewReader(reader))
for {
doc, err := yamlReader.Read()
if err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("failed to read YAML document: %w", err)
}
// Skip empty documents
if len(bytes.TrimSpace(doc)) == 0 {
continue
}
obj := &unstructured.Unstructured{}
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(doc), len(doc))
if err := decoder.Decode(obj); err != nil {
// Skip documents that can't be decoded (might be comments or empty)
if err == io.EOF {
continue
}
return nil, fmt.Errorf("failed to decode YAML document: %w", err)
}
// Skip empty objects (no kind)
if obj.GetKind() == "" {
continue
}
objects = append(objects, obj)
}
return objects, nil
}
// applyManifests applies Kubernetes objects using server-side apply.
func applyManifests(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error {
logger := log.FromContext(ctx)
// Separate CRDs and namespaces from other resources
var stageOne []*unstructured.Unstructured // CRDs and Namespaces
var stageTwo []*unstructured.Unstructured // Everything else
for _, obj := range objects {
if isClusterDefinition(obj) {
stageOne = append(stageOne, obj)
} else {
stageTwo = append(stageTwo, obj)
}
}
// Apply stage one (CRDs and Namespaces) first
if len(stageOne) > 0 {
logger.Info("Applying cluster definitions", "count", len(stageOne))
if err := applyObjects(ctx, k8sClient, stageOne); err != nil {
return fmt.Errorf("failed to apply cluster definitions: %w", err)
}
// Wait a bit for CRDs to be registered
time.Sleep(2 * time.Second)
}
// Apply stage two (everything else)
if len(stageTwo) > 0 {
logger.Info("Applying resources", "count", len(stageTwo))
if err := applyObjects(ctx, k8sClient, stageTwo); err != nil {
return fmt.Errorf("failed to apply resources: %w", err)
}
}
return nil
}
// applyObjects applies a list of objects using server-side apply.
func applyObjects(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error {
for _, obj := range objects {
// Use server-side apply with force ownership and field manager
// FieldManager is required for apply patch operations
patchOptions := &client.PatchOptions{
FieldManager: "cozystack-operator",
Force: func() *bool { b := true; return &b }(),
}
if err := k8sClient.Patch(ctx, obj, client.Apply, patchOptions); err != nil {
return fmt.Errorf("failed to apply object %s/%s: %w", obj.GetKind(), obj.GetName(), err)
}
}
return nil
}
// extractNamespace extracts the namespace name from the Namespace object in the manifests.
func extractNamespace(objects []*unstructured.Unstructured) (string, error) {
for _, obj := range objects {
if obj.GetKind() == "Namespace" {
namespace := obj.GetName()
if namespace == "" {
return "", fmt.Errorf("Namespace object has no name")
}
return namespace, nil
}
}
return "", fmt.Errorf("no Namespace object found in manifests")
}
// isClusterDefinition checks if an object is a CRD or Namespace.
func isClusterDefinition(obj *unstructured.Unstructured) bool {
kind := obj.GetKind()
return kind == "CustomResourceDefinition" || kind == "Namespace"
}
// injectKubernetesServiceEnv injects KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT
// environment variables into all containers of Deployment, StatefulSet, and DaemonSet objects
// if these variables are set in the operator's environment.
// Errors are logged but do not stop processing of other objects.
func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
kubernetesHost := os.Getenv("KUBERNETES_SERVICE_HOST")
kubernetesPort := os.Getenv("KUBERNETES_SERVICE_PORT")
// If neither variable is set, nothing to do
if kubernetesHost == "" && kubernetesPort == "" {
return nil
}
var firstErr error
for _, obj := range objects {
kind := obj.GetKind()
if kind != "Deployment" && kind != "StatefulSet" && kind != "DaemonSet" {
continue
}
// Navigate to spec.template.spec.containers
spec, found, err := unstructured.NestedMap(obj.Object, "spec", "template", "spec")
if !found {
continue
}
if err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to get spec for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
// Update containers
containers, found, err := unstructured.NestedSlice(spec, "containers")
if err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to get containers for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
if found {
containers = updateContainersEnv(containers, kubernetesHost, kubernetesPort)
if err := unstructured.SetNestedSlice(spec, containers, "containers"); err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to set containers for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
}
// Update initContainers
initContainers, found, err := unstructured.NestedSlice(spec, "initContainers")
if err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to get initContainers for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
if found {
initContainers = updateContainersEnv(initContainers, kubernetesHost, kubernetesPort)
if err := unstructured.SetNestedSlice(spec, initContainers, "initContainers"); err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to set initContainers for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
}
// Update spec in the object
if err := unstructured.SetNestedMap(obj.Object, spec, "spec", "template", "spec"); err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to update spec for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
}
return firstErr
}
// updateContainersEnv updates environment variables for a slice of containers.
func updateContainersEnv(containers []interface{}, kubernetesHost, kubernetesPort string) []interface{} {
for i, container := range containers {
containerMap, ok := container.(map[string]interface{})
if !ok {
continue
}
env, found, err := unstructured.NestedSlice(containerMap, "env")
if err != nil {
continue
}
if !found {
env = []interface{}{}
}
// Update or add KUBERNETES_SERVICE_HOST
if kubernetesHost != "" {
env = setEnvVar(env, "KUBERNETES_SERVICE_HOST", kubernetesHost)
}
// Update or add KUBERNETES_SERVICE_PORT
if kubernetesPort != "" {
env = setEnvVar(env, "KUBERNETES_SERVICE_PORT", kubernetesPort)
}
// Update the container's env
if err := unstructured.SetNestedSlice(containerMap, env, "env"); err != nil {
continue
}
// Update the container in the slice
containers[i] = containerMap
}
return containers
}
// setEnvVar updates or adds an environment variable in the env slice.
func setEnvVar(env []interface{}, name, value string) []interface{} {
// Check if variable already exists
for i, envVar := range env {
envVarMap, ok := envVar.(map[string]interface{})
if !ok {
continue
}
if envVarMap["name"] == name {
// Update existing variable
envVarMap["value"] = value
env[i] = envVarMap
return env
}
}
// Add new variable
env = append(env, map[string]interface{}{
"name": name,
"value": value,
})
return env
}

View File

@@ -1,51 +0,0 @@
/*
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.
*/
package fluxinstall
import (
"embed"
"fmt"
"io/fs"
"os"
"path"
)
//go:embed manifests/*.yaml
var embeddedFluxManifests embed.FS
// WriteEmbeddedManifests extracts embedded Flux manifests to a temporary directory.
func WriteEmbeddedManifests(dir string) error {
manifests, err := fs.ReadDir(embeddedFluxManifests, "manifests")
if err != nil {
return fmt.Errorf("failed to read embedded manifests: %w", err)
}
for _, manifest := range manifests {
data, err := fs.ReadFile(embeddedFluxManifests, path.Join("manifests", manifest.Name()))
if err != nil {
return fmt.Errorf("failed to read file %s: %w", manifest.Name(), err)
}
outputPath := path.Join(dir, manifest.Name())
if err := os.WriteFile(outputPath, data, 0666); err != nil {
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,944 +0,0 @@
/*
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.
*/
package operator
import (
"context"
"fmt"
"strings"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
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/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// PackageReconciler reconciles Package resources
type PackageReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=cozystack.io,resources=packages,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cozystack.io,resources=packages/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=cozystack.io,resources=packagesources,verbs=get;list;watch
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch
// Reconcile is part of the main kubernetes reconciliation loop
func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
pkg := &cozyv1alpha1.Package{}
if err := r.Get(ctx, req.NamespacedName, pkg); err != nil {
if apierrors.IsNotFound(err) {
// Resource not found, return (ownerReference will handle cleanup)
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// Get PackageSource with the same name
packageSource := &cozyv1alpha1.PackageSource{}
if err := r.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
if apierrors.IsNotFound(err) {
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "PackageSourceNotFound",
Message: fmt.Sprintf("PackageSource %s not found", pkg.Name),
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// Determine variant (default to "default" if not specified)
variantName := pkg.Spec.Variant
if variantName == "" {
variantName = "default"
}
// Find the variant in PackageSource
var variant *cozyv1alpha1.Variant
for i := range packageSource.Spec.Variants {
if packageSource.Spec.Variants[i].Name == variantName {
variant = &packageSource.Spec.Variants[i]
break
}
}
if variant == nil {
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "VariantNotFound",
Message: fmt.Sprintf("Variant %s not found in PackageSource %s", variantName, pkg.Name),
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
// Reconcile namespaces from components
if err := r.reconcileNamespaces(ctx, pkg, variant); err != nil {
logger.Error(err, "failed to reconcile namespaces")
return ctrl.Result{}, err
}
// Update dependencies status
if err := r.updateDependenciesStatus(ctx, pkg, variant); err != nil {
logger.Error(err, "failed to update dependencies status")
// Don't return error, continue with reconciliation
}
// Validate variant dependencies before creating HelmReleases
// Check if all dependencies are ready based on status
if !r.areDependenciesReady(pkg, variant) {
logger.Info("variant dependencies not ready, skipping HelmRelease creation", "package", pkg.Name)
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "DependenciesNotReady",
Message: "One or more dependencies are not ready",
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
// Return success to avoid requeue, but don't create HelmReleases
return ctrl.Result{}, nil
}
// Create HelmReleases for components with Install section
helmReleaseCount := 0
for _, component := range variant.Components {
// Skip components without Install section
if component.Install == nil {
continue
}
// Check if component is disabled via Package spec
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok {
if pkgComponent.Enabled != nil && !*pkgComponent.Enabled {
logger.V(1).Info("skipping disabled component", "package", pkg.Name, "component", component.Name)
continue
}
}
// Build artifact name: <packagesource>-<variant>-<componentname> (with dots replaced by dashes)
artifactName := fmt.Sprintf("%s-%s-%s",
strings.ReplaceAll(packageSource.Name, ".", "-"),
strings.ReplaceAll(variantName, ".", "-"),
strings.ReplaceAll(component.Name, ".", "-"))
// Namespace must be set
namespace := component.Install.Namespace
if namespace == "" {
logger.Error(fmt.Errorf("component %s has empty namespace in Install section", component.Name), "namespace validation failed")
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "InvalidConfiguration",
Message: fmt.Sprintf("Component %s has empty namespace in Install section", component.Name),
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, fmt.Errorf("component %s has empty namespace in Install section", component.Name)
}
// Determine release name (from Install or use component name)
releaseName := component.Install.ReleaseName
if releaseName == "" {
releaseName = component.Name
}
// Build labels
labels := make(map[string]string)
labels["cozystack.io/package"] = pkg.Name
if component.Install.Privileged {
labels["cozystack.io/privileged"] = "true"
}
// Create HelmRelease
hr := &helmv2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: releaseName,
Namespace: namespace,
Labels: labels,
},
Spec: helmv2.HelmReleaseSpec{
Interval: metav1.Duration{Duration: 5 * 60 * 1000000000}, // 5m
ChartRef: &helmv2.CrossNamespaceSourceReference{
Kind: "ExternalArtifact",
Name: artifactName,
Namespace: "cozy-system",
},
Install: &helmv2.Install{
Remediation: &helmv2.InstallRemediation{
Retries: -1,
},
},
Upgrade: &helmv2.Upgrade{
Remediation: &helmv2.UpgradeRemediation{
Retries: -1,
},
},
},
}
// Set ownerReference
gvk, err := apiutil.GVKForObject(pkg, r.Scheme)
if err != nil {
logger.Error(err, "failed to get GVK for Package")
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "InternalError",
Message: fmt.Sprintf("Failed to get GVK for Package: %v", err),
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, fmt.Errorf("failed to get GVK for Package: %w", err)
}
hr.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: pkg.Name,
UID: pkg.UID,
Controller: func() *bool { b := true; return &b }(),
},
}
// Merge values from Package spec if provided
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok && pkgComponent.Values != nil {
hr.Spec.Values = pkgComponent.Values
}
// Build DependsOn from component Install and variant DependsOn
dependsOn, err := r.buildDependsOn(ctx, pkg, packageSource, variant, &component)
if err != nil {
logger.Error(err, "failed to build DependsOn", "component", component.Name)
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "DependsOnFailed",
Message: fmt.Sprintf("Failed to build DependsOn for component %s: %v", component.Name, err),
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
// Return nil to stop reconciliation, error is recorded in status
return ctrl.Result{}, nil
}
if len(dependsOn) > 0 {
hr.Spec.DependsOn = dependsOn
}
// Set valuesFiles annotation
if len(component.ValuesFiles) > 0 {
if hr.Annotations == nil {
hr.Annotations = make(map[string]string)
}
hr.Annotations["cozyhr.cozystack.io/values-files"] = strings.Join(component.ValuesFiles, ",")
}
if err := r.createOrUpdateHelmRelease(ctx, hr); err != nil {
logger.Error(err, "failed to reconcile HelmRelease", "name", releaseName, "namespace", namespace)
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "HelmReleaseFailed",
Message: fmt.Sprintf("Failed to create HelmRelease %s: %v", releaseName, err),
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, err
}
helmReleaseCount++
logger.Info("reconciled HelmRelease", "package", pkg.Name, "component", component.Name, "releaseName", releaseName, "namespace", namespace)
}
// Cleanup orphaned HelmReleases
if err := r.cleanupOrphanedHelmReleases(ctx, pkg, variant); err != nil {
logger.Error(err, "failed to cleanup orphaned HelmReleases")
// Don't return error, continue with status update
}
// Update status with success message
message := fmt.Sprintf("reconciliation succeeded, generated %d helmrelease(s)", helmReleaseCount)
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "ReconciliationSucceeded",
Message: message,
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
logger.Info("reconciled Package", "name", pkg.Name, "helmReleaseCount", helmReleaseCount)
// Update dependencies status for Packages that depend on this Package
// This ensures they get re-enqueued when their dependency becomes ready
if err := r.updateDependentPackagesDependencies(ctx, pkg.Name); err != nil {
logger.V(1).Error(err, "failed to update dependent packages dependencies", "package", pkg.Name)
// Don't return error, this is best-effort
}
// Dependent Packages will be automatically enqueued by the watch handler
// when this Package's status is updated (see SetupWithManager watch handler)
return ctrl.Result{}, nil
}
// createOrUpdateHelmRelease creates or updates a HelmRelease
func (r *PackageReconciler) createOrUpdateHelmRelease(ctx context.Context, hr *helmv2.HelmRelease) error {
existing := &helmv2.HelmRelease{}
key := types.NamespacedName{
Name: hr.Name,
Namespace: hr.Namespace,
}
err := r.Get(ctx, key, existing)
if apierrors.IsNotFound(err) {
return r.Create(ctx, hr)
} else if err != nil {
return err
}
// Preserve resource version
hr.SetResourceVersion(existing.GetResourceVersion())
// Merge labels
labels := hr.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
for k, v := range existing.GetLabels() {
if _, ok := labels[k]; !ok {
labels[k] = v
}
}
hr.SetLabels(labels)
// Merge annotations
annotations := hr.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
for k, v := range existing.GetAnnotations() {
if _, ok := annotations[k]; !ok {
annotations[k] = v
}
}
hr.SetAnnotations(annotations)
// Update Spec
existing.Spec = hr.Spec
existing.SetLabels(hr.GetLabels())
existing.SetAnnotations(hr.GetAnnotations())
existing.SetOwnerReferences(hr.GetOwnerReferences())
return r.Update(ctx, existing)
}
// getVariantForPackage retrieves the Variant for a given Package
// Returns the Variant and an error if not found
// If c is nil, uses the reconciler's client
func (r *PackageReconciler) getVariantForPackage(ctx context.Context, pkg *cozyv1alpha1.Package, c client.Client) (*cozyv1alpha1.Variant, error) {
// Use provided client or fall back to reconciler's client
cl := c
if cl == nil {
cl = r.Client
}
// Determine variant name (default to "default" if not specified)
variantName := pkg.Spec.Variant
if variantName == "" {
variantName = "default"
}
// Get the PackageSource
packageSource := &cozyv1alpha1.PackageSource{}
if err := cl.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
if apierrors.IsNotFound(err) {
return nil, fmt.Errorf("PackageSource %s not found", pkg.Name)
}
return nil, fmt.Errorf("failed to get PackageSource %s: %w", pkg.Name, err)
}
// Find the variant in PackageSource
var variant *cozyv1alpha1.Variant
for i := range packageSource.Spec.Variants {
if packageSource.Spec.Variants[i].Name == variantName {
variant = &packageSource.Spec.Variants[i]
break
}
}
if variant == nil {
return nil, fmt.Errorf("variant %s not found in PackageSource %s", variantName, pkg.Name)
}
return variant, nil
}
// buildDependsOn builds DependsOn list for a component
// Includes:
// 1. Dependencies from component.Install.DependsOn (with namespace from referenced component)
// 2. Dependencies from variant.DependsOn (all components with Install from referenced Package)
func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha1.Package, packageSource *cozyv1alpha1.PackageSource, variant *cozyv1alpha1.Variant, component *cozyv1alpha1.Component) ([]helmv2.DependencyReference, error) {
logger := log.FromContext(ctx)
dependsOn := []helmv2.DependencyReference{}
// Build map of component names to their release names and namespaces in current variant
componentMap := make(map[string]struct {
releaseName string
namespace string
})
for _, comp := range variant.Components {
if comp.Install == nil {
continue
}
compNamespace := comp.Install.Namespace
if compNamespace == "" {
return nil, fmt.Errorf("component %s has empty namespace in Install section", comp.Name)
}
compReleaseName := comp.Install.ReleaseName
if compReleaseName == "" {
compReleaseName = comp.Name
}
componentMap[comp.Name] = struct {
releaseName string
namespace string
}{
releaseName: compReleaseName,
namespace: compNamespace,
}
}
// Add dependencies from component.Install.DependsOn
if len(component.Install.DependsOn) > 0 {
for _, depName := range component.Install.DependsOn {
depComp, ok := componentMap[depName]
if !ok {
return nil, fmt.Errorf("component %s not found in variant for dependency %s", depName, component.Name)
}
dependsOn = append(dependsOn, helmv2.DependencyReference{
Name: depComp.releaseName,
Namespace: depComp.namespace,
})
logger.V(1).Info("added component dependency", "component", component.Name, "dependsOn", depName, "releaseName", depComp.releaseName, "namespace", depComp.namespace)
}
}
// Add dependencies from variant.DependsOn
if len(variant.DependsOn) > 0 {
for _, depPackageName := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == depPackageName {
ignore = true
break
}
}
if ignore {
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
continue
}
// Get the Package
depPackage := &cozyv1alpha1.Package{}
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
if apierrors.IsNotFound(err) {
return nil, fmt.Errorf("dependent Package %s not found", depPackageName)
}
return nil, fmt.Errorf("failed to get dependent Package %s: %w", depPackageName, err)
}
// Get the variant from dependent Package
depVariant, err := r.getVariantForPackage(ctx, depPackage, nil)
if err != nil {
return nil, fmt.Errorf("failed to get variant for dependent Package %s: %w", depPackageName, err)
}
// Add all components with Install from dependent variant
for _, depComp := range depVariant.Components {
if depComp.Install == nil {
continue
}
// Check if component is disabled in dependent Package
if depPkgComponent, ok := depPackage.Spec.Components[depComp.Name]; ok {
if depPkgComponent.Enabled != nil && !*depPkgComponent.Enabled {
continue
}
}
depCompNamespace := depComp.Install.Namespace
if depCompNamespace == "" {
return nil, fmt.Errorf("component %s in dependent Package %s has empty namespace in Install section", depComp.Name, depPackageName)
}
depCompReleaseName := depComp.Install.ReleaseName
if depCompReleaseName == "" {
depCompReleaseName = depComp.Name
}
dependsOn = append(dependsOn, helmv2.DependencyReference{
Name: depCompReleaseName,
Namespace: depCompNamespace,
})
logger.V(1).Info("added variant dependency", "package", pkg.Name, "dependency", depPackageName, "component", depComp.Name, "releaseName", depCompReleaseName, "namespace", depCompNamespace)
}
}
}
return dependsOn, nil
}
// updateDependenciesStatus updates the dependencies status in Package status
// It checks the readiness of each dependency and updates pkg.Status.Dependencies
// Old dependency keys that are no longer in the dependency list are removed
func (r *PackageReconciler) updateDependenciesStatus(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
logger := log.FromContext(ctx)
// Initialize dependencies map if nil
if pkg.Status.Dependencies == nil {
pkg.Status.Dependencies = make(map[string]cozyv1alpha1.DependencyStatus)
}
// Build set of current dependencies (excluding ignored ones)
currentDeps := make(map[string]bool)
if len(variant.DependsOn) > 0 {
for _, depPackageName := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == depPackageName {
ignore = true
break
}
}
if ignore {
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
continue
}
currentDeps[depPackageName] = true
}
}
// Remove old dependencies that are no longer in the list
for depName := range pkg.Status.Dependencies {
if !currentDeps[depName] {
delete(pkg.Status.Dependencies, depName)
logger.V(1).Info("removed old dependency from status", "package", pkg.Name, "dependency", depName)
}
}
// Update status for each current dependency
for depPackageName := range currentDeps {
// Get the Package
depPackage := &cozyv1alpha1.Package{}
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
if apierrors.IsNotFound(err) {
// Dependency not found, mark as not ready
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
Ready: false,
}
logger.V(1).Info("dependency not found, marking as not ready", "package", pkg.Name, "dependency", depPackageName)
continue
}
// Error getting dependency, keep existing status or mark as not ready
if _, exists := pkg.Status.Dependencies[depPackageName]; !exists {
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
Ready: false,
}
}
logger.V(1).Error(err, "failed to get dependency, keeping existing status", "package", pkg.Name, "dependency", depPackageName)
continue
}
// Check Ready condition
readyCondition := meta.FindStatusCondition(depPackage.Status.Conditions, "Ready")
isReady := readyCondition != nil && readyCondition.Status == metav1.ConditionTrue
// Update dependency status
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
Ready: isReady,
}
logger.V(1).Info("updated dependency status", "package", pkg.Name, "dependency", depPackageName, "ready", isReady)
}
return nil
}
// areDependenciesReady checks if all dependencies are ready based on status
func (r *PackageReconciler) areDependenciesReady(pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) bool {
if len(variant.DependsOn) == 0 {
return true
}
for _, depPackageName := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == depPackageName {
ignore = true
break
}
}
if ignore {
continue
}
// Check dependency status
depStatus, exists := pkg.Status.Dependencies[depPackageName]
if !exists || !depStatus.Ready {
return false
}
}
return true
}
// updateDependentPackagesDependencies updates dependencies status for all Packages that depend on the given Package
// This ensures dependent packages get re-enqueued when their dependency status changes
func (r *PackageReconciler) updateDependentPackagesDependencies(ctx context.Context, packageName string) error {
logger := log.FromContext(ctx)
// Get all Packages
packageList := &cozyv1alpha1.PackageList{}
if err := r.List(ctx, packageList); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
// Get the updated Package to check its readiness
updatedPkg := &cozyv1alpha1.Package{}
if err := r.Get(ctx, types.NamespacedName{Name: packageName}, updatedPkg); err != nil {
if apierrors.IsNotFound(err) {
return nil // Package not found, nothing to update
}
return fmt.Errorf("failed to get Package %s: %w", packageName, err)
}
// Check Ready condition of the updated Package
readyCondition := meta.FindStatusCondition(updatedPkg.Status.Conditions, "Ready")
isReady := readyCondition != nil && readyCondition.Status == metav1.ConditionTrue
// For each Package, check if it depends on the given Package
for _, pkg := range packageList.Items {
// Skip the Package itself
if pkg.Name == packageName {
continue
}
// Get variant
variant, err := r.getVariantForPackage(ctx, &pkg, nil)
if err != nil {
// Continue if PackageSource or variant not found (best-effort operation)
logger.V(1).Info("skipping package, failed to get variant", "package", pkg.Name, "error", err)
continue
}
// Check if this Package depends on the given Package
dependsOn := false
for _, dep := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == dep {
ignore = true
break
}
}
if ignore {
continue
}
if dep == packageName {
dependsOn = true
break
}
}
if dependsOn {
// Update the dependency status in this Package
if pkg.Status.Dependencies == nil {
pkg.Status.Dependencies = make(map[string]cozyv1alpha1.DependencyStatus)
}
pkg.Status.Dependencies[packageName] = cozyv1alpha1.DependencyStatus{
Ready: isReady,
}
if err := r.Status().Update(ctx, &pkg); err != nil {
logger.V(1).Error(err, "failed to update dependency status for dependent Package", "package", pkg.Name, "dependency", packageName)
continue
}
logger.V(1).Info("updated dependency status for dependent Package", "package", pkg.Name, "dependency", packageName, "ready", isReady)
}
}
return nil
}
// reconcileNamespaces creates or updates namespaces based on components in the variant
func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
logger := log.FromContext(ctx)
// Collect namespaces from components
// Map: namespace -> {isPrivileged}
type namespaceInfo struct {
privileged bool
}
namespacesMap := make(map[string]namespaceInfo)
for _, component := range variant.Components {
// Skip components without Install section
if component.Install == nil {
continue
}
// Check if component is disabled via Package spec
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok {
if pkgComponent.Enabled != nil && !*pkgComponent.Enabled {
continue
}
}
// Namespace must be set
namespace := component.Install.Namespace
if namespace == "" {
return fmt.Errorf("component %s has empty namespace in Install section", component.Name)
}
info, exists := namespacesMap[namespace]
if !exists {
info = namespaceInfo{
privileged: false,
}
}
// If component is privileged, mark namespace as privileged
if component.Install.Privileged {
info.privileged = true
}
namespacesMap[namespace] = info
}
// Create or update all namespaces
for nsName, info := range namespacesMap {
namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
Labels: make(map[string]string),
Annotations: map[string]string{
"helm.sh/resource-policy": "keep",
},
},
}
// Add system label only for non-tenant namespaces
if !strings.HasPrefix(nsName, "tenant-") {
namespace.Labels["cozystack.io/system"] = "true"
}
// Add privileged label if needed
if info.privileged {
namespace.Labels["pod-security.kubernetes.io/enforce"] = "privileged"
}
if err := r.createOrUpdateNamespace(ctx, namespace); err != nil {
logger.Error(err, "failed to reconcile namespace", "name", nsName, "privileged", info.privileged)
return fmt.Errorf("failed to reconcile namespace %s: %w", nsName, err)
}
logger.Info("reconciled namespace", "name", nsName, "privileged", info.privileged)
}
return nil
}
// createOrUpdateNamespace creates or updates a namespace using server-side apply
func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace) error {
// Ensure TypeMeta is set for server-side apply
namespace.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Namespace"))
// Use server-side apply with field manager
// This is atomic and avoids race conditions from Get/Create/Update pattern
// Labels and annotations will be merged automatically by the server
// Each label/annotation key is treated as a separate field, so existing ones are preserved
return r.Patch(ctx, namespace, client.Apply, client.FieldOwner("cozystack-package-controller"))
}
// cleanupOrphanedHelmReleases removes HelmReleases that are no longer needed
func (r *PackageReconciler) cleanupOrphanedHelmReleases(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
logger := log.FromContext(ctx)
// Build map of desired HelmRelease names (from components with Install)
desiredReleases := make(map[types.NamespacedName]bool)
for _, component := range variant.Components {
if component.Install == nil {
continue
}
// Check if component is disabled via Package spec
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok {
if pkgComponent.Enabled != nil && !*pkgComponent.Enabled {
continue
}
}
namespace := component.Install.Namespace
if namespace == "" {
// Skip components with empty namespace (they shouldn't exist anyway)
continue
}
releaseName := component.Install.ReleaseName
if releaseName == "" {
releaseName = component.Name
}
desiredReleases[types.NamespacedName{
Name: releaseName,
Namespace: namespace,
}] = true
}
// Find all HelmReleases owned by this Package
hrList := &helmv2.HelmReleaseList{}
if err := r.List(ctx, hrList, client.MatchingLabels{
"cozystack.io/package": pkg.Name,
}); err != nil {
return err
}
// Delete HelmReleases that are not in desired list
for _, hr := range hrList.Items {
key := types.NamespacedName{
Name: hr.Name,
Namespace: hr.Namespace,
}
if !desiredReleases[key] {
logger.Info("deleting orphaned HelmRelease", "name", hr.Name, "namespace", hr.Namespace, "package", pkg.Name)
if err := r.Delete(ctx, &hr); err != nil && !apierrors.IsNotFound(err) {
logger.Error(err, "failed to delete orphaned HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
}
}
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *PackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Named("cozystack-package").
For(&cozyv1alpha1.Package{}).
Watches(
&cozyv1alpha1.PackageSource{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
ps, ok := obj.(*cozyv1alpha1.PackageSource)
if !ok {
return nil
}
// Find Package with the same name as PackageSource
// PackageSource and Package share the same name
pkg := &cozyv1alpha1.Package{}
if err := mgr.GetClient().Get(ctx, types.NamespacedName{Name: ps.Name}, pkg); err != nil {
// Package not found, that's ok - it might not exist yet
return nil
}
// Trigger reconcile for the corresponding Package
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Name: pkg.Name,
},
}}
}),
).
Watches(
&cozyv1alpha1.Package{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
updatedPkg, ok := obj.(*cozyv1alpha1.Package)
if !ok {
return nil
}
// Find all Packages that depend on this Package
packageList := &cozyv1alpha1.PackageList{}
if err := mgr.GetClient().List(ctx, packageList); err != nil {
return nil
}
var requests []reconcile.Request
for _, pkg := range packageList.Items {
if pkg.Name == updatedPkg.Name {
continue // Skip the Package itself
}
// Get variant to check dependencies
variant, err := r.getVariantForPackage(ctx, &pkg, mgr.GetClient())
if err != nil {
// Continue if PackageSource or variant not found
continue
}
// Check if this variant depends on updatedPkg
for _, dep := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == dep {
ignore = true
break
}
}
if ignore {
continue
}
if dep == updatedPkg.Name {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: pkg.Name,
},
})
break
}
}
}
return requests
}),
).
Complete(r)
}

View File

@@ -1,434 +0,0 @@
/*
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.
*/
package operator
import (
"context"
"fmt"
"strings"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
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/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// PackageSourceReconciler reconciles PackageSource resources
type PackageSourceReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=cozystack.io,resources=packagesources,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cozystack.io,resources=packagesources/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=source.extensions.fluxcd.io,resources=artifactgenerators,verbs=get;list;watch;create;update;patch;delete
// Reconcile is part of the main kubernetes reconciliation loop
func (r *PackageSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
packageSource := &cozyv1alpha1.PackageSource{}
if err := r.Get(ctx, req.NamespacedName, packageSource); err != nil {
if apierrors.IsNotFound(err) {
// Resource not found, return (ownerReference will handle cleanup)
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// Generate ArtifactGenerator for package source
if err := r.reconcileArtifactGenerators(ctx, packageSource); err != nil {
logger.Error(err, "failed to reconcile ArtifactGenerator")
return ctrl.Result{}, err
}
// Update PackageSource status (variants and conditions from ArtifactGenerator)
if err := r.updateStatus(ctx, packageSource); err != nil {
logger.Error(err, "failed to update status")
// Don't return error, status update is not critical
}
return ctrl.Result{}, nil
}
// reconcileArtifactGenerators generates a single ArtifactGenerator for the package source
// Creates one ArtifactGenerator per package source with all OutputArtifacts from components
func (r *PackageSourceReconciler) reconcileArtifactGenerators(ctx context.Context, packageSource *cozyv1alpha1.PackageSource) error {
logger := log.FromContext(ctx)
// Check if SourceRef is set
if packageSource.Spec.SourceRef == nil {
logger.Info("skipping ArtifactGenerator creation, SourceRef not set", "packageSource", packageSource.Name)
return nil
}
// Namespace is always cozy-system
namespace := "cozy-system"
// ArtifactGenerator name is the package source name
agName := packageSource.Name
// Collect all OutputArtifacts
outputArtifacts := []sourcewatcherv1beta1.OutputArtifact{}
// Process all variants and their components
for _, variant := range packageSource.Spec.Variants {
// Build library map for this variant
// Map key is the library name (from lib.Name or extracted from path)
// This allows components in this variant to reference libraries by name
// Libraries are scoped per variant to avoid conflicts between variants
libraryMap := make(map[string]cozyv1alpha1.Library)
for _, lib := range variant.Libraries {
libName := lib.Name
if libName == "" {
// If library name is not set, extract from path
libName = r.getPackageNameFromPath(lib.Path)
}
if libName != "" {
// Store library with the resolved name
libraryMap[libName] = lib
}
}
for _, component := range variant.Components {
// Skip components without path
if component.Path == "" {
logger.V(1).Info("skipping component without path", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name)
continue
}
logger.V(1).Info("processing component", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name, "path", component.Path)
// Extract component name from path (last component)
componentPathName := r.getPackageNameFromPath(component.Path)
if componentPathName == "" {
logger.Info("skipping component with invalid path", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name, "path", component.Path)
continue
}
// Get basePath with default values
basePath := r.getBasePath(packageSource)
// Build copy operations
copyOps := []sourcewatcherv1beta1.CopyOperation{
{
From: r.buildSourcePath(packageSource.Spec.SourceRef.Name, basePath, component.Path),
To: fmt.Sprintf("@artifact/%s/", componentPathName),
},
}
// Add libraries if specified
for _, libName := range component.Libraries {
if lib, ok := libraryMap[libName]; ok {
copyOps = append(copyOps, sourcewatcherv1beta1.CopyOperation{
From: r.buildSourcePath(packageSource.Spec.SourceRef.Name, basePath, lib.Path),
To: fmt.Sprintf("@artifact/%s/charts/%s/", componentPathName, libName),
})
}
}
// Add valuesFiles if specified
for i, valuesFile := range component.ValuesFiles {
strategy := "Merge"
if i == 0 {
strategy = "Overwrite"
}
copyOps = append(copyOps, sourcewatcherv1beta1.CopyOperation{
From: r.buildSourceFilePath(packageSource.Spec.SourceRef.Name, basePath, fmt.Sprintf("%s/%s", component.Path, valuesFile)),
To: fmt.Sprintf("@artifact/%s/values.yaml", componentPathName),
Strategy: strategy,
})
}
// Artifact name: <packagesource>-<variant>-<componentname>
// Replace dots with dashes to comply with Kubernetes naming requirements
artifactName := fmt.Sprintf("%s-%s-%s",
strings.ReplaceAll(packageSource.Name, ".", "-"),
strings.ReplaceAll(variant.Name, ".", "-"),
strings.ReplaceAll(component.Name, ".", "-"))
outputArtifacts = append(outputArtifacts, sourcewatcherv1beta1.OutputArtifact{
Name: artifactName,
Copy: copyOps,
})
logger.Info("added OutputArtifact for component", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name, "artifactName", artifactName)
}
}
// If there are no OutputArtifacts, return (ownerReference will handle cleanup if needed)
if len(outputArtifacts) == 0 {
logger.Info("no OutputArtifacts to generate, skipping ArtifactGenerator creation", "packageSource", packageSource.Name)
return nil
}
// Build labels
labels := make(map[string]string)
labels["cozystack.io/packagesource"] = packageSource.Name
// Create single ArtifactGenerator for the package source
ag := &sourcewatcherv1beta1.ArtifactGenerator{
ObjectMeta: metav1.ObjectMeta{
Name: agName,
Namespace: namespace,
Labels: labels,
},
Spec: sourcewatcherv1beta1.ArtifactGeneratorSpec{
Sources: []sourcewatcherv1beta1.SourceReference{
{
Alias: packageSource.Spec.SourceRef.Name,
Kind: packageSource.Spec.SourceRef.Kind,
Name: packageSource.Spec.SourceRef.Name,
Namespace: packageSource.Spec.SourceRef.Namespace,
},
},
OutputArtifacts: outputArtifacts,
},
}
// Set ownerReference
gvk, err := apiutil.GVKForObject(packageSource, r.Scheme)
if err != nil {
return fmt.Errorf("failed to get GVK for PackageSource: %w", err)
}
ag.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: packageSource.Name,
UID: packageSource.UID,
Controller: func() *bool { b := true; return &b }(),
},
}
logger.Info("creating ArtifactGenerator for package source", "packageSource", packageSource.Name, "agName", agName, "namespace", namespace, "outputArtifactCount", len(outputArtifacts))
if err := r.createOrUpdate(ctx, ag); err != nil {
return fmt.Errorf("failed to reconcile ArtifactGenerator %s: %w", agName, err)
}
logger.Info("reconciled ArtifactGenerator for package source", "name", agName, "namespace", namespace, "outputArtifactCount", len(outputArtifacts))
return nil
}
// Helper functions
func (r *PackageSourceReconciler) getPackageNameFromPath(path string) string {
parts := strings.Split(path, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return ""
}
// getBasePath returns the basePath with default values based on source kind
func (r *PackageSourceReconciler) getBasePath(packageSource *cozyv1alpha1.PackageSource) string {
// If path is explicitly set in SourceRef, use it (but normalize "/" to empty)
if packageSource.Spec.SourceRef.Path != "" {
path := strings.Trim(packageSource.Spec.SourceRef.Path, "/")
// If path is "/" or empty after trim, return empty string
if path == "" {
return ""
}
return path
}
// Default values based on kind
if packageSource.Spec.SourceRef.Kind == "OCIRepository" {
return "" // Root for OCI
}
// Default for GitRepository
return "packages"
}
// buildSourcePath builds the full source path using basePath with glob pattern
func (r *PackageSourceReconciler) buildSourcePath(sourceName, basePath, path string) string {
// Remove leading/trailing slashes and combine
parts := []string{}
if basePath != "" {
trimmed := strings.Trim(basePath, "/")
if trimmed != "" {
parts = append(parts, trimmed)
}
}
if path != "" {
trimmed := strings.Trim(path, "/")
if trimmed != "" {
parts = append(parts, trimmed)
}
}
fullPath := strings.Join(parts, "/")
if fullPath == "" {
return fmt.Sprintf("@%s/**", sourceName)
}
return fmt.Sprintf("@%s/%s/**", sourceName, fullPath)
}
// buildSourceFilePath builds the full source path for a specific file (without glob pattern)
func (r *PackageSourceReconciler) buildSourceFilePath(sourceName, basePath, path string) string {
// Remove leading/trailing slashes and combine
parts := []string{}
if basePath != "" {
trimmed := strings.Trim(basePath, "/")
if trimmed != "" {
parts = append(parts, trimmed)
}
}
if path != "" {
trimmed := strings.Trim(path, "/")
if trimmed != "" {
parts = append(parts, trimmed)
}
}
fullPath := strings.Join(parts, "/")
if fullPath == "" {
return fmt.Sprintf("@%s", sourceName)
}
return fmt.Sprintf("@%s/%s", sourceName, fullPath)
}
// createOrUpdate creates or updates a resource using server-side apply
func (r *PackageSourceReconciler) createOrUpdate(ctx context.Context, obj client.Object) error {
// Ensure TypeMeta is set for server-side apply
// Use type assertion to set GVK if the object supports it
if runtimeObj, ok := obj.(runtime.Object); ok {
gvk, err := apiutil.GVKForObject(obj, r.Scheme)
if err != nil {
return fmt.Errorf("failed to get GVK for object: %w", err)
}
runtimeObj.GetObjectKind().SetGroupVersionKind(gvk)
}
// Use server-side apply with field manager
// This is atomic and avoids race conditions from Get/Create/Update pattern
// Labels, annotations, and spec will be merged automatically by the server
// Each field is treated separately, so existing ones are preserved
return r.Patch(ctx, obj, client.Apply, client.FieldOwner("cozystack-packagesource-controller"))
}
// updateStatus updates PackageSource status (variants and conditions from ArtifactGenerator)
func (r *PackageSourceReconciler) updateStatus(ctx context.Context, packageSource *cozyv1alpha1.PackageSource) error {
logger := log.FromContext(ctx)
// Update variants in status from spec
variantNames := make([]string, 0, len(packageSource.Spec.Variants))
for _, variant := range packageSource.Spec.Variants {
variantNames = append(variantNames, variant.Name)
}
packageSource.Status.Variants = strings.Join(variantNames, ",")
// Check if SourceRef is set
if packageSource.Spec.SourceRef == nil {
// Set status to unknown if SourceRef is not set
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionUnknown,
Reason: "SourceRefNotSet",
Message: "SourceRef is not configured",
})
return r.Status().Update(ctx, packageSource)
}
// Get ArtifactGenerator
ag := &sourcewatcherv1beta1.ArtifactGenerator{}
agKey := types.NamespacedName{
Name: packageSource.Name,
Namespace: "cozy-system",
}
if err := r.Get(ctx, agKey, ag); err != nil {
if apierrors.IsNotFound(err) {
// ArtifactGenerator not found, set status to unknown
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionUnknown,
Reason: "ArtifactGeneratorNotFound",
Message: "ArtifactGenerator not found",
})
return r.Status().Update(ctx, packageSource)
}
return fmt.Errorf("failed to get ArtifactGenerator: %w", err)
}
// Find Ready condition in ArtifactGenerator
readyCondition := meta.FindStatusCondition(ag.Status.Conditions, "Ready")
if readyCondition == nil {
// No Ready condition in ArtifactGenerator, set status to unknown
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionUnknown,
Reason: "ArtifactGeneratorNotReady",
Message: "ArtifactGenerator Ready condition not found",
})
return r.Status().Update(ctx, packageSource)
}
// Copy Ready condition from ArtifactGenerator to PackageSource
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: readyCondition.Status,
Reason: readyCondition.Reason,
Message: readyCondition.Message,
ObservedGeneration: packageSource.Generation,
LastTransitionTime: readyCondition.LastTransitionTime,
})
logger.V(1).Info("updated PackageSource status from ArtifactGenerator",
"packageSource", packageSource.Name,
"status", readyCondition.Status,
"reason", readyCondition.Reason)
return r.Status().Update(ctx, packageSource)
}
// SetupWithManager sets up the controller with the Manager.
func (r *PackageSourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Named("cozystack-packagesource").
For(&cozyv1alpha1.PackageSource{}).
Watches(
&sourcewatcherv1beta1.ArtifactGenerator{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
ag, ok := obj.(*sourcewatcherv1beta1.ArtifactGenerator)
if !ok {
return nil
}
// Find the PackageSource that owns this ArtifactGenerator by ownerReference
for _, ownerRef := range ag.OwnerReferences {
if ownerRef.Kind == "PackageSource" {
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Name: ownerRef.Name,
},
}}
}
}
return nil
}),
).
Complete(r)
}

View File

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

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

View File

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

View File

@@ -145,31 +145,31 @@ See the reference for components utilized in this service:
### Kubernetes Control Plane Configuration
| Name | Description | Type | Value |
| --------------------------------------------------- | ------------------------------------------------ | ---------- | -------- |
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `medium` |
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
| Name | Description | Type | Value |
| --------------------------------------------------- | ------------------------------------------------ | ---------- | ------- |
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `large` |
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
## Parameter examples and reference

View File

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

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.0.0@sha256:5335c044313b69ee13b30ca4941687e509005e55f4ae25723861edbf2fbd6dd2
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.0.0@sha256:dee69d15fa8616aa6a1e5a67fc76370e7698a7f58b25e30650eb39c9fb826de8

View File

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

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:a09724a7f95283f9130b3da2a89d81c4c6051c6edf0392a81b6fc90f404b76b6
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:71a74ca30f75967bae309be2758f19aa3d37c60b19426b9b622ff1c33a80362f

View File

@@ -287,7 +287,7 @@
"resourcesPreset": {
"description": "Preset if `resources` omitted.",
"type": "string",
"default": "medium",
"default": "large",
"enum": [
"nano",
"micro",

View File

@@ -153,7 +153,7 @@ addons:
## @typedef {struct} APIServer - API Server configuration.
## @field {Resources} resources - CPU and memory resources for API Server.
## @field {ResourcesPreset} resourcesPreset="medium" - Preset if `resources` omitted.
## @field {ResourcesPreset} resourcesPreset="large" - Preset if `resources` omitted.
## @typedef {struct} ControllerManager - Controller Manager configuration.
## @field {Resources} resources - CPU and memory resources for Controller Manager.
@@ -182,7 +182,7 @@ controlPlane:
replicas: 2
apiServer:
resources: {}
resourcesPreset: "medium"
resourcesPreset: "large"
controllerManager:
resources: {}
resourcesPreset: "micro"

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/mariadb-backup:0.0.0@sha256:1c0beb1b23a109b0e13727b4c73d2c74830e11cede92858ab20101b66f45a858
ghcr.io/cozystack/cozystack/mariadb-backup:0.0.0@sha256:0ddbbec0568dcb9fbc317cd9cc654e826dbe88ba3f184fa9b6b58aacb93b4570

View File

@@ -12,10 +12,7 @@ apply:
diff:
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f-
update: update-old update-new
# TODO: remove old manifest after migration to cozystack-operator
update-old:
update:
timoni bundle build -f flux-aio.cue > templates/fluxcd.yaml
yq eval '(select(.kind == "Namespace") | .metadata.labels."pod-security.kubernetes.io/enforce") = "privileged"' -i templates/fluxcd.yaml
sed -i templates/fluxcd.yaml \
@@ -23,12 +20,3 @@ update-old:
-e 's|\.cluster\.local\.,||g' -e 's|\.cluster\.local\,||g' -e 's|\.cluster\.local\.||g' \
-e '/value: .svc/a \ {{- include "cozy.kubernetes_envs" . | nindent 12 }}' \
-e '/hostNetwork: true/i \ dnsPolicy: ClusterFirstWithHostNet'
update-new:
timoni bundle build -f flux-aio.cue > ../../../internal/fluxinstall/manifests/fluxcd.yaml
yq eval '(select(.kind == "Namespace") | .metadata.labels."pod-security.kubernetes.io/enforce") = "privileged"' -i ../../../internal/fluxinstall/manifests/fluxcd.yaml
sed -i ../../../internal/fluxinstall/manifests/fluxcd.yaml \
-e '/timoni/d' \
-e 's|\.cluster\.local\.,||g' -e 's|\.cluster\.local\,||g' -e 's|\.cluster\.local\.||g'
# TODO: solve dns issue with hostNetwork for installing helmreleases in tenant k8s clusters
#-e '/hostNetwork: true/i \ dnsPolicy: ClusterFirstWithHostNet'

View File

@@ -27,31 +27,3 @@ image-cozystack:
IMAGE="$(REGISTRY)/installer:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/installer.json -o json -r)" \
yq -i '.cozystack.image = strenv(IMAGE)' values.yaml
rm -f images/installer.json
update-version:
TAG="$(call settag,$(TAG))" \
yq -i '.cozystackOperator.cozystackVersion = strenv(TAG)' values.yaml
image-operator:
docker buildx build -f images/cozystack-operator/Dockerfile ../../.. \
--tag $(REGISTRY)/cozystack-operator:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/cozystack-operator:latest \
--cache-to type=inline \
--metadata-file images/cozystack-operator.json \
$(BUILDX_ARGS)
IMAGE="$(REGISTRY)/cozystack-operator:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/cozystack-operator.json -o json -r)" \
yq -i '.cozystackOperator.image = strenv(IMAGE)' values.yaml
rm -f images/cozystack-operator.json
image-packages: update-version
mkdir -p ../../../_out/assets images
flux push artifact \
oci://$(REGISTRY)/platform-packages:$(call settag,$(TAG)) \
--path=../../../packages \
--source=https://github.com/cozystack/cozystack \
--revision="$$(git describe --tags):$$(git rev-parse HEAD)" \
2>&1 | tee images/cozystack-packages.log
export REPO="oci://$(REGISTRY)/platform-packages"; \
export DIGEST=$$(awk -F@ '/artifact successfully pushed/ {print $$2}' images/cozystack-packages.log; rm -f images/cozystack-packages.log); \
test -n "$$DIGEST" && yq -i '.cozystackOperator.platformSource = (strenv(REPO) + "@" + strenv(DIGEST))' values.yaml

View File

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

View File

@@ -1,171 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.4
name: packages.cozystack.io
spec:
group: cozystack.io
names:
kind: Package
listKind: PackageList
plural: packages
shortNames:
- pkg
- pkgs
singular: package
scope: Cluster
versions:
- additionalPrinterColumns:
- description: Selected variant
jsonPath: .spec.variant
name: Variant
type: string
- description: Ready status
jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
- description: Ready message
jsonPath: .status.conditions[?(@.type=='Ready')].message
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: Package is the Schema for the packages API
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: PackageSpec defines the desired state of Package
properties:
components:
additionalProperties:
description: PackageComponent defines overrides for a specific component
properties:
enabled:
description: |-
Enabled indicates whether this component should be installed
If false, the component will be disabled even if it's defined in the PackageSource
type: boolean
values:
description: |-
Values contains Helm chart values as a JSON object
These values will be merged with the default values from the PackageSource
x-kubernetes-preserve-unknown-fields: true
type: object
description: |-
Components is a map of release name to component overrides
Allows overriding values and enabling/disabling specific components from the PackageSource
type: object
ignoreDependencies:
description: |-
IgnoreDependencies is a list of package source dependencies to ignore
Dependencies listed here will not be installed even if they are specified in the PackageSource
items:
type: string
type: array
variant:
description: |-
Variant is the name of the variant to use from the PackageSource
If not specified, defaults to "default"
type: string
type: object
status:
description: PackageStatus defines the observed state of Package
properties:
conditions:
description: Conditions represents the latest available observations
of a Package'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
dependencies:
additionalProperties:
description: DependencyStatus represents the readiness status of
a dependency
properties:
ready:
description: Ready indicates whether the dependency is ready
type: boolean
required:
- ready
type: object
description: |-
Dependencies tracks the readiness status of each dependency
Key is the dependency package name, value indicates if the dependency is ready
type: object
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -1,250 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.4
name: packagesources.cozystack.io
spec:
group: cozystack.io
names:
kind: PackageSource
listKind: PackageSourceList
plural: packagesources
shortNames:
- pks
singular: packagesource
scope: Cluster
versions:
- additionalPrinterColumns:
- description: Package variants (comma-separated)
jsonPath: .status.variants
name: Variants
type: string
- description: Ready status
jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
- description: Ready message
jsonPath: .status.conditions[?(@.type=='Ready')].message
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: PackageSource is the Schema for the packagesources API
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: PackageSourceSpec defines the desired state of PackageSource
properties:
sourceRef:
description: SourceRef is the source reference for the package source
charts
properties:
kind:
description: Kind of the source reference
enum:
- GitRepository
- OCIRepository
type: string
name:
description: Name of the source reference
type: string
namespace:
description: Namespace of the source reference
type: string
path:
description: |-
Path is the base path where packages are located in the source.
For GitRepository, defaults to "packages" if not specified.
For OCIRepository, defaults to empty string (root) if not specified.
type: string
required:
- kind
- name
- namespace
type: object
variants:
description: |-
Variants is a list of package source variants
Each variant defines components, applications, dependencies, and libraries for a specific configuration
items:
description: Variant defines a single variant configuration
properties:
components:
description: Components is a list of Helm releases to be installed
as part of this variant
items:
description: Component defines a single Helm release component
within a package source
properties:
install:
description: Install defines installation parameters for
this component
properties:
dependsOn:
description: DependsOn is a list of component names
that must be installed before this component
items:
type: string
type: array
namespace:
description: Namespace is the Kubernetes namespace
where the release will be installed
type: string
privileged:
description: Privileged indicates whether this release
requires privileged access
type: boolean
releaseName:
description: |-
ReleaseName is the name of the HelmRelease resource that will be created
If not specified, defaults to the component Name field
type: string
type: object
libraries:
description: |-
Libraries is a list of library names that this component depends on
These libraries must be defined at the variant level
items:
type: string
type: array
name:
description: Name is the unique identifier for this component
within the package source
type: string
path:
description: Path is the path to the Helm chart directory
type: string
valuesFiles:
description: ValuesFiles is a list of values file names
to use
items:
type: string
type: array
required:
- name
- path
type: object
type: array
dependsOn:
description: |-
DependsOn is a list of package source dependencies
For example: "cozystack.networking"
items:
type: string
type: array
libraries:
description: Libraries is a list of Helm library charts used
by components in this variant
items:
description: Library defines a Helm library chart
properties:
name:
description: Name is the optional name for library placed
in charts
type: string
path:
description: Path is the path to the library chart directory
type: string
required:
- path
type: object
type: array
name:
description: Name is the unique identifier for this variant
type: string
required:
- name
type: object
type: array
type: object
status:
description: PackageSourceStatus defines the observed state of PackageSource
properties:
conditions:
description: Conditions represents the latest available observations
of a PackageSource'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
variants:
description: |-
Variants is a comma-separated list of package variant names
This field is populated by the controller based on spec.variants keys
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -1,20 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: Package
metadata:
name: cozystack.cozystack-platform
spec:
variant: isp-full
components:
platform:
values:
publishing:
host: "dev5.infra.aenix.org"
apiServerEndpoint: "https://api.dev5.infra.aenix.org"
externalIPs:
- 10.4.0.94
- 10.4.0.179
- 10.4.0.26
authentication:
oidc:
enabled: true

View File

@@ -1,23 +0,0 @@
FROM golang:1.25-alpine as builder
ARG TARGETOS
ARG TARGETARCH
RUN apk add --no-cache make git
COPY . /src/
WORKDIR /src
RUN go mod download
# Build cozystack-operator
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-ldflags="-w -s" \
-o /cozystack-operator \
./cmd/cozystack-operator
FROM alpine:3.22
COPY --from=builder /cozystack-operator /usr/bin/cozystack-operator
ENTRYPOINT ["/usr/bin/cozystack-operator"]

View File

@@ -1,12 +0,0 @@
# Exclude everything except src directory
*
!src/**
!api/**
!cmd/**
!hack/**
!internal/**
!packages/**
!pkg/**
!scripts/**
!go.mod
!go.sum

View File

@@ -28,7 +28,7 @@ RUN go mod download
FROM alpine:3.22
RUN wget -O- https://github.com/cozystack/cozyhr/raw/refs/heads/main/hack/install.sh | sh -s -- -v 1.5.0
RUN wget -O- https://github.com/cozystack/cozyhr/raw/refs/heads/main/hack/install.sh | sh -s -- -v 1.6.1
RUN apk add --no-cache make kubectl helm coreutils git jq openssl

View File

@@ -1,122 +0,0 @@
{{- if .Values.cozystackOperator.enabled }}
---
apiVersion: v1
kind: Namespace
metadata:
name: cozy-system
labels:
cozystack.io/system: "true"
pod-security.kubernetes.io/enforce: privileged
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: cozystack
namespace: cozy-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cozystack
subjects:
- kind: ServiceAccount
name: cozystack
namespace: cozy-system
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cozystack-operator
namespace: cozy-system
spec:
replicas: 1
selector:
matchLabels:
app: cozystack-operator
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
template:
metadata:
labels:
app: cozystack-operator
spec:
serviceAccountName: cozystack
containers:
- name: cozystack-operator
image: "{{ .Values.cozystackOperator.image }}"
args:
- --leader-elect=true
- --install-flux=true
- --metrics-bind-address=0
- --health-probe-bind-address=
- --cozystack-version={{ .Values.cozystackOperator.cozystackVersion }}
{{- if .Values.cozystackOperator.disableTelemetry }}
- --disable-telemetry
{{- end }}
- --platform-source-name=cozystack-platform
- --platform-source-url={{ .Values.cozystackOperator.platformSourceUrl }}
{{- if .Values.cozystackOperator.platformSourceRef }}
- --platform-source-ref={{ .Values.cozystackOperator.platformSourceRef }}
{{- end }}
env:
- name: KUBERNETES_SERVICE_HOST
value: localhost
- name: KUBERNETES_SERVICE_PORT
value: "7445"
hostNetwork: true
tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoSchedule"
- key: "node.cilium.io/agent-not-ready"
operator: "Exists"
effect: "NoSchedule"
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.cozystack-platform
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
components:
- install:
namespace: cozy-system
releaseName: cozystack-platform
name: cozystack-platform
path: core/platform
valuesFiles:
- values.yaml
- name: isp-full
components:
- install:
namespace: cozy-system
releaseName: cozystack-platform
name: cozystack-platform
path: core/platform
valuesFiles:
- values.yaml
- values-isp-full.yaml
- name: isp-hosted
components:
- install:
namespace: cozy-system
releaseName: cozystack-platform
name: cozystack-platform
path: core/platform
valuesFiles:
- values.yaml
- values-isp-hosted.yaml
{{- end }}

View File

@@ -1,4 +1,3 @@
{{- if not .Values.cozystackOperator.enabled }}
---
apiVersion: v1
kind: Namespace
@@ -78,4 +77,3 @@ spec:
- key: "node.cilium.io/agent-not-ready"
operator: "Exists"
effect: "NoSchedule"
{{- end }}

View File

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

View File

@@ -1,8 +1,2 @@
cozystack:
image: ghcr.io/cozystack/cozystack/installer:v0.38.2@sha256:9ff92b655de6f9bea3cba4cd42dcffabd9aace6966dcfb1cc02dda2420ea4a15
cozystackOperator:
enabled: false
image: ghcr.io/cozystack/cozystack/cozystack-operator:latest@sha256:f7f6e0fd9e896b7bfa642d0bfa4378bc14e646bc5c2e86e2e09a82770ef33181
platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/platform-packages'
platformSourceRef: 'digest=sha256:0576491291b33936cdf770a5c5b5692add97339c1505fc67a92df9d69dfbfdf6'
cozystackVersion: latest
image: ghcr.io/cozystack/cozystack/installer:v0.40.5@sha256:2bb898e0c6da96fd36e6427f69cfbb706e2131dd825e5755d1b1fd32ad82c833

View File

@@ -357,6 +357,12 @@ releases:
privileged: true
dependsOn: [piraeus-operator,cilium,cert-manager,snapshot-controller]
- name: linstor-scheduler
releaseName: linstor-scheduler
chart: cozy-linstor-scheduler
namespace: cozy-linstor
dependsOn: [linstor,cert-manager]
- name: nfs-driver
releaseName: nfs-driver
chart: cozy-nfs-driver

View File

@@ -69,48 +69,42 @@ releases:
releaseName: cozystack
chart: cozy-cozy-proxy
namespace: cozy-system
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: cert-manager-crds
releaseName: cert-manager-crds
chart: cozy-cert-manager-crds
namespace: cozy-cert-manager
dependsOn: [cilium, kubeovn]
dependsOn: []
- name: cozystack-api
releaseName: cozystack-api
chart: cozy-cozystack-api
namespace: cozy-system
dependsOn: [cilium,kubeovn,cozystack-controller]
dependsOn: [cilium,kubeovn,multus,cozystack-controller]
- name: cozystack-controller
releaseName: cozystack-controller
chart: cozy-cozystack-controller
namespace: cozy-system
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
{{- if eq (index $cozyConfig.data "telemetry-enabled") "false" }}
values:
cozystackController:
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
namespace: cozy-system
dependsOn: [cozystack-controller,cilium,kubeovn,cert-manager]
dependsOn: [cozystack-controller,cilium,kubeovn,multus,cert-manager]
- name: cozystack-resource-definition-crd
releaseName: cozystack-resource-definition-crd
chart: cozystack-resource-definition-crd
namespace: cozy-system
dependsOn: [cilium,kubeovn,cozystack-api,cozystack-controller]
dependsOn: [cilium,kubeovn,multus,cozystack-api,cozystack-controller]
- name: bootbox-rd
releaseName: bootbox-rd
@@ -266,13 +260,13 @@ releases:
releaseName: cert-manager
chart: cozy-cert-manager
namespace: cozy-cert-manager
dependsOn: [cert-manager-crds]
dependsOn: [cilium,kubeovn,multus,cert-manager-crds]
- name: cert-manager-issuers
releaseName: cert-manager-issuers
chart: cozy-cert-manager-issuers
namespace: cozy-cert-manager
dependsOn: [cilium,kubeovn,cert-manager]
dependsOn: [cert-manager]
- name: prometheus-operator-crds
releaseName: prometheus-operator-crds
@@ -284,20 +278,20 @@ releases:
releaseName: metrics-server
chart: cozy-metrics-server
namespace: cozy-monitoring
dependsOn: [cilium,kubeovn,prometheus-operator-crds]
dependsOn: [cilium,kubeovn,multus,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,prometheus-operator-crds]
dependsOn: [cilium,kubeovn,multus,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, metrics-server]
dependsOn: [victoria-metrics-operator,vertical-pod-autoscaler-crds,metrics-server]
values:
scrapeRules:
etcd:
@@ -307,14 +301,14 @@ releases:
releaseName: kubevirt-operator
chart: cozy-kubevirt-operator
namespace: cozy-kubevirt
dependsOn: [cilium,kubeovn,victoria-metrics-operator]
dependsOn: [cilium,kubeovn,multus,victoria-metrics-operator]
- name: kubevirt
releaseName: kubevirt
chart: cozy-kubevirt
namespace: cozy-kubevirt
privileged: true
dependsOn: [cilium,kubeovn,kubevirt-operator]
dependsOn: [cilium,kubeovn,multus,kubevirt-operator]
{{- $cpuAllocationRatio := index $cozyConfig.data "cpu-allocation-ratio" }}
{{- if $cpuAllocationRatio }}
values:
@@ -325,19 +319,19 @@ releases:
releaseName: kubevirt-instancetypes
chart: cozy-kubevirt-instancetypes
namespace: cozy-kubevirt
dependsOn: [cilium,kubeovn,kubevirt-operator,kubevirt]
dependsOn: [cilium,kubeovn,multus,kubevirt-operator,kubevirt]
- name: kubevirt-cdi-operator
releaseName: kubevirt-cdi-operator
chart: cozy-kubevirt-cdi-operator
namespace: cozy-kubevirt-cdi
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: kubevirt-cdi
releaseName: kubevirt-cdi
chart: cozy-kubevirt-cdi
namespace: cozy-kubevirt-cdi
dependsOn: [cilium,kubeovn,kubevirt-cdi-operator]
dependsOn: [cilium,kubeovn,multus,kubevirt-cdi-operator]
- name: gpu-operator
releaseName: gpu-operator
@@ -345,7 +339,7 @@ releases:
namespace: cozy-gpu-operator
privileged: true
optional: true
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
valuesFiles:
- values.yaml
- values-talos.yaml
@@ -355,25 +349,25 @@ releases:
chart: cozy-metallb
namespace: cozy-metallb
privileged: true
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: etcd-operator
releaseName: etcd-operator
chart: cozy-etcd-operator
namespace: cozy-etcd-operator
dependsOn: [cilium,kubeovn,cert-manager]
dependsOn: [cilium,kubeovn,multus,cert-manager]
- name: grafana-operator
releaseName: grafana-operator
chart: cozy-grafana-operator
namespace: cozy-grafana-operator
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: mariadb-operator
releaseName: mariadb-operator
chart: cozy-mariadb-operator
namespace: cozy-mariadb-operator
dependsOn: [cilium,kubeovn,cert-manager,victoria-metrics-operator]
dependsOn: [cilium,kubeovn,multus,cert-manager,victoria-metrics-operator]
values:
mariadb-operator:
clusterName: {{ $clusterDomain }}
@@ -382,13 +376,13 @@ releases:
releaseName: postgres-operator
chart: cozy-postgres-operator
namespace: cozy-postgres-operator
dependsOn: [cilium,kubeovn,cert-manager]
dependsOn: [cilium,kubeovn,multus,cert-manager]
- name: kafka-operator
releaseName: kafka-operator
chart: cozy-kafka-operator
namespace: cozy-kafka-operator
dependsOn: [cilium,kubeovn,victoria-metrics-operator]
dependsOn: [cilium,kubeovn,multus,victoria-metrics-operator]
values:
strimzi-kafka-operator:
kubernetesServiceDnsDomain: {{ $clusterDomain }}
@@ -397,65 +391,71 @@ releases:
releaseName: clickhouse-operator
chart: cozy-clickhouse-operator
namespace: cozy-clickhouse-operator
dependsOn: [cilium,kubeovn,victoria-metrics-operator]
dependsOn: [cilium,kubeovn,multus,victoria-metrics-operator]
- name: foundationdb-operator
releaseName: foundationdb-operator
chart: cozy-foundationdb-operator
namespace: cozy-foundationdb-operator
dependsOn: [cilium,kubeovn,cert-manager]
dependsOn: [cilium,kubeovn,multus,cert-manager]
- name: rabbitmq-operator
releaseName: rabbitmq-operator
chart: cozy-rabbitmq-operator
namespace: cozy-rabbitmq-operator
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: redis-operator
releaseName: redis-operator
chart: cozy-redis-operator
namespace: cozy-redis-operator
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: piraeus-operator
releaseName: piraeus-operator
chart: cozy-piraeus-operator
namespace: cozy-linstor
dependsOn: [cilium,kubeovn,cert-manager,victoria-metrics-operator]
dependsOn: [cilium,kubeovn,multus,cert-manager,victoria-metrics-operator]
- name: linstor
releaseName: linstor
chart: cozy-linstor
namespace: cozy-linstor
privileged: true
dependsOn: [piraeus-operator,cilium,kubeovn,cert-manager,snapshot-controller]
dependsOn: [piraeus-operator,cilium,kubeovn,multus,cert-manager,snapshot-controller]
- name: linstor-scheduler
releaseName: linstor-scheduler
chart: cozy-linstor-scheduler
namespace: cozy-linstor
dependsOn: [linstor,cert-manager]
- name: nfs-driver
releaseName: nfs-driver
chart: cozy-nfs-driver
namespace: cozy-nfs-driver
privileged: true
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
optional: true
- name: snapshot-controller
releaseName: snapshot-controller
chart: cozy-snapshot-controller
namespace: cozy-snapshot-controller
dependsOn: [cilium,kubeovn,cert-manager-issuers]
dependsOn: [cilium,kubeovn,multus,cert-manager-issuers]
- name: objectstorage-controller
releaseName: objectstorage-controller
chart: cozy-objectstorage-controller
namespace: cozy-objectstorage-controller
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: telepresence
releaseName: traffic-manager
chart: cozy-telepresence
namespace: cozy-telepresence
optional: true
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: dashboard
releaseName: dashboard
@@ -468,6 +468,7 @@ releases:
dependsOn:
- cilium
- kubeovn
- multus
{{- if eq $oidcEnabled "true" }}
- keycloak-configure
{{- end }}
@@ -476,56 +477,56 @@ releases:
releaseName: kamaji
chart: cozy-kamaji
namespace: cozy-kamaji
dependsOn: [cilium,kubeovn,cert-manager]
dependsOn: [cilium,kubeovn,multus,cert-manager]
- name: capi-operator
releaseName: capi-operator
chart: cozy-capi-operator
namespace: cozy-cluster-api
privileged: true
dependsOn: [cilium,kubeovn,cert-manager]
dependsOn: [cilium,kubeovn,multus,cert-manager]
- name: capi-providers-bootstrap
releaseName: capi-providers-bootstrap
chart: cozy-capi-providers-bootstrap
namespace: cozy-cluster-api
privileged: true
dependsOn: [cilium,kubeovn,capi-operator]
dependsOn: [cilium,kubeovn,multus,capi-operator]
- name: capi-providers-core
releaseName: capi-providers-core
chart: cozy-capi-providers-core
namespace: cozy-cluster-api
privileged: true
dependsOn: [cilium,kubeovn,capi-operator]
dependsOn: [cilium,kubeovn,multus,capi-operator]
- name: capi-providers-cpprovider
releaseName: capi-providers-cpprovider
chart: cozy-capi-providers-cpprovider
namespace: cozy-cluster-api
privileged: true
dependsOn: [cilium,kubeovn,capi-operator]
dependsOn: [cilium,kubeovn,multus,capi-operator]
- name: capi-providers-infraprovider
releaseName: capi-providers-infraprovider
chart: cozy-capi-providers-infraprovider
namespace: cozy-cluster-api
privileged: true
dependsOn: [cilium,kubeovn,capi-operator]
dependsOn: [cilium,kubeovn,multus,capi-operator]
- name: external-dns
releaseName: external-dns
chart: cozy-external-dns
namespace: cozy-external-dns
optional: true
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: external-secrets-operator
releaseName: external-secrets-operator
chart: cozy-external-secrets-operator
namespace: cozy-external-secrets-operator
optional: true
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
- name: bootbox
releaseName: bootbox
@@ -533,7 +534,7 @@ releases:
namespace: cozy-bootbox
privileged: true
optional: true
dependsOn: [cilium,kubeovn]
dependsOn: [cilium,kubeovn,multus]
{{- if $oidcEnabled }}
- name: keycloak
@@ -582,7 +583,7 @@ releases:
chart: cozy-vertical-pod-autoscaler-crds
namespace: cozy-vertical-pod-autoscaler
privileged: true
dependsOn: [cilium, kubeovn]
dependsOn: []
- name: reloader
releaseName: reloader

View File

@@ -38,11 +38,6 @@ 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

View File

@@ -1,22 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.backup-controller
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
components:
- name: backup-controller
path: system/backup-controller
install:
privileged: true
namespace: cozy-backup-controller
releaseName: backup-controller

View File

@@ -1,32 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.bootbox-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: bootbox-system
path: system/bootbox
install:
releaseName: bootbox-application
namespace: cozy-system
- name: bootbox
path: extra/bootbox
libraries: ["cozy-lib"]
- name: bootbox-rd
path: system/bootbox-rd
install:
namespace: cozy-system
releaseName: bootbox-rd

View File

@@ -1,23 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.bootbox
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.bootbox-application
components:
- name: bootbox
path: system/bootbox
install:
privileged: true
namespace: cozy-bootbox
releaseName: bootbox

View File

@@ -1,30 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.bucket-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.objectstorage-controller
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: bucket-system
path: system/bucket
- name: bucket
path: apps/bucket
libraries: ["cozy-lib"]
- name: bucket-rd
path: system/bucket-rd
install:
namespace: cozy-system
releaseName: bucket-rd

View File

@@ -1,22 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.capi-operator
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
components:
- name: capi-operator
path: system/capi-operator
install:
privileged: true
namespace: cozy-cluster-api
releaseName: capi-operator

View File

@@ -1,23 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.capi-providers-bootstrap
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: kubevirt
dependsOn:
- cozystack.networking
- cozystack.capi-operator
components:
- name: capi-providers-bootstrap
path: system/capi-providers-bootstrap
install:
privileged: true
namespace: cozy-cluster-api
releaseName: capi-providers-bootstrap

View File

@@ -1,23 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.capi-providers-core
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.capi-operator
components:
- name: capi-providers-core
path: system/capi-providers-core
install:
privileged: true
namespace: cozy-cluster-api
releaseName: capi-providers-core

View File

@@ -1,24 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.capi-providers-cpprovider
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: kamaji
dependsOn:
- cozystack.networking
- cozystack.capi-operator
- cozystack.kamaji
components:
- name: capi-providers-cpprovider
path: system/capi-providers-cpprovider
install:
privileged: true
namespace: cozy-cluster-api
releaseName: capi-providers-cpprovider

View File

@@ -1,24 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.capi-providers-infraprovider
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: kubevirt
dependsOn:
- cozystack.networking
- cozystack.capi-operator
- cozystack.kubevirt
components:
- name: capi-providers-infraprovider
path: system/capi-providers-infraprovider
install:
privileged: true
namespace: cozy-cluster-api
releaseName: capi-providers-infraprovider

View File

@@ -1,35 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.cert-manager
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
components:
- name: cert-manager-crds
path: system/cert-manager-crds
install:
namespace: cozy-cert-manager
releaseName: cert-manager-crds
- name: cert-manager
path: system/cert-manager
install:
namespace: cozy-cert-manager
releaseName: cert-manager
dependsOn:
- cert-manager-crds
- name: cert-manager-issuers
path: system/cert-manager-issuers
install:
namespace: cozy-cert-manager
releaseName: cert-manager-issuers
dependsOn:
- cert-manager

View File

@@ -1,27 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.clickhouse-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.clickhouse-operator
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: clickhouse
path: apps/clickhouse
libraries: ["cozy-lib"]
- name: clickhouse-rd
path: system/clickhouse-rd
install:
namespace: cozy-system
releaseName: clickhouse-rd

View File

@@ -1,22 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.clickhouse-operator
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.prometheus-operator-crds
components:
- name: clickhouse-operator
path: system/clickhouse-operator
install:
namespace: cozy-clickhouse-operator
releaseName: clickhouse-operator

View File

@@ -1,21 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.cozy-proxy
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
components:
- name: cozy-proxy
path: system/cozy-proxy
install:
namespace: cozy-system
releaseName: cozystack

View File

@@ -1,21 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.cozystack-basics
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.tenant-application
components:
- name: cozystack-basics
path: system/cozystack-basics
install:
namespace: cozy-system
releaseName: cozystack-basics

View File

@@ -1,108 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.cozystack-engine
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: cozystack-resource-definition-crd
path: system/cozystack-resource-definition-crd
install:
namespace: cozy-system
releaseName: cozystack-resource-definition-crd
- name: cozystack-controller
path: system/cozystack-controller
install:
namespace: cozy-system
releaseName: cozystack-controller
dependsOn:
- cozystack-resource-definition-crd
- name: cozystack-api
path: system/cozystack-api
install:
namespace: cozy-system
releaseName: cozystack-api
dependsOn:
- cozystack-controller
- cozystack-resource-definition-crd
- name: lineage-controller-webhook
path: system/lineage-controller-webhook
install:
namespace: cozy-system
releaseName: lineage-controller-webhook
dependsOn:
- cozystack-controller
- cozystack-resource-definition-crd
- name: dashboard
path: system/dashboard
install:
namespace: cozy-dashboard
releaseName: dashboard
dependsOn:
- cozystack-api
- cozystack-controller
- cozystack-resource-definition-crd
- name: oidc
dependsOn:
- cozystack.networking
- cozystack.keycloak
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: cozystack-resource-definition-crd
path: system/cozystack-resource-definition-crd
install:
namespace: cozy-system
releaseName: cozystack-resource-definition-crd
- name: cozystack-controller
path: system/cozystack-controller
install:
namespace: cozy-system
releaseName: cozystack-controller
dependsOn:
- cozystack-resource-definition-crd
- name: cozystack-api
path: system/cozystack-api
install:
namespace: cozy-system
releaseName: cozystack-api
dependsOn:
- cozystack-controller
- cozystack-resource-definition-crd
- name: lineage-controller-webhook
path: system/lineage-controller-webhook
install:
namespace: cozy-system
releaseName: lineage-controller-webhook
dependsOn:
- cozystack-controller
- cozystack-resource-definition-crd
- name: dashboard
path: system/dashboard
install:
namespace: cozy-dashboard
releaseName: dashboard
dependsOn:
- cozystack-api
- cozystack-controller
- keycloak-configure
- cozystack-resource-definition-crd
- name: keycloak-configure
path: system/keycloak-configure
install:
namespace: cozy-keycloak
releaseName: keycloak-configure
dependsOn: []

View File

@@ -1,27 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.etcd-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: etcd
path: extra/etcd
libraries: ["cozy-lib"]
- name: etcd-rd
path: system/etcd-rd
install:
namespace: cozy-system
releaseName: etcd-rd

View File

@@ -1,21 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.external-dns
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
components:
- name: external-dns
path: system/external-dns
install:
namespace: cozy-external-dns
releaseName: external-dns

View File

@@ -1,21 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.external-secrets-operator
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
components:
- name: external-secrets-operator
path: system/external-secrets-operator
install:
namespace: cozy-external-secrets-operator
releaseName: external-secrets-operator

View File

@@ -1,27 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.ferretdb-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: ferretdb
path: apps/ferretdb
libraries: [cozy-lib]
- name: ferretdb-rd
path: system/ferretdb-rd
install:
namespace: cozy-system
releaseName: ferretdb-rd

View File

@@ -1,28 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.foundationdb-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.foundationdb-operator
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: foundationdb
path: apps/foundationdb
libraries: ["cozy-lib"]
- name: foundationdb-rd
path: system/foundationdb-rd
install:
namespace: cozy-system
releaseName: foundationdb-rd

View File

@@ -1,22 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.foundationdb-operator
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.cert-manager
components:
- name: foundationdb-operator
path: system/foundationdb-operator
install:
namespace: cozy-foundationdb-operator
releaseName: foundationdb-operator

View File

@@ -1,23 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.goldpinger
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.prometheus-operator-crds
components:
- name: goldpinger
path: system/goldpinger
install:
privileged: true
namespace: cozy-goldpinger
releaseName: goldpinger

View File

@@ -1,25 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.gpu-operator
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
components:
- name: gpu-operator
path: system/gpu-operator
valuesFiles:
- values.yaml
- values-talos.yaml
install:
privileged: true
namespace: cozy-gpu-operator
releaseName: gpu-operator

View File

@@ -1,21 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.grafana-operator
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
components:
- name: grafana-operator
path: system/grafana-operator
install:
namespace: cozy-grafana-operator
releaseName: grafana-operator

View File

@@ -1,21 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.hetzner-robotlb
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
components:
- name: hetzner-robotlb
path: system/hetzner-robotlb
install:
namespace: cozy-hetzner-robotlb
releaseName: robotlb

View File

@@ -1,27 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.http-cache-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: http-cache
path: apps/http-cache
libraries: [cozy-lib]
- name: http-cache-rd
path: system/http-cache-rd
install:
namespace: cozy-system
releaseName: http-cache-rd

View File

@@ -1,27 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.info-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: info
path: extra/info
libraries: ["cozy-lib"]
- name: info-rd
path: system/info-rd
install:
namespace: cozy-system
releaseName: info-rd

View File

@@ -1,29 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.ingress-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: ingress-system
path: system/ingress-nginx
- name: ingress
path: extra/ingress
libraries: ["cozy-lib"]
- name: ingress-rd
path: system/ingress-rd
install:
namespace: cozy-system
releaseName: ingress-rd

View File

@@ -1,28 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.kafka-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.kafka-operator
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: kafka
path: apps/kafka
libraries: ["cozy-lib"]
- name: kafka-rd
path: system/kafka-rd
install:
namespace: cozy-system
releaseName: kafka-rd

View File

@@ -1,22 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.kafka-operator
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.prometheus-operator-crds
components:
- name: kafka-operator
path: system/kafka-operator
install:
namespace: cozy-kafka-operator
releaseName: kafka-operator

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