Compare commits

..

96 Commits

Author SHA1 Message Date
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
Timofei Larkin
38d6b98a70 [kubeovn] Package from external repo (#1535)
## What this PR does

Previously, all system charts have had their versions bumped with every
update of Cozystack, even if the underlying images and templates did not
change. Additionally, some components would have their docker images
rebuilt and get updated digests because of a changed docker context,
even though no relevant code had been changed. This would cause an
upgrade of several HelmReleases on every upgrade of Cozystack, even when
in practice nothing had changed. This is especially noticeable for
charts, such as kubeovn, which is a dependency for almost all other
system HelmReleases. By moving kubeovn out of the monorepo and
decoupling its release cycle from the release cycle of Cozystack, this
issue can be resolved and is another step towards decomposing Cozystack.

### Release note

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

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

Main Cozystack bundles were updated to reflect packages changes. 

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

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

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

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

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

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-05 14:44:53 +01:00
Andrei Kvapil
1ee6eb8482 Update Flux Operator (v0.33.0) (#1649)
https://github.com/fluxcd/flux2/discussions/5572

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

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

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

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

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

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

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

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-05 14:42:43 +01:00
Andrei Kvapil
cbfa99148b [monitoring] Improve tenant metrics collection (#1684)
This patch introduces vmagent support inside tenant-level monitoring
stacks, ensuring that all tenant workloads have consistent and reliable
metrics scraping. By deploying a vmagent per tenant and wiring it to the
configured VictoriaMetrics short- and long-term storage, metrics are now
properly collected, labeled, and remote-written. This resolves gaps
where tenant environments previously missed metrics due to the absence
of dedicated agents.

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

## What this PR does


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

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

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

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

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-05 14:41:10 +01:00
Andrei Kvapil
54d0f52245 [postgres] Add version management system with automated version updates (#1671)
This PR adds a version management system for PostgreSQL that
automatically fetches and updates supported versions from the
CloudNativePG PostgreSQL container registry.

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

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

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

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

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

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

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-05 14:38:50 +01:00
Andrei Kvapil
d0bfb6e2fc [kubernetes] Add version management system with automated version updates (#1672)
This PR adds a version management system for Kubernetes that
automatically fetches and updates supported versions based on kamaji.

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

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

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

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

## Summary by CodeRabbit

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

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

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-05 14:38:22 +01:00
Andrei Kvapil
f86896eceb [redis] Add version management system with automated version updates (#1681)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does


### Release note

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

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

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

## What this PR does


### Release note

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

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

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

## Summary by CodeRabbit

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

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

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

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

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2025-12-03 22:18:19 +03:00
nbykov0
153635379a [system] separate prometheus-operator-crds
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-03 16:36:14 +03:00
nbykov0
fbb2ea095a [apps:kubernetes] separate metrics-server
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-03 16:36:03 +03:00
nbykov0
50c1d1a067 [core:bundles] separate metrics-server
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-02 21:02:39 +03:00
Nikita
52ebcae8a2 [core:installer] Address buildx warnings (#1682)
## What this PR does
Buildx is worried about Dockerfile syntax, this pr fixes it.
```
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)
 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 16)
```

### Release note
```release-note
Buildx warnings addressed.
```
2025-12-02 18:10:26 +03:00
nbykov0
5314d61987 [core:installer] Fix buildx warnings
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-02 17:34:07 +03:00
Nikita
8b29c53a45 [system:coredns] update coredns app labels to match Talos coredns labels (#1675)
## What this PR does
Updates coredns app labels to match Talos coredns labels

### Release note

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

### Release note
```release-note
Renamed coredns metrics service not to interfere with coredns service used for name resolution in tenant k8s clusters.
```
2025-12-02 12:55:29 +03:00
nbykov0
f2a4f1b1c8 [system:coredns] rename service to kube-dns
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-02 12:54:36 +03:00
Andrei Kvapil
33128748e6 [mariadb] Add version management system with automated version updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-02 00:36:00 +01:00
Andrei Kvapil
7e1cad26e7 [redis] Add version management system with automated version updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-02 00:34:52 +01:00
Andrei Kvapil
3d5118f5b3 [postgres] Add version management system with automated version updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-01 23:56:55 +01:00
Andrei Kvapil
06a25c1c45 [kubernetes] Add version management system with automated version updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-01 23:55:27 +01:00
nbykov0
67e47256e2 [system:monitoring-agents] separate metrics-server
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-01 20:51:26 +03:00
nbykov0
df277b350c [system:coredns] update app coredns labels to match Talos coredns labels
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-01 17:51:27 +03:00
nbykov0
644d71eef7 [system:monitoring-agents] rename coredns metrics service
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-01 17:50:02 +03:00
Kingdon B
8e351f1827 Upgrade Flux to 2.7 series
Flux 2.7.3 is currently the latest release, (I think Flux 2.7.4 is still
just around the corner.)

Signed-off-by: Kingdon B <kingdon@urmanac.com>
2025-11-23 19:40:08 -05:00
Kingdon B
38a4adfaa3 update flux-instance chart as well (v0.33.0)
Signed-off-by: Kingdon B <kingdon@urmanac.com>
2025-11-23 19:39:57 -05:00
Timofei Larkin
03885f5ae2 Merge branch 'main' into feat/external-kubeovn-build 2025-11-19 07:46:51 +03:00
Timofei Larkin
d26d3e1f40 [kubeovn] Package from external repo
Previously, all system charts have had their versions bumped with every
update of Cozystack, even if the underlying images and templates did not
change. Additionally, some components would have their docker images
rebuilt and get updated digests because of a changed docker context,
even though no relevant code had been changed. This would cause an
upgrade of several HelmReleases on every upgrade of Cozystack, even when
in practice nothing had changed. This is especially noticeable for
charts, such as kubeovn, which is a dependency for almost all other
system HelmReleases. By moving kubeovn out of the monorepo and
decoupling its release cycle from the release cycle of Cozystack, this
issue can be resolved and is another step towards decomposing Cozystack.

```release-note
[kubeovn] Decouple the release cycle of kubeovn from Cozystack's release
cycle, preventing unnecessary redeployments of all Helm releases, when
upgrading Cozystack.
```

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

188
.github/workflows/auto-release.yaml vendored Normal file
View File

@@ -0,0 +1,188 @@
name: Auto Patch Release
on:
schedule:
# Run daily at 2:00 AM CET (1:00 UTC in winter, 0:00 UTC in summer)
# Using 1:00 UTC to approximate 2:00 AM CET
- cron: '0 1 * * *'
workflow_dispatch: # Allow manual trigger
concurrency:
group: auto-release-${{ github.workflow }}
cancel-in-progress: false
jobs:
auto-release:
name: Auto Patch Release
runs-on: [self-hosted]
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Configure git
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
git config --unset-all http.https://github.com/.extraheader || true
- name: Process release branches
uses: actions/github-script@v7
env:
GH_PAT: ${{ secrets.GH_PAT }}
with:
github-token: ${{ secrets.GH_PAT }}
script: |
const { execSync } = require('child_process');
// Configure git to use PAT for authentication
execSync('git config user.name "cozystack-bot"', { encoding: 'utf8' });
execSync('git config user.email "217169706+cozystack-bot@users.noreply.github.com"', { encoding: 'utf8' });
execSync(`git remote set-url origin https://cozystack-bot:${process.env.GH_PAT}@github.com/${process.env.GITHUB_REPOSITORY}`, { encoding: 'utf8' });
// Remove GITHUB_TOKEN extraheader to ensure PAT is used (needed to trigger other workflows)
execSync('git config --unset-all http.https://github.com/.extraheader || true', { encoding: 'utf8' });
// Get all release-X.Y branches
const branches = execSync('git branch -r | grep -E "origin/release-[0-9]+\\.[0-9]+$" | sed "s|origin/||" | tr -d " "', { encoding: 'utf8' })
.split('\n')
.filter(b => b.trim())
.filter(b => /^release-\d+\.\d+$/.test(b));
console.log(`Found ${branches.length} release branches: ${branches.join(', ')}`);
// Get all published releases (not draft)
const allReleases = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
});
// Filter to only published releases (not draft) with tags matching vX.Y.Z (no suffixes)
const publishedReleases = allReleases.data
.filter(r => !r.draft)
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name));
console.log(`Found ${publishedReleases.length} published releases without suffixes`);
for (const branch of branches) {
console.log(`\n=== Processing branch: ${branch} ===`);
// Extract X.Y from branch name (release-X.Y)
const match = branch.match(/^release-(\d+\.\d+)$/);
if (!match) {
console.log(` ⚠️ Branch ${branch} doesn't match pattern, skipping`);
continue;
}
const [major, minor] = match[1].split('.');
const versionPrefix = `v${major}.${minor}.`;
console.log(` Looking for releases with prefix: ${versionPrefix}`);
// Find the latest published release for this branch (vX.Y.Z without suffixes)
const branchReleases = publishedReleases
.filter(r => r.tag_name.startsWith(versionPrefix))
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name)); // Ensure no suffixes
if (branchReleases.length === 0) {
console.log(` ⚠️ No published releases found for ${branch}, skipping`);
continue;
}
// Sort by version (descending) to get the latest
branchReleases.sort((a, b) => {
const aVersion = a.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
const bVersion = b.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
if (!aVersion || !bVersion) return 0;
const aNum = parseInt(aVersion[1]) * 10000 + parseInt(aVersion[2]) * 100 + parseInt(aVersion[3]);
const bNum = parseInt(bVersion[1]) * 10000 + parseInt(bVersion[2]) * 100 + parseInt(bVersion[3]);
return bNum - aNum;
});
const latestRelease = branchReleases[0];
console.log(` ✅ Latest published release: ${latestRelease.tag_name}`);
// Get the commit SHA for this release tag
let releaseCommitSha;
try {
releaseCommitSha = execSync(`git rev-list -n 1 ${latestRelease.tag_name}`, { encoding: 'utf8' }).trim();
console.log(` Release commit SHA: ${releaseCommitSha}`);
} catch (error) {
console.log(` ⚠️ Could not find commit for tag ${latestRelease.tag_name}, skipping`);
continue;
}
// Checkout the branch
execSync(`git fetch origin ${branch}:${branch}`, { encoding: 'utf8' });
execSync(`git checkout ${branch}`, { encoding: 'utf8' });
// Get the latest commit on the branch
const latestBranchCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
console.log(` Latest branch commit: ${latestBranchCommit}`);
// Check if there are new commits after the release
const commitsAfterRelease = execSync(
`git rev-list ${releaseCommitSha}..HEAD --oneline`,
{ encoding: 'utf8' }
).trim();
if (!commitsAfterRelease) {
console.log(` No new commits after ${latestRelease.tag_name}, skipping`);
continue;
}
console.log(` ✅ Found new commits after release:`);
console.log(commitsAfterRelease);
// Calculate next version (Z+1)
const versionMatch = latestRelease.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
if (!versionMatch) {
console.log(` ❌ Could not parse version from ${latestRelease.tag_name}, skipping`);
continue;
}
const nextPatch = parseInt(versionMatch[3]) + 1;
const nextTag = `v${versionMatch[1]}.${versionMatch[2]}.${nextPatch}`;
console.log(` 🏷️ Creating new tag: ${nextTag} on commit ${latestBranchCommit}`);
// Create and push the tag with base_ref for workflow triggering
try {
// Delete local tag if exists to force update
try {
execSync(`git tag -d ${nextTag}`, { encoding: 'utf8' });
} catch (e) {
// Tag doesn't exist locally, that's fine
}
// Delete remote tag if exists
try {
execSync(`git push origin :refs/tags/${nextTag}`, { encoding: 'utf8' });
} catch (e) {
// Tag doesn't exist remotely, that's fine
}
// Create tag locally
execSync(`git tag ${nextTag} ${latestBranchCommit}`, { encoding: 'utf8' });
// Push tag with HEAD reference to preserve base_ref
execSync(`git push origin HEAD:refs/tags/${nextTag}`, { encoding: 'utf8' });
console.log(` ✅ Successfully created and pushed tag ${nextTag}`);
} catch (error) {
console.log(` ❌ Error creating/pushing tag ${nextTag}: ${error.message}`);
core.setFailed(`Failed to create tag ${nextTag} for branch ${branch}`);
}
}
console.log(`\n✅ Finished processing all release branches`);

View File

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

View File

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

View File

@@ -46,7 +46,12 @@ jobs:
fetch-depth: 0
- name: Create tag on merge commit
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
git tag -f ${{ steps.get_tag.outputs.tag }} ${{ github.sha }}
git push -f origin ${{ steps.get_tag.outputs.tag }}
@@ -105,67 +110,95 @@ jobs:
}
}
# Get the latest published release
- name: Get the latest published release
id: latest_release
uses: actions/github-script@v7
with:
script: |
try {
const rel = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo
});
core.setOutput('tag', rel.data.tag_name);
} catch (_) {
core.setOutput('tag', '');
}
# Compare current tag vs latest using semver-utils
- name: Semver compare
id: semver
uses: madhead/semver-utils@v4.3.0
with:
version: ${{ steps.get_tag.outputs.tag }}
compare-to: ${{ steps.latest_release.outputs.tag }}
# Derive flags: prerelease? make_latest?
- name: Calculate publish flags
id: flags
uses: actions/github-script@v7
with:
script: |
const tag = '${{ steps.get_tag.outputs.tag }}'; // v0.31.5-rc.1
const m = tag.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/);
if (!m) {
core.setFailed(`❌ tag '${tag}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`);
return;
}
const version = m[1] + (m[2] ?? ''); // 0.31.5-rc.1
const isRc = Boolean(m[2]);
core.setOutput('is_rc', isRc);
const outdated = '${{ steps.semver.outputs.comparison-result }}' === '<';
core.setOutput('make_latest', isRc || outdated ? 'false' : 'legacy');
# Publish draft release with correct flags
# Publish draft release and ensure correct latest flag
- name: Publish draft release
uses: actions/github-script@v7
with:
script: |
const tag = '${{ steps.get_tag.outputs.tag }}';
const m = tag.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/);
if (!m) {
core.setFailed(`❌ tag '${tag}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`);
return;
}
const isRc = Boolean(m[2]);
// Parse semver string to comparable numbers
function parseSemver(v) {
const match = v.replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
if (!match) return null;
return {
major: parseInt(match[1]),
minor: parseInt(match[2]),
patch: parseInt(match[3])
};
}
// Compare two semver objects
function compareSemver(a, b) {
if (a.major !== b.major) return a.major - b.major;
if (a.minor !== b.minor) return a.minor - b.minor;
return a.patch - b.patch;
}
const currentSemver = parseSemver(tag);
// Get all releases
const releases = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo
});
const draft = releases.data.find(r => r.tag_name === tag && r.draft);
if (!draft) throw new Error(`Draft release for ${tag} not found`);
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: draft.id,
draft: false,
prerelease: ${{ steps.flags.outputs.is_rc }},
make_latest: '${{ steps.flags.outputs.make_latest }}'
repo: context.repo.repo,
per_page: 100
});
console.log(`🚀 Published release for ${tag}`);
// Find draft release to publish
const draft = releases.data.find(r => r.tag_name === tag && r.draft);
if (!draft) throw new Error(`Draft release for ${tag} not found`);
// Find max semver among published releases (excluding current draft)
const publishedReleases = releases.data
.filter(r => !r.draft && !r.prerelease)
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name))
.map(r => ({ id: r.id, tag: r.tag_name, semver: parseSemver(r.tag_name) }))
.filter(r => r.semver !== null);
let maxRelease = null;
for (const rel of publishedReleases) {
if (!maxRelease || compareSemver(rel.semver, maxRelease.semver) > 0) {
maxRelease = rel;
}
}
// Determine if this release should be latest
const isOutdated = maxRelease && compareSemver(currentSemver, maxRelease.semver) < 0;
const makeLatest = (isRc || isOutdated) ? 'false' : 'true';
if (isRc) {
console.log(`🏷️ ${tag} is a prerelease, make_latest: false`);
} else if (isOutdated) {
console.log(`🏷️ ${tag} < ${maxRelease.tag} (max semver), make_latest: false`);
} else {
console.log(`🏷️ ${tag} is the highest version, make_latest: true`);
}
// Publish the release
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: draft.id,
draft: false,
prerelease: isRc,
make_latest: makeLatest
});
console.log(`🚀 Published release ${tag}`);
// If this is a backport/outdated release, ensure the correct release is marked as latest
if (isOutdated && maxRelease) {
console.log(`🔧 Ensuring ${maxRelease.tag} remains the latest release...`);
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: maxRelease.id,
make_latest: 'true'
});
console.log(`✅ Restored ${maxRelease.tag} as latest release`);
}

78
.github/workflows/retest.yaml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: Retest
on:
issue_comment:
types: [created]
jobs:
retest:
name: Retest PR
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '/retest')
permissions:
actions: write
pull-requests: read
steps:
- name: Rerun from Prepare environment
uses: actions/github-script@v7
with:
script: |
const prNumber = context.issue.number;
// Get the PR to find the head SHA
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
// Find the latest workflow run for this PR
const runs = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'pull-requests.yaml',
head_sha: pr.data.head.sha
});
if (runs.data.workflow_runs.length === 0) {
core.setFailed('No workflow runs found for this PR');
return;
}
const latestRun = runs.data.workflow_runs[0];
console.log(`Found workflow run: ${latestRun.id} (${latestRun.status})`);
// Check if workflow is waiting for approval (fork PRs)
if (latestRun.conclusion === 'action_required') {
core.setFailed('Workflow is waiting for approval. A maintainer must approve the workflow first.');
return;
}
// Get jobs for this run
const jobs = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: latestRun.id
});
// Find "Prepare environment" job
const prepareJob = jobs.data.jobs.find(j => j.name === 'Prepare environment');
if (!prepareJob) {
core.setFailed('Could not find "Prepare environment" job');
return;
}
console.log(`Found job: ${prepareJob.name} (id: ${prepareJob.id}, status: ${prepareJob.status})`);
// Rerun the job
await github.rest.actions.reRunJobForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
job_id: prepareJob.id
});
console.log(`✅ Triggered rerun of job "${prepareJob.name}" (${prepareJob.id})`);

View File

@@ -123,32 +123,6 @@ jobs:
git commit -m "Prepare release ${GITHUB_REF#refs/tags/}" -s || echo "No changes to commit"
git push origin HEAD || true
# Get `latest_version` from latest published release
- name: Get latest published release
if: steps.check_release.outputs.skip == 'false'
id: latest_release
uses: actions/github-script@v7
with:
script: |
try {
const rel = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo
});
core.setOutput('tag', rel.data.tag_name);
} catch (_) {
core.setOutput('tag', '');
}
# Compare tag (A) with latest (B)
- name: Semver compare
if: steps.check_release.outputs.skip == 'false'
id: semver
uses: madhead/semver-utils@v4.3.0
with:
version: ${{ steps.tag.outputs.tag }} # A
compare-to: ${{ steps.latest_release.outputs.tag }} # B
# Create or reuse draft release
- name: Create / reuse draft release
if: steps.check_release.outputs.skip == 'false'

View File

@@ -0,0 +1,92 @@
name: Update Release Notes
on:
push:
branches:
- main
concurrency:
group: update-releasenotes-${{ github.workflow }}
cancel-in-progress: false
jobs:
update-releasenotes:
name: Update Release Notes
runs-on: [self-hosted]
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Update release notes from changelogs
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const changelogDir = 'docs/changelogs';
// Get releases from first page
const releases = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 30
});
console.log(`Found ${releases.data.length} releases (first page only)`);
// Process each release
for (const release of releases.data) {
const tag = release.tag_name;
const changelogFile = `${tag}.md`;
const changelogPath = path.join(changelogDir, changelogFile);
console.log(`\nProcessing release: ${tag}`);
// Check if changelog file exists
if (!fs.existsSync(changelogPath)) {
console.log(` ⚠️ Changelog file ${changelogFile} does not exist, skipping...`);
continue;
}
// Read changelog file content
let changelogContent;
try {
changelogContent = fs.readFileSync(changelogPath, 'utf8');
} catch (error) {
console.log(` ❌ Error reading file ${changelogPath}: ${error.message}`);
continue;
}
if (!changelogContent.trim()) {
console.log(` ⚠️ Changelog file ${changelogFile} is empty, skipping...`);
continue;
}
// Check if content is already up to date
const currentBody = release.body || '';
if (currentBody.trim() === changelogContent.trim()) {
console.log(` ✓ Content is already up to date, skipping...`);
continue;
}
// Update release notes
try {
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.id,
body: changelogContent
});
console.log(` ✅ Successfully updated release notes for ${tag}`);
} catch (error) {
console.log(` ❌ Error updating release ${tag}: ${error.message}`);
core.setFailed(`Failed to update release notes for ${tag}`);
}
}

View File

@@ -32,4 +32,4 @@ This list is sorted in chronological order, based on the submission date.
| [Urmanac](https://urmanac.com) | @kingdonb | 2024-12-04 | Urmanac is the future home of a hosting platform for the knowledge base of a community of personal server enthusiasts. We use Cozystack to provide support services for web sites hosted using both conventional deployments and on SpinKube, with WASM. |
| [Hidora](https://hikube.cloud) | @matthieu-robin | 2025-09-17 | Hidora is a Swiss cloud provider delivering managed services and infrastructure solutions through datacenters located in Switzerland, ensuring data sovereignty and reliability. Its sovereign cloud platform, Hikube, is designed to run workloads with high availability across multiple datacenters, providing enterprises with a secure and scalable foundation for their applications based on Cozystack. |
| [QOSI](https://qosi.kz) | @tabu-a | 2025-10-04 | QOSI is a non-profit organization driving open-source adoption and digital sovereignty across Kazakhstan and Central Asia. We use Cozystack as a platform for deploying sovereign, GPU-enabled clouds and educational environments under the National AI Program. Our goal is to accelerate the regions transition toward open, self-hosted cloud-native technologies |
|
| [Cloupard](https://cloupard.kz/) | @serjiott | 2025-12-18 | Cloupard is a public cloud provider offering IaaS and PaaS services via datacenters in Kazakhstan and Uzbekistan. Uses CozyStack on bare metal to extend its managed PaaS offerings. |

View File

@@ -3,14 +3,38 @@
This file provides structured guidance for AI coding assistants and agents
working with the **Cozystack** project.
## Agent Documentation
## Activation
| Agent | Purpose |
|-------|---------|
| [overview.md](./docs/agents/overview.md) | Project structure and conventions |
| [contributing.md](./docs/agents/contributing.md) | Commits, pull requests, and git workflow |
| [changelog.md](./docs/agents/changelog.md) | Changelog generation instructions |
| [releasing.md](./docs/agents/releasing.md) | Release process and workflow |
**CRITICAL**: When the user asks you to do something that matches the scope of a documented process, you MUST read the corresponding documentation file and follow the instructions exactly as written.
- **Commits, PRs, git operations** (e.g., "create a commit", "make a PR", "fix review comments", "rebase", "cherry-pick")
- Read: [`contributing.md`](./docs/agents/contributing.md)
- Action: Read the entire file and follow ALL instructions step-by-step
- **Changelog generation** (e.g., "generate changelog", "create changelog", "prepare changelog for version X")
- Read: [`changelog.md`](./docs/agents/changelog.md)
- Action: Read the entire file and follow ALL steps in the checklist. Do NOT skip any mandatory steps
- **Release creation** (e.g., "create release", "prepare release", "tag release", "make a release")
- Read: [`releasing.md`](./docs/agents/releasing.md)
- Action: Read the file and follow the referenced release process in `docs/release.md`
- **Project structure, conventions, code layout** (e.g., "where should I put X", "what's the convention for Y", "how is the project organized")
- Read: [`overview.md`](./docs/agents/overview.md)
- Action: Read relevant sections to understand project structure and conventions
- **General questions about contributing**
- Read: [`contributing.md`](./docs/agents/contributing.md)
- Action: Read the file to understand git workflow, commit format, PR process
**Important rules:**
-**ONLY read the file if the task matches the documented process scope** - do not read files for tasks that don't match their purpose
-**ALWAYS read the file FIRST** before starting the task (when applicable)
-**Follow instructions EXACTLY** as written in the documentation
-**Do NOT skip mandatory steps** (especially in changelog.md)
-**Do NOT assume** you know the process - always check the documentation when the task matches
-**Do NOT read files** for tasks that are outside their documented scope
- 📖 **Note**: [`overview.md`](./docs/agents/overview.md) can be useful as a reference to understand project structure and conventions, even when not explicitly required by the task
## Project Overview

View File

@@ -17,7 +17,7 @@ build: build-deps
make -C packages/system/cozystack-controller image
make -C packages/system/lineage-controller-webhook image
make -C packages/system/cilium image
make -C packages/system/kubeovn image
make -C packages/system/linstor image
make -C packages/system/kubeovn-webhook image
make -C packages/system/kubeovn-plunger image
make -C packages/system/dashboard image
@@ -26,15 +26,20 @@ build: build-deps
make -C packages/system/bucket image
make -C packages/system/objectstorage-controller image
make -C packages/core/testing image
make -C packages/core/installer image-operator
make -C packages/core/talos image
make -C packages/core/platform image
make -C packages/core/installer image-packages
make -C packages/core/installer image
make manifests
repos:
rm -rf _out
make -C packages/system repo
make -C packages/apps repo
make -C packages/extra repo
manifests:
mkdir -p _out/assets
(cd packages/core/installer/; helm template -n cozy-system cozystack-operator . | sed '/^WARNING/d') > _out/assets/cozystack-installer.yaml
(cd packages/core/installer/; helm template -n cozy-installer installer .) > _out/assets/cozystack-installer.yaml
assets:
make -C packages/core/talos assets

View File

@@ -1,230 +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=bundle
// Bundle is the Schema for the bundles API
type Bundle struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BundleSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// BundleList contains a list of Bundles
type BundleList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Bundle `json:"items"`
}
func init() {
SchemeBuilder.Register(&Bundle{}, &BundleList{})
}
// BundleSpec defines the desired state of Bundle
type BundleSpec struct {
// SourceRef is the source reference for the bundle charts
// +required
SourceRef BundleSourceRef `json:"sourceRef"`
// DependsOn is a list of bundle dependencies in the format "bundleName/target"
// For example: "cozystack-system/network"
// If specified, the dependencies listed in the target's packages will be taken
// from the specified bundle and added to all packages in this bundle
// +optional
DependsOn []string `json:"dependsOn,omitempty"`
// DependencyTargets defines named groups of packages that can be referenced
// by other bundles via dependsOn. Each target has a name and a list of packages.
// +optional
DependencyTargets []BundleDependencyTarget `json:"dependencyTargets,omitempty"`
// Libraries is a list of Helm library charts used by packages
// +optional
Libraries []BundleLibrary `json:"libraries,omitempty"`
// Artifacts is a list of Helm charts that will be built as ExternalArtifacts
// These artifacts can be referenced by ApplicationDefinitions
// +optional
Artifacts []BundleArtifact `json:"artifacts,omitempty"`
// Packages is a list of Helm releases to be installed as part of this bundle
// +required
Packages []BundleRelease `json:"packages"`
// DeletionPolicy defines how child resources should be handled when the bundle is deleted.
// - "Delete" (default): Child resources will be deleted when the bundle is deleted (via ownerReference).
// - "Orphan": Child resources will be orphaned (ownerReferences will be removed).
// +kubebuilder:validation:Enum=Delete;Orphan
// +kubebuilder:default=Delete
// +optional
DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`
// Labels are labels that will be applied to all resources created by this bundle
// (ArtifactGenerators and HelmReleases). These labels are merged with the default
// cozystack.io/bundle label.
// +optional
Labels map[string]string `json:"labels,omitempty"`
// BasePath 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
BasePath string `json:"basePath,omitempty"`
}
// DeletionPolicy defines how child resources should be handled when the parent is deleted.
// +kubebuilder:validation:Enum=Delete;Orphan
type DeletionPolicy string
const (
// DeletionPolicyDelete means child resources will be deleted when the parent is deleted.
DeletionPolicyDelete DeletionPolicy = "Delete"
// DeletionPolicyOrphan means child resources will be orphaned (ownerReferences removed).
DeletionPolicyOrphan DeletionPolicy = "Orphan"
)
// BundleDependencyTarget defines a named group of packages that can be referenced
// by other bundles via dependsOn
type BundleDependencyTarget struct {
// Name is the unique identifier for this dependency target
// +required
Name string `json:"name"`
// Packages is a list of package names that belong to this target
// These packages will be added as dependencies when this target is referenced
// +required
Packages []string `json:"packages"`
}
// BundleLibrary defines a Helm library chart
type BundleLibrary struct {
// Name is the unique identifier for this library
// +required
Name string `json:"name"`
// Path is the path to the library chart directory
// +required
Path string `json:"path"`
}
// BundleArtifact defines a Helm chart artifact that will be built as ExternalArtifact
type BundleArtifact struct {
// Name is the unique identifier for this artifact (used as ExternalArtifact name)
// +required
Name string `json:"name"`
// Path is the path to the Helm chart directory
// +required
Path string `json:"path"`
// Libraries is a list of library names that this artifact depends on
// +optional
Libraries []string `json:"libraries,omitempty"`
}
// BundleSourceRef defines the source reference for bundle charts
type BundleSourceRef 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"`
}
// +kubebuilder:validation:XValidation:rule="(has(self.path) && !has(self.artifact)) || (!has(self.path) && has(self.artifact))",message="either path or artifact must be set, but not both"
// BundleRelease defines a single Helm release within a bundle
type BundleRelease struct {
// Name is the unique identifier for this release within the bundle
// +required
Name string `json:"name"`
// ReleaseName is the name of the HelmRelease resource that will be created
// +required
ReleaseName string `json:"releaseName"`
// Path is the path to the Helm chart directory
// Either Path or Artifact must be specified, but not both
// +optional
Path string `json:"path,omitempty"`
// Artifact is the name of an artifact from the bundle's artifacts list
// The artifact must exist in the bundle's artifacts section
// Either Path or Artifact must be specified, but not both
// +optional
Artifact string `json:"artifact,omitempty"`
// Namespace is the Kubernetes namespace where the release will be installed
// +required
Namespace string `json:"namespace"`
// Privileged indicates whether this release requires privileged access
// +optional
Privileged bool `json:"privileged,omitempty"`
// Disabled indicates whether this release is disabled (should not be installed)
// +optional
Disabled bool `json:"disabled,omitempty"`
// DependsOn is a list of release names that must be installed before this release
// +optional
DependsOn []string `json:"dependsOn,omitempty"`
// Libraries is a list of library names that this package depends on
// +optional
Libraries []string `json:"libraries,omitempty"`
// Values contains Helm chart values as a JSON object
// +optional
Values *apiextensionsv1.JSON `json:"values,omitempty"`
// ValuesFiles is a list of values file names to use
// +optional
ValuesFiles []string `json:"valuesFiles,omitempty"`
// Labels are labels that will be applied to the HelmRelease created for this package
// These labels are merged with bundle-level labels and the default cozystack.io/bundle label
// +optional
Labels map[string]string `json:"labels,omitempty"`
// NamespaceLabels are labels that will be applied to the namespace for this package
// These labels are merged with labels from other packages in the same namespace
// +optional
NamespaceLabels map[string]string `json:"namespaceLabels,omitempty"`
// NamespaceAnnotations are annotations that will be applied to the namespace for this package
// These annotations are merged with annotations from other packages in the same namespace
// +optional
NamespaceAnnotations map[string]string `json:"namespaceAnnotations,omitempty"`
}

View File

@@ -18,51 +18,50 @@ package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster,shortName=appdef
// +kubebuilder:resource:scope=Cluster
// ApplicationDefinition is the Schema for the applicationdefinitions API
type ApplicationDefinition struct {
// CozystackResourceDefinition is the Schema for the cozystackresourcedefinitions API
type CozystackResourceDefinition struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ApplicationDefinitionSpec `json:"spec,omitempty"`
Spec CozystackResourceDefinitionSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// ApplicationDefinitionList contains a list of ApplicationDefinitions
type ApplicationDefinitionList struct {
// CozystackResourceDefinitionList contains a list of CozystackResourceDefinitions
type CozystackResourceDefinitionList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ApplicationDefinition `json:"items"`
Items []CozystackResourceDefinition `json:"items"`
}
func init() {
SchemeBuilder.Register(&ApplicationDefinition{}, &ApplicationDefinitionList{})
SchemeBuilder.Register(&CozystackResourceDefinition{}, &CozystackResourceDefinitionList{})
}
type ApplicationDefinitionSpec struct {
type CozystackResourceDefinitionSpec struct {
// Application configuration
Application ApplicationDefinitionApplication `json:"application"`
Application CozystackResourceDefinitionApplication `json:"application"`
// Release configuration
Release ApplicationDefinitionRelease `json:"release"`
Release CozystackResourceDefinitionRelease `json:"release"`
// Secret selectors
Secrets ApplicationDefinitionResources `json:"secrets,omitempty"`
Secrets CozystackResourceDefinitionResources `json:"secrets,omitempty"`
// Service selectors
Services ApplicationDefinitionResources `json:"services,omitempty"`
Services CozystackResourceDefinitionResources `json:"services,omitempty"`
// Ingress selectors
Ingresses ApplicationDefinitionResources `json:"ingresses,omitempty"`
Ingresses CozystackResourceDefinitionResources `json:"ingresses,omitempty"`
// Dashboard configuration for this resource
Dashboard *ApplicationDefinitionDashboard `json:"dashboard,omitempty"`
Dashboard *CozystackResourceDefinitionDashboard `json:"dashboard,omitempty"`
}
type ApplicationDefinitionChart struct {
type CozystackResourceDefinitionChart struct {
// Name of the Helm chart
Name string `json:"name"`
// Source reference for the Helm chart
@@ -80,7 +79,7 @@ type SourceRef struct {
Namespace string `json:"namespace"`
}
type ApplicationDefinitionApplication struct {
type CozystackResourceDefinitionApplication struct {
// Kind of the application, used for UI and API
Kind string `json:"kind"`
// OpenAPI schema for the application, used for API validation
@@ -91,30 +90,16 @@ type ApplicationDefinitionApplication struct {
Singular string `json:"singular"`
}
// +kubebuilder:validation:XValidation:rule="(has(self.chart) && !has(self.chartRef)) || (!has(self.chart) && has(self.chartRef))",message="either chart or chartRef must be set, but not both"
type ApplicationDefinitionRelease struct {
// Helm chart configuration (for HelmRepository source)
// +optional
Chart *ApplicationDefinitionChart `json:"chart,omitempty"`
// Chart reference configuration (for ExternalArtifact source)
// +optional
ChartRef *ApplicationDefinitionChartRef `json:"chartRef,omitempty"`
type CozystackResourceDefinitionRelease struct {
// Helm chart configuration
Chart CozystackResourceDefinitionChart `json:"chart"`
// Labels for the release
Labels map[string]string `json:"labels,omitempty"`
// Prefix for the release name
Prefix string `json:"prefix"`
// Default values to be merged into every HelmRelease created from this resource definition
// User-specified values in Application spec will override these default values
// +optional
Values *apiextensionsv1.JSON `json:"values,omitempty"`
}
type ApplicationDefinitionChartRef struct {
// Source reference for the chart (ExternalArtifact)
SourceRef SourceRef `json:"sourceRef"`
}
// ApplicationDefinitionResourceSelector extends metav1.LabelSelector with resourceNames support.
// CozystackResourceDefinitionResourceSelector extends metav1.LabelSelector with resourceNames support.
// A resource matches this selector only if it satisfies ALL criteria:
// - Label selector conditions (matchExpressions and matchLabels)
// - AND has a name that matches one of the names in resourceNames (if specified)
@@ -136,7 +121,7 @@ type ApplicationDefinitionChartRef struct {
// - "{{ .name }}-secret"
// - "{{ .kind }}-{{ .name }}-tls"
// - "specificname"
type ApplicationDefinitionResourceSelector struct {
type CozystackResourceDefinitionResourceSelector struct {
metav1.LabelSelector `json:",inline"`
// ResourceNames is a list of resource names to match
// If specified, the resource must have one of these exact names to match the selector
@@ -144,16 +129,16 @@ type ApplicationDefinitionResourceSelector struct {
ResourceNames []string `json:"resourceNames,omitempty"`
}
type ApplicationDefinitionResources struct {
type CozystackResourceDefinitionResources struct {
// Exclude contains an array of resource selectors that target resources.
// If a resource matches the selector in any of the elements in the array, it is
// hidden from the user, regardless of the matches in the include array.
Exclude []*ApplicationDefinitionResourceSelector `json:"exclude,omitempty"`
Exclude []*CozystackResourceDefinitionResourceSelector `json:"exclude,omitempty"`
// Include contains an array of resource selectors that target resources.
// If a resource matches the selector in any of the elements in the array, and
// matches none of the selectors in the exclude array that resource is marked
// as a tenant resource and is visible to users.
Include []*ApplicationDefinitionResourceSelector `json:"include,omitempty"`
Include []*CozystackResourceDefinitionResourceSelector `json:"include,omitempty"`
}
// ---- Dashboard types ----
@@ -170,8 +155,8 @@ const (
DashboardTabYAML DashboardTab = "yaml"
)
// ApplicationDefinitionDashboard describes how this resource appears in the UI.
type ApplicationDefinitionDashboard struct {
// CozystackResourceDefinitionDashboard describes how this resource appears in the UI.
type CozystackResourceDefinitionDashboard struct {
// Human-readable name shown in the UI (e.g., "Bucket")
Singular string `json:"singular"`
// Plural human-readable name (e.g., "Buckets")

View File

@@ -1,71 +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=platform
// Platform is the Schema for the platforms API
type Platform struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PlatformSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// PlatformList contains a list of Platform
type PlatformList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Platform `json:"items"`
}
func init() {
SchemeBuilder.Register(&Platform{}, &PlatformList{})
}
// PlatformSpec defines the desired state of Platform
type PlatformSpec struct {
// SourceRef is the source reference for the platform chart
// This is used to generate the ArtifactGenerator
// +required
SourceRef SourceRef `json:"sourceRef"`
// Values contains Helm chart values as a JSON object
// These values are passed directly to HelmRelease.values
// +optional
Values *apiextensionsv1.JSON `json:"values,omitempty"`
// Interval is the interval at which to reconcile the HelmRelease
// +kubebuilder:default="5m"
// +optional
Interval *metav1.Duration `json:"interval,omitempty"`
// BasePath is the base path where the platform chart is located in the source.
// For GitRepository, defaults to "packages/core/platform" if not specified.
// For OCIRepository, defaults to "core/platform" if not specified.
// +optional
BasePath string `json:"basePath,omitempty"`
}

View File

@@ -21,32 +21,30 @@ 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 *ApplicationDefinition) DeepCopyInto(out *ApplicationDefinition) {
func (in *CozystackResourceDefinition) DeepCopyInto(out *CozystackResourceDefinition) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinition.
func (in *ApplicationDefinition) DeepCopy() *ApplicationDefinition {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinition.
func (in *CozystackResourceDefinition) DeepCopy() *CozystackResourceDefinition {
if in == nil {
return nil
}
out := new(ApplicationDefinition)
out := new(CozystackResourceDefinition)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ApplicationDefinition) DeepCopyObject() runtime.Object {
func (in *CozystackResourceDefinition) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
@@ -54,54 +52,38 @@ func (in *ApplicationDefinition) DeepCopyObject() runtime.Object {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionApplication) DeepCopyInto(out *ApplicationDefinitionApplication) {
func (in *CozystackResourceDefinitionApplication) DeepCopyInto(out *CozystackResourceDefinitionApplication) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionApplication.
func (in *ApplicationDefinitionApplication) DeepCopy() *ApplicationDefinitionApplication {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionApplication.
func (in *CozystackResourceDefinitionApplication) DeepCopy() *CozystackResourceDefinitionApplication {
if in == nil {
return nil
}
out := new(ApplicationDefinitionApplication)
out := new(CozystackResourceDefinitionApplication)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionChart) DeepCopyInto(out *ApplicationDefinitionChart) {
func (in *CozystackResourceDefinitionChart) DeepCopyInto(out *CozystackResourceDefinitionChart) {
*out = *in
out.SourceRef = in.SourceRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionChart.
func (in *ApplicationDefinitionChart) DeepCopy() *ApplicationDefinitionChart {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionChart.
func (in *CozystackResourceDefinitionChart) DeepCopy() *CozystackResourceDefinitionChart {
if in == nil {
return nil
}
out := new(ApplicationDefinitionChart)
out := new(CozystackResourceDefinitionChart)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionChartRef) DeepCopyInto(out *ApplicationDefinitionChartRef) {
*out = *in
out.SourceRef = in.SourceRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionChartRef.
func (in *ApplicationDefinitionChartRef) DeepCopy() *ApplicationDefinitionChartRef {
if in == nil {
return nil
}
out := new(ApplicationDefinitionChartRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionDashboard) DeepCopyInto(out *ApplicationDefinitionDashboard) {
func (in *CozystackResourceDefinitionDashboard) DeepCopyInto(out *CozystackResourceDefinitionDashboard) {
*out = *in
if in.Tags != nil {
in, out := &in.Tags, &out.Tags
@@ -126,42 +108,42 @@ func (in *ApplicationDefinitionDashboard) DeepCopyInto(out *ApplicationDefinitio
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionDashboard.
func (in *ApplicationDefinitionDashboard) DeepCopy() *ApplicationDefinitionDashboard {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionDashboard.
func (in *CozystackResourceDefinitionDashboard) DeepCopy() *CozystackResourceDefinitionDashboard {
if in == nil {
return nil
}
out := new(ApplicationDefinitionDashboard)
out := new(CozystackResourceDefinitionDashboard)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionList) DeepCopyInto(out *ApplicationDefinitionList) {
func (in *CozystackResourceDefinitionList) DeepCopyInto(out *CozystackResourceDefinitionList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ApplicationDefinition, len(*in))
*out = make([]CozystackResourceDefinition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionList.
func (in *ApplicationDefinitionList) DeepCopy() *ApplicationDefinitionList {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionList.
func (in *CozystackResourceDefinitionList) DeepCopy() *CozystackResourceDefinitionList {
if in == nil {
return nil
}
out := new(ApplicationDefinitionList)
out := new(CozystackResourceDefinitionList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ApplicationDefinitionList) DeepCopyObject() runtime.Object {
func (in *CozystackResourceDefinitionList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
@@ -169,18 +151,9 @@ func (in *ApplicationDefinitionList) DeepCopyObject() runtime.Object {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionRelease) DeepCopyInto(out *ApplicationDefinitionRelease) {
func (in *CozystackResourceDefinitionRelease) DeepCopyInto(out *CozystackResourceDefinitionRelease) {
*out = *in
if in.Chart != nil {
in, out := &in.Chart, &out.Chart
*out = new(ApplicationDefinitionChart)
**out = **in
}
if in.ChartRef != nil {
in, out := &in.ChartRef, &out.ChartRef
*out = new(ApplicationDefinitionChartRef)
**out = **in
}
out.Chart = in.Chart
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
@@ -188,25 +161,20 @@ func (in *ApplicationDefinitionRelease) DeepCopyInto(out *ApplicationDefinitionR
(*out)[key] = val
}
}
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 ApplicationDefinitionRelease.
func (in *ApplicationDefinitionRelease) DeepCopy() *ApplicationDefinitionRelease {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionRelease.
func (in *CozystackResourceDefinitionRelease) DeepCopy() *CozystackResourceDefinitionRelease {
if in == nil {
return nil
}
out := new(ApplicationDefinitionRelease)
out := new(CozystackResourceDefinitionRelease)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionResourceSelector) DeepCopyInto(out *ApplicationDefinitionResourceSelector) {
func (in *CozystackResourceDefinitionResourceSelector) DeepCopyInto(out *CozystackResourceDefinitionResourceSelector) {
*out = *in
in.LabelSelector.DeepCopyInto(&out.LabelSelector)
if in.ResourceNames != nil {
@@ -216,55 +184,55 @@ func (in *ApplicationDefinitionResourceSelector) DeepCopyInto(out *ApplicationDe
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionResourceSelector.
func (in *ApplicationDefinitionResourceSelector) DeepCopy() *ApplicationDefinitionResourceSelector {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionResourceSelector.
func (in *CozystackResourceDefinitionResourceSelector) DeepCopy() *CozystackResourceDefinitionResourceSelector {
if in == nil {
return nil
}
out := new(ApplicationDefinitionResourceSelector)
out := new(CozystackResourceDefinitionResourceSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionResources) DeepCopyInto(out *ApplicationDefinitionResources) {
func (in *CozystackResourceDefinitionResources) DeepCopyInto(out *CozystackResourceDefinitionResources) {
*out = *in
if in.Exclude != nil {
in, out := &in.Exclude, &out.Exclude
*out = make([]*ApplicationDefinitionResourceSelector, len(*in))
*out = make([]*CozystackResourceDefinitionResourceSelector, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(ApplicationDefinitionResourceSelector)
*out = new(CozystackResourceDefinitionResourceSelector)
(*in).DeepCopyInto(*out)
}
}
}
if in.Include != nil {
in, out := &in.Include, &out.Include
*out = make([]*ApplicationDefinitionResourceSelector, len(*in))
*out = make([]*CozystackResourceDefinitionResourceSelector, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(ApplicationDefinitionResourceSelector)
*out = new(CozystackResourceDefinitionResourceSelector)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionResources.
func (in *ApplicationDefinitionResources) DeepCopy() *ApplicationDefinitionResources {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionResources.
func (in *CozystackResourceDefinitionResources) DeepCopy() *CozystackResourceDefinitionResources {
if in == nil {
return nil
}
out := new(ApplicationDefinitionResources)
out := new(CozystackResourceDefinitionResources)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionSpec) DeepCopyInto(out *ApplicationDefinitionSpec) {
func (in *CozystackResourceDefinitionSpec) DeepCopyInto(out *CozystackResourceDefinitionSpec) {
*out = *in
out.Application = in.Application
in.Release.DeepCopyInto(&out.Release)
@@ -273,339 +241,17 @@ func (in *ApplicationDefinitionSpec) DeepCopyInto(out *ApplicationDefinitionSpec
in.Ingresses.DeepCopyInto(&out.Ingresses)
if in.Dashboard != nil {
in, out := &in.Dashboard, &out.Dashboard
*out = new(ApplicationDefinitionDashboard)
*out = new(CozystackResourceDefinitionDashboard)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionSpec.
func (in *ApplicationDefinitionSpec) DeepCopy() *ApplicationDefinitionSpec {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionSpec.
func (in *CozystackResourceDefinitionSpec) DeepCopy() *CozystackResourceDefinitionSpec {
if in == nil {
return nil
}
out := new(ApplicationDefinitionSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Bundle) DeepCopyInto(out *Bundle) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bundle.
func (in *Bundle) DeepCopy() *Bundle {
if in == nil {
return nil
}
out := new(Bundle)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Bundle) 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 *BundleArtifact) DeepCopyInto(out *BundleArtifact) {
*out = *in
if in.Libraries != nil {
in, out := &in.Libraries, &out.Libraries
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleArtifact.
func (in *BundleArtifact) DeepCopy() *BundleArtifact {
if in == nil {
return nil
}
out := new(BundleArtifact)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BundleDependencyTarget) DeepCopyInto(out *BundleDependencyTarget) {
*out = *in
if in.Packages != nil {
in, out := &in.Packages, &out.Packages
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleDependencyTarget.
func (in *BundleDependencyTarget) DeepCopy() *BundleDependencyTarget {
if in == nil {
return nil
}
out := new(BundleDependencyTarget)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BundleLibrary) DeepCopyInto(out *BundleLibrary) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleLibrary.
func (in *BundleLibrary) DeepCopy() *BundleLibrary {
if in == nil {
return nil
}
out := new(BundleLibrary)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BundleList) DeepCopyInto(out *BundleList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Bundle, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleList.
func (in *BundleList) DeepCopy() *BundleList {
if in == nil {
return nil
}
out := new(BundleList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BundleList) 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 *BundleRelease) DeepCopyInto(out *BundleRelease) {
*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([]string, len(*in))
copy(*out, *in)
}
if in.Values != nil {
in, out := &in.Values, &out.Values
*out = new(v1.JSON)
(*in).DeepCopyInto(*out)
}
if in.ValuesFiles != nil {
in, out := &in.ValuesFiles, &out.ValuesFiles
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.NamespaceLabels != nil {
in, out := &in.NamespaceLabels, &out.NamespaceLabels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.NamespaceAnnotations != nil {
in, out := &in.NamespaceAnnotations, &out.NamespaceAnnotations
*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 BundleRelease.
func (in *BundleRelease) DeepCopy() *BundleRelease {
if in == nil {
return nil
}
out := new(BundleRelease)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BundleSourceRef) DeepCopyInto(out *BundleSourceRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleSourceRef.
func (in *BundleSourceRef) DeepCopy() *BundleSourceRef {
if in == nil {
return nil
}
out := new(BundleSourceRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BundleSpec) DeepCopyInto(out *BundleSpec) {
*out = *in
out.SourceRef = in.SourceRef
if in.DependsOn != nil {
in, out := &in.DependsOn, &out.DependsOn
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.DependencyTargets != nil {
in, out := &in.DependencyTargets, &out.DependencyTargets
*out = make([]BundleDependencyTarget, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Libraries != nil {
in, out := &in.Libraries, &out.Libraries
*out = make([]BundleLibrary, len(*in))
copy(*out, *in)
}
if in.Artifacts != nil {
in, out := &in.Artifacts, &out.Artifacts
*out = make([]BundleArtifact, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Packages != nil {
in, out := &in.Packages, &out.Packages
*out = make([]BundleRelease, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*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 BundleSpec.
func (in *BundleSpec) DeepCopy() *BundleSpec {
if in == nil {
return nil
}
out := new(BundleSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Platform) DeepCopyInto(out *Platform) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Platform.
func (in *Platform) DeepCopy() *Platform {
if in == nil {
return nil
}
out := new(Platform)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Platform) 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 *PlatformList) DeepCopyInto(out *PlatformList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Platform, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformList.
func (in *PlatformList) DeepCopy() *PlatformList {
if in == nil {
return nil
}
out := new(PlatformList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PlatformList) 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 *PlatformSpec) DeepCopyInto(out *PlatformSpec) {
*out = *in
out.SourceRef = in.SourceRef
if in.Values != nil {
in, out := &in.Values, &out.Values
*out = new(v1.JSON)
(*in).DeepCopyInto(*out)
}
if in.Interval != nil {
in, out := &in.Interval, &out.Interval
*out = new(metav1.Duration)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformSpec.
func (in *PlatformSpec) DeepCopy() *PlatformSpec {
if in == nil {
return nil
}
out := new(PlatformSpec)
out := new(CozystackResourceDefinitionSpec)
in.DeepCopyInto(out)
return out
}

View File

@@ -0,0 +1,29 @@
package main
import (
"flag"
"log"
"net/http"
"path/filepath"
)
func main() {
addr := flag.String("address", ":8123", "Address to listen on")
dir := flag.String("dir", "/cozystack/assets", "Directory to serve files from")
flag.Parse()
absDir, err := filepath.Abs(*dir)
if err != nil {
log.Fatalf("Error getting absolute path for %s: %v", *dir, err)
}
fs := http.FileServer(http.Dir(absDir))
http.Handle("/", fs)
log.Printf("Server starting on %s, serving directory %s", *addr, absDir)
err = http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}

View File

@@ -20,6 +20,7 @@ import (
"crypto/tls"
"flag"
"os"
"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.
@@ -38,6 +39,7 @@ import (
cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"github.com/cozystack/cozystack/internal/controller"
"github.com/cozystack/cozystack/internal/controller/dashboard"
"github.com/cozystack/cozystack/internal/telemetry"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
// +kubebuilder:scaffold:imports
@@ -63,6 +65,10 @@ func main() {
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var disableTelemetry bool
var telemetryEndpoint string
var telemetryInterval string
var cozystackVersion string
var reconcileDeployment bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
@@ -75,6 +81,14 @@ func main() {
"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")
flag.BoolVar(&disableTelemetry, "disable-telemetry", false,
"Disable telemetry collection")
flag.StringVar(&telemetryEndpoint, "telemetry-endpoint", "https://telemetry.cozystack.io",
"Endpoint for sending telemetry data")
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
"Interval between telemetry data collection (e.g. 15m, 1h)")
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
"Version of Cozystack")
flag.BoolVar(&reconcileDeployment, "reconcile-deployment", false,
"If set, the Cozystack API server is assumed to run as a Deployment, else as a DaemonSet.")
opts := zap.Options{
@@ -83,6 +97,21 @@ func main() {
opts.BindFlags(flag.CommandLine)
flag.Parse()
// Parse telemetry interval
interval, err := time.ParseDuration(telemetryInterval)
if err != nil {
setupLog.Error(err, "invalid telemetry interval")
os.Exit(1)
}
// Configure telemetry
telemetryConfig := telemetry.Config{
Disabled: disableTelemetry,
Endpoint: telemetryEndpoint,
Interval: interval,
CozystackVersion: cozystackVersion,
}
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// if the enable-http2 flag is false (the default), http/2 should be disabled
@@ -171,20 +200,24 @@ func main() {
os.Exit(1)
}
if err = (&controller.NamespaceHelmReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
cozyAPIKind := "DaemonSet"
if reconcileDeployment {
cozyAPIKind = "Deployment"
}
if err = (&controller.CozystackResourceDefinitionReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
CozystackAPIKind: cozyAPIKind,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "NamespaceHelmReconciler")
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionReconciler")
os.Exit(1)
}
if err = (&controller.ApplicationDefinitionReconciler{
if err = (&controller.CozystackResourceDefinitionHelmReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
CozystackAPIKind: "Deployment",
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ApplicationDefinitionReconciler")
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionHelmReconciler")
os.Exit(1)
}
@@ -208,6 +241,19 @@ func main() {
os.Exit(1)
}
// Initialize telemetry collector
collector, err := telemetry.NewCollector(mgr.GetClient(), &telemetryConfig, mgr.GetConfig())
if err != nil {
setupLog.V(1).Error(err, "unable to create telemetry collector, telemetry will be disabled")
}
if collector != nil {
if err := mgr.Add(collector); err != nil {
setupLog.Error(err, "unable to set up telemetry collector")
setupLog.V(1).Error(err, "unable to set up telemetry collector, continuing without telemetry")
}
}
setupLog.Info("starting manager")
ctx := ctrl.SetupSignalHandler()
dashboardManager.InitializeStaticResources(ctx)

View File

@@ -1,312 +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"
"encoding/json"
"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"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"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/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/fluxinstall"
"github.com/cozystack/cozystack/internal/operator"
"github.com/cozystack/cozystack/internal/telemetry"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
// stringSliceFlag is a custom flag type that allows multiple values
type stringSliceFlag []string
func (f *stringSliceFlag) String() string {
return strings.Join(*f, ",")
}
func (f *stringSliceFlag) Set(value string) error {
*f = append(*f, value)
return nil
}
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 disableTelemetry bool
var telemetryEndpoint string
var telemetryInterval string
var cozystackVersion string
var installFluxResources stringSliceFlag
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.Var(&installFluxResources, "install-flux-resource", "Install Flux resource (JSON format). Can be specified multiple times. Applied after Flux installation.")
flag.BoolVar(&disableTelemetry, "disable-telemetry", false,
"Disable telemetry collection")
flag.StringVar(&telemetryEndpoint, "telemetry-endpoint", "https://telemetry.cozystack.io",
"Endpoint for sending telemetry data")
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
"Interval between telemetry data collection (e.g. 15m, 1h)")
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
"Version of Cozystack")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
// Parse telemetry interval
interval, err := time.ParseDuration(telemetryInterval)
if err != nil {
setupLog.Error(err, "invalid telemetry interval")
os.Exit(1)
}
// Configure telemetry
telemetryConfig := telemetry.Config{
Disabled: disableTelemetry,
Endpoint: telemetryEndpoint,
Interval: interval,
CozystackVersion: cozystackVersion,
}
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
config := ctrl.GetConfigOrDie()
// Start the controller manager
setupLog.Info("Starting controller manager")
mgr, err := ctrl.NewManager(config, ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
},
WebhookServer: webhook.NewServer(webhook.Options{
Port: 9443,
}),
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "platform-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()
// The namespace will be automatically extracted from the embedded manifests
if err := fluxinstall.Install(installCtx, mgr.GetClient(), fluxinstall.WriteEmbeddedManifests); err != nil {
setupLog.Error(err, "failed to install Flux, continuing anyway")
// Don't exit - allow operator to start even if Flux install fails
// This allows the operator to work in environments where Flux is already installed
} else {
setupLog.Info("Flux installation completed successfully")
}
}
// Install Flux resources after Flux installation
if len(installFluxResources) > 0 {
setupLog.Info("Installing Flux resources", "count", len(installFluxResources))
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer installCancel()
if err := installFluxResourcesFunc(installCtx, mgr.GetClient(), installFluxResources); err != nil {
setupLog.Error(err, "failed to install Flux resources, continuing anyway")
// Don't exit - allow operator to start even if resource installation fails
} else {
setupLog.Info("Flux resources installation completed successfully")
}
}
bundleReconciler := &operator.BundleReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}
if err = bundleReconciler.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Bundle")
os.Exit(1)
}
platformReconciler := &operator.PlatformReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}
if err = platformReconciler.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Platform")
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)
}
// Initialize telemetry collector
collector, err := telemetry.NewCollector(mgr.GetClient(), &telemetryConfig, mgr.GetConfig())
if err != nil {
setupLog.V(1).Error(err, "unable to create telemetry collector, telemetry will be disabled")
}
if collector != nil {
if err := mgr.Add(collector); err != nil {
setupLog.Error(err, "unable to set up telemetry collector")
setupLog.V(1).Error(err, "unable to set up telemetry collector, continuing without telemetry")
}
}
setupLog.Info("Starting controller manager")
mgrCtx := ctrl.SetupSignalHandler()
if err := mgr.Start(mgrCtx); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
// installFluxResourcesFunc installs Flux resources from JSON strings
func installFluxResourcesFunc(ctx context.Context, k8sClient client.Client, resources []string) error {
logger := log.FromContext(ctx)
for i, resourceJSON := range resources {
logger.Info("Installing Flux resource", "index", i+1, "total", len(resources))
// Parse JSON into unstructured object
var obj unstructured.Unstructured
if err := json.Unmarshal([]byte(resourceJSON), &obj.Object); err != nil {
return fmt.Errorf("failed to parse resource JSON at index %d: %w", i, err)
}
// Validate that it has required fields
if obj.GetAPIVersion() == "" {
return fmt.Errorf("resource at index %d missing apiVersion", i)
}
if obj.GetKind() == "" {
return fmt.Errorf("resource at index %d missing kind", i)
}
if obj.GetName() == "" {
return fmt.Errorf("resource at index %d missing metadata.name", i)
}
// Apply the resource (create or update)
logger.Info("Applying Flux resource",
"apiVersion", obj.GetAPIVersion(),
"kind", obj.GetKind(),
"name", obj.GetName(),
"namespace", obj.GetNamespace(),
)
// Use server-side apply or create/update
existing := &unstructured.Unstructured{}
existing.SetGroupVersionKind(obj.GroupVersionKind())
key := client.ObjectKey{
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
}
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.GetKind(), obj.GetName(), err)
}
logger.Info("Created Flux resource", "kind", obj.GetKind(), "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.GetKind(), obj.GetName(), err)
}
logger.Info("Updated Flux resource", "kind", obj.GetKind(), "name", obj.GetName())
}
}
return nil
}

View File

@@ -22,7 +22,7 @@ When the user asks to generate a changelog, follow these steps in the specified
- [ ] Step 5: Get the list of commits for the release period
- [ ] Step 6: Check additional repositories (website is REQUIRED, optional repos if tags exist)
- [ ] **MANDATORY**: Check website repository for documentation changes WITH authors and PR links via GitHub CLI
- [ ] **MANDATORY**: Check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy) for tags during release period
- [ ] **MANDATORY**: Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) for tags during release period
- [ ] **MANDATORY**: For ALL commits from additional repos, get GitHub username via CLI, prioritizing PR author over commit author.
- [ ] Step 7: Analyze commits (extract PR numbers, authors, user impact)
- [ ] **MANDATORY**: For EVERY PR in main repo, get PR author via `gh pr view <PR_NUMBER> --json author --jq .author.login` (do NOT skip this step)
@@ -146,7 +146,7 @@ Cozystack release may include changes from related repositories. Check and inclu
**Optional repositories (MUST check ALL of them for tags during release period):**
- [https://github.com/cozystack/talm](https://github.com/cozystack/talm)
- [https://github.com/cozystack/boot-to-talos](https://github.com/cozystack/boot-to-talos)
- [https://github.com/cozystack/cozypkg](https://github.com/cozystack/cozypkg)
- [https://github.com/cozystack/cozyhr](https://github.com/cozystack/cozyhr)
- [https://github.com/cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy)
**⚠️ IMPORTANT**: You MUST check ALL optional repositories for tags created during the release period. Do NOT skip this step even if you think there might not be any tags. Use the process below to verify.
@@ -195,7 +195,7 @@ Cozystack release may include changes from related repositories. Check and inclu
3. **For optional repositories, check if tags exist during release period:**
**⚠️ MANDATORY: You MUST check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy). Do NOT skip any repository!**
**⚠️ MANDATORY: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy). Do NOT skip any repository!**
**Use the helper script:**
```bash
@@ -208,7 +208,7 @@ Cozystack release may include changes from related repositories. Check and inclu
```
The script will:
- Check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy)
- Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy)
- Look for tags created during the release period
- Get commits between tags (if tags exist) or by date range (if no tags)
- Extract PR numbers from commit messages
@@ -569,7 +569,7 @@ Create a new changelog file in the format matching previous versions:
- [ ] Step 5 completed: **ALL commits included** (including merge commits and backports) - do not skip any commits
- [ ] Step 5 completed: **Backports identified and handled correctly** - original PR author used, both original and backport PR numbers included
- [ ] Step 6 completed: Website repository checked for documentation changes WITH authors and PR links via GitHub CLI
- [ ] Step 6 completed: **ALL** optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy) checked for tags during release period
- [ ] Step 6 completed: **ALL** optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) checked for tags during release period
- [ ] Step 6 completed: For ALL commits from additional repos, GitHub username obtained via GitHub CLI (not skipped). For commits with PR numbers, PR author used via `gh pr view` (not commit author)
- [ ] Step 7 completed: For EVERY PR in main repo (including backports), PR author obtained via `gh pr view <PR_NUMBER> --json author --jq .author.login` (not skipped or assumed). Commit author NOT used - always use PR author
- [ ] Step 7 completed: **Backports verified** - for each backport PR, original PR found and original PR author used in changelog
@@ -628,7 +628,7 @@ Save the changelog to file `docs/changelogs/v<version>.md` according to the vers
- **Additional repositories (Step 6) - MANDATORY**:
- **⚠️ CRITICAL**: Always check the **website** repository for documentation changes during the release period. This is a required step and MUST NOT be skipped.
- **⚠️ CRITICAL**: You MUST check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy) for tags during the release period. Do NOT skip any repository even if you think there might not be tags.
- **⚠️ CRITICAL**: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) for tags during the release period. Do NOT skip any repository even if you think there might not be tags.
- **CRITICAL**: For ALL entries from additional repositories (website and optional), you MUST:
- **MANDATORY**: Extract PR number from commit message first
- **MANDATORY**: For commits with PR numbers, ALWAYS use `gh pr view <PR_NUMBER> --repo cozystack/<repo> --json author --jq .author.login` to get PR author (not commit author)
@@ -637,7 +637,7 @@ Save the changelog to file `docs/changelogs/v<version>.md` according to the vers
- **MANDATORY**: Do NOT use commit author for PRs - always use PR author
- Include PR link or commit hash reference
- Format: `* **[repo] Description**: details ([**@username**](https://github.com/username) in cozystack/repo#123)`
- For **optional repositories** (talm, boot-to-talos, cozypkg, cozy-proxy), you MUST check ALL of them for tags during the release period. Use the loop provided in Step 6 to check each repository systematically.
- For **optional repositories** (talm, boot-to-talos, cozyhr, cozy-proxy), you MUST check ALL of them for tags during the release period. Use the loop provided in Step 6 to check each repository systematically.
- When including changes from additional repositories, use the format: `[repo-name] Description` and link to the repository's PR/issue if available
- **Prefer PR numbers over commit hashes**: For commits from additional repositories, extract PR number from commit message using GitHub API. Use PR format (`cozystack/website#123`) instead of commit hash (`cozystack/website@abc1234`) when available
- **Never add entries without author and PR/commit reference**: Every entry from additional repositories must have both author and link

View File

@@ -155,6 +155,91 @@ git diff
The user will commit and push when ready.
## Code Review Comments
When asked to fix code review comments, **always work only with unresolved (open) comments**. Resolved comments should be ignored as they have already been addressed.
### Getting Unresolved Review Comments
Use GitHub GraphQL API to fetch only unresolved review comments from a pull request:
```bash
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 100) {
nodes {
id
path
line
author { login }
bodyText
url
createdAt
}
}
}
}
}
}
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[]'
```
### Filtering for Unresolved Comments
The key filter is `select(.isResolved == false)` which ensures only unresolved review threads are processed. Each thread can contain multiple comments, but if the thread is resolved, all its comments should be ignored.
### Working with Review Comments
1. **Fetch unresolved comments** using the GraphQL query above
2. **Parse the results** to identify:
- File path (`path`)
- Line number (`line` or `originalLine`)
- Comment text (`bodyText`)
- Author (`author.login`)
3. **Address each unresolved comment** by:
- Locating the relevant code section
- Making the requested changes
- Ensuring the fix addresses the concern raised
4. **Do NOT process resolved comments** - they have already been handled
### Example: Compact List of Unresolved Comments
For a quick overview of unresolved comments:
```bash
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 100) {
nodes {
path
line
author { login }
bodyText
}
}
}
}
}
}
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[] | "\(.path):\(.line // "N/A") - \(.author.login): \(.bodyText[:150])"'
```
### Important Notes
- **REST API limitation**: The REST endpoint `/pulls/{pr}/reviews` returns review summaries, not individual review comments. Use GraphQL API for accessing `reviewThreads` with `isResolved` status.
- **Thread-based resolution**: Comments are organized in threads. If a thread is resolved (`isResolved: true`), ignore all comments in that thread.
- **Always filter**: Never process comments from resolved threads, even if they appear in the results.
### Example Workflow
```bash

View File

@@ -10,7 +10,7 @@ Cozystack is an open-source Kubernetes-based platform and framework for building
- **Multi-tenancy**: Full isolation and self-service for tenants
- **GitOps-driven**: FluxCD-based continuous delivery
- **Modular Architecture**: Extensible with custom packages and services
- **Developer Experience**: Simplified local development with cozypkg tool
- **Developer Experience**: Simplified local development with cozyhr tool
The platform exposes infrastructure services via the Kubernetes API with ready-made configs, built-in monitoring, and alerts.

View File

@@ -5,10 +5,13 @@ https://github.com/cozystack/cozystack/releases/tag/v0.36.2
## Features and Improvements
## Security
* [vm-disk] New SVG icon for VM disk application. (@kvaps and @kvapsova in https://github.com/cozystack/cozystack/pull/1435)
## Fixes
* [kubernetes] Pin CoreDNS image tag to v1.12.4 for consistent, reproducible deployments. (@kvaps in https://github.com/cozystack/cozystack/pull/1469)
* [dashboard] Fix FerretDB spec typo that prevented deploy/display in the web UI. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1440)
## Dependencies
## Development, Testing, and CI/CD

View File

@@ -0,0 +1,14 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.38.3
-->
## Improvements
* **[core:installer] Address buildx warnings for installer image builds**: Aligns Dockerfile syntax casing to remove buildx warnings, keeping installer builds clean ([**@nbykov0**](https://github.com/nbykov0) in #1682).
* **[system:coredns] Align CoreDNS app labels with Talos defaults**: Matches CoreDNS labels to Talos conventions so services select pods consistently across platform and tenant clusters ([**@nbykov0**](https://github.com/nbykov0) in #1675).
* **[system:monitoring-agents] Rename CoreDNS metrics service to avoid conflicts**: Renames the metrics service so it no longer clashes with the CoreDNS service used for name resolution in tenant clusters ([**@nbykov0**](https://github.com/nbykov0) in #1676).
---
**Full Changelog**: [v0.38.2...v0.38.3](https://github.com/cozystack/cozystack/compare/v0.38.2...v0.38.3)

View File

@@ -0,0 +1,18 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.38.4
-->
## Fixes
* **[linstor] Update piraeus-operator v2.10.2 to handle fsck checks reliably**: Upgrades LINSTOR CSI to avoid failed mounts when fsck sees mounted volumes, improving volume publish reliability ([**@kvaps**](https://github.com/kvaps) in #1689, #1697).
* **[dashboard] Nest CustomFormsOverride properties under spec.properties**: Fixes schema generation so custom form properties are placed under `spec.properties`, preventing mis-rendered or missing form fields ([**@kvaps**](https://github.com/kvaps) in #1692, #1700).
* **[virtual-machine] Guard PVC resize to only expand storage**: Ensures resize jobs run only when storage size increases, avoiding unintended shrink attempts during VM updates ([**@kvaps**](https://github.com/kvaps) in #1688, #1701).
## Documentation
* **[website] Clarify GPU check command**: Makes the kubectl command for validating GPU binding more explicit, including namespace context ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website#379).
---
**Full Changelog**: [v0.38.3...v0.38.4](https://github.com/cozystack/cozystack/compare/v0.38.3...v0.38.4)

View File

@@ -0,0 +1,95 @@
# Cozystack v0.39 — "Enhanced Networking & Monitoring"
This release introduces topology-aware routing for Cilium services, automatic pod rollouts on configuration changes, improved monitoring capabilities, and numerous bug fixes and improvements across the platform.
## Highlights
* **Topology-Aware Routing**: Enabled topology-aware routing for Cilium services, improving traffic distribution and reducing latency by routing traffic to endpoints in the same zone when possible ([**@nbykov0**](https://github.com/nbykov0) in #1734).
* **Automatic Pod Rollouts**: Cilium and Cilium operator pods now automatically restart when configuration changes, ensuring configuration updates are applied immediately ([**@kvaps**](https://github.com/kvaps) in #1728).
* **Windows VM Scheduling**: Added nodeAffinity configuration for Windows VMs based on scheduling config, enabling dedicated nodes for Windows workloads ([**@kvaps**](https://github.com/kvaps) in #1693).
* **SeaweedFS Updates**: Updated to SeaweedFS v4.02 with improved S3 daemon performance and fixes ([**@kvaps**](https://github.com/kvaps) in #1725).
---
## Major Features and Improvements
### Networking
* **[system/cilium] Enable topology-aware routing for services**: Enabled topology-aware routing for services, improving traffic distribution and reducing latency by routing traffic to endpoints in the same zone when possible. This feature helps optimize network performance in multi-zone deployments ([**@nbykov0**](https://github.com/nbykov0) in #1734).
* **[cilium] Enable automatic pod rollout on configmap updates**: Cilium and Cilium operator pods now automatically restart when the cilium-config ConfigMap is updated, ensuring configuration changes are applied immediately without manual intervention ([**@kvaps**](https://github.com/kvaps) in #1728).
### Virtual Machines
* **[virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config**: Added nodeAffinity configuration to virtual-machine and vm-instance charts to support dedicated nodes for Windows VMs. When `dedicatedNodesForWindowsVMs` is enabled in the `cozystack-scheduling` ConfigMap, Windows VMs are scheduled on nodes with label `scheduling.cozystack.io/vm-windows=true`, while non-Windows VMs prefer nodes without this label ([**@kvaps**](https://github.com/kvaps) in #1693).
### Storage
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 with improved performance for S3 daemon and fixes for known issues. This update includes better S3 compatibility and performance improvements ([**@kvaps**](https://github.com/kvaps) in #1725).
### Tools
* **[talm] feat(init)!: require --name flag for cluster name**: Breaking change: The `talm init` command now requires the `--name` flag to specify the cluster name. This ensures consistent cluster naming and prevents accidental initialization without a name ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#86).
* **[talm] feat(template): preserve extra YAML documents in output**: Templates now preserve extra YAML documents in the output, allowing for more flexible template processing ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#87).
* **[talm] feat: add directory expansion for -f flag**: Added directory expansion support for the `-f` flag, allowing users to specify directories instead of individual files ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@ca5713e).
* **[talm] Introduce automatic root detection**: Added automatic root detection logic to simplify talm usage and reduce manual configuration ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@d165162).
* **[talm] Introduce talm kubeconfig --login command**: Added new `talm kubeconfig --login` command for easier kubeconfig management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@5f7e05b).
* **[talm] Introduce encryption**: Added encryption support to talm for secure configuration management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#81).
* **[talm] Replace code-generation with wrapper on talosctl**: Refactored talm to use a wrapper on talosctl instead of code generation, simplifying the codebase and improving maintainability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#80).
* **[talm] Use go embed instead of code generation**: Migrated from code generation to go embed for better build performance and simpler dependency management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#79).
* **[boot-to-talos] Cozystack: Update Talos Linux v1.11.3**: Updated boot-to-talos to use Talos Linux v1.11.3 ([**@kvaps**](https://github.com/kvaps) in cozystack/boot-to-talos#7).
## Improvements
* **[seaweedfs] Extended CA certificate duration to reduce disruptive CA rotations**: Extended CA certificate duration to reduce disruptive CA rotations, improving long-term certificate management and reducing operational overhead ([**@IvanHunters**](https://github.com/IvanHunters) in #1657).
* **[dashboard] Add config hash annotations to restart pods on config changes**: Added config hash annotations to dashboard deployment templates to ensure pods are automatically restarted when their configuration changes, ensuring configuration updates are applied immediately ([**@kvaps**](https://github.com/kvaps) in #1662).
* **[tenant][kubernetes] Introduce better cleanup logic**: Improved cleanup logic for tenant Kubernetes resources, ensuring proper resource cleanup when tenants are deleted or updated. Added automated pre-delete cleanup job for tenant namespaces to remove tenant-related releases during uninstall ([**@kvaps**](https://github.com/kvaps) in #1661).
* **[system:coredns] update coredns app labels to match Talos coredns labels**: Updated coredns app labels to match Talos coredns labels, ensuring consistency across the platform ([**@nbykov0**](https://github.com/nbykov0) in #1675).
* **[system:monitoring-agents] rename coredns metrics service**: Renamed coredns metrics service to avoid interference with coredns service used for name resolution in tenant k8s clusters ([**@nbykov0**](https://github.com/nbykov0) in #1676).
* **[core:installer] Address buildx warnings**: Fixed Dockerfile syntax warnings from buildx, ensuring clean builds without warnings ([**@nbykov0**](https://github.com/nbykov0) in #1682).
* **[talm] Refactor root detection logic into single file**: Improved code organization by consolidating root detection logic into a single file ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@487b479).
* **[talm] Refactor init logic, better upgrade**: Improved initialization logic and upgrade process for better reliability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@c512777).
* **[talm] Sugar for kubeconfig command**: Added convenience features to the kubeconfig command for improved usability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@a4010b3).
* **[talm] wrap upgrade command**: Wrapped upgrade command for better integration and error handling ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@2e1afbf).
* **[talm] docs(readme): add Homebrew installation option**: Added Homebrew installation option to the README for easier installation on macOS ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm@12bd4f2).
* **[talm] cozystack: disable nodeCIDRs allocation**: Disabled nodeCIDRs allocation in talm for better network configuration control ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#82).
* **[talm] Update license to Apache2.0**: Updated license to Apache 2.0 for better compatibility and clarity ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@eda1032).
## Fixes
* **[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK**: Refactored 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, fixed 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 fix includes the upstream fix from kubernetes/kubernetes#135537 ([**@kvaps**](https://github.com/kvaps) in #1679).
* **[dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties**: Fixed the logic for generating CustomFormsOverride schema to properly nest properties under `spec.properties` instead of directly under `properties`, ensuring correct form schema generation ([**@kvaps**](https://github.com/kvaps) in #1692).
* **[virtual-machine] Improve check for resizing job**: Improved storage resize logic to only expand persistent volume claims when storage is being increased, preventing unintended storage reduction operations. Added validation to accurately compare current and desired storage sizes before triggering resize operations ([**@kvaps**](https://github.com/kvaps) in #1688).
* **[linstor] Update piraeus-operator v2.10.2**: Updated LINSTOR CSI to fix issues with the new fsck behaviour, resolving mount failures when fsck attempts to run on mounted devices ([**@kvaps**](https://github.com/kvaps) in #1689).
* **[api] Revert dynamic list kinds representation fix (fixes namespace deletion regression)**: Reverted changes from #1630 that caused a regression affecting namespace deletion and upgrades from previous versions. The regression caused namespace deletion failures with errors like "content is not a list: []unstructured.Unstructured" during namespace finalization. This revert restores compatibility with namespace deletion controller and fixes upgrade issues from previous versions ([**@kvaps**](https://github.com/kvaps) in #1677).
* **[talm] fix: normalize template paths for Windows compatibility**: Fixed template path handling to ensure Windows compatibility by normalizing paths ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#88).
## Dependencies
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 ([**@kvaps**](https://github.com/kvaps) in #1725).
* **[linstor] Update piraeus-operator v2.10.2**: Updated piraeus-operator to version 2.10.2 ([**@kvaps**](https://github.com/kvaps) in #1689).
* **[talm] Cozystack: Update Talos Linux v1.11.3**: Updated talm to use Talos Linux v1.11.3 ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#83).
## Documentation
* **[website] Add article: Talm v0.17: Built-in Age Encryption for Secrets Management**: Added comprehensive blog post announcing Talm v0.17 and its built-in age-based encryption for secrets. Covers initial setup and key generation, encryption/decryption workflows, idempotent encryption behavior, automatic .gitignore handling, file permission safeguards, security best practices, and guidance for GitOps and CI/CD integration ([**@kvaps**](https://github.com/kvaps) in [cozystack/website#384](https://github.com/cozystack/website/pull/384)).
* **[website] docs(talm): update talm init syntax for mandatory --preset and --name flags**: Updated documentation to reflect breaking changes in talm, adding mandatory `--preset` and `--name` flags to the talm init command ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#386).
---
## Contributors
We'd like to thank all contributors who made this release possible:
* [**@IvanHunters**](https://github.com/IvanHunters)
* [**@kvaps**](https://github.com/kvaps)
* [**@lexfrei**](https://github.com/lexfrei)
* [**@nbykov0**](https://github.com/nbykov0)
---
**Full Changelog**: [v0.38.0...v0.39.0](https://github.com/cozystack/cozystack/compare/v0.38.0...v0.39.0)
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.39.0
-->

View File

@@ -0,0 +1,12 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.39.1
-->
## Features and Improvements
* **[monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant monitoring**: Introduced 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, added a VMAgent resource template for scraping metrics within tenant namespaces, improving monitoring granularity and control. This enhancement allows administrators to configure which alert severities are sent to Slack and enables tenant-specific metrics collection for better observability ([**@IvanHunters**](https://github.com/IvanHunters) in #1712).
---
**Full Changelog**: [v0.39.0...v0.39.1](https://github.com/cozystack/cozystack/compare/v0.39.0...v0.39.1)

View File

@@ -1,31 +0,0 @@
apiVersion: cozystack.io/v1alpha1
kind: Platform
metadata:
name: cozystack-platform
# Cluster-scoped resource, no namespace needed
spec:
# SourceRef is required - reference to the OCIRepository or GitRepository
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
# Optional: Interval for HelmRelease reconciliation (default: 5m)
interval: 5m
# Optional: BasePath is the base path where the platform chart is located in the source.
# For GitRepository, defaults to "packages/core/platform" if not specified.
# For OCIRepository, defaults to "core/platform" if not specified.
# basePath: core/platform
# Optional: Values to pass to HelmRelease
# These values will be merged with sourceRef (which is automatically added)
values:
# Any custom values can be added here
# sourceRef will be automatically added by the controller
# Example custom values:
# customKey: customValue
# nested:
# config:
# enabled: true

8
go.mod
View File

@@ -6,16 +6,14 @@ go 1.25.0
require (
github.com/fluxcd/helm-controller/api v1.4.3
github.com/fluxcd/source-controller/api v1.6.2
github.com/fluxcd/source-watcher/api/v2 v2.0.2
github.com/go-logr/logr v1.4.3
github.com/go-logr/zapr v1.3.0
github.com/google/gofuzz v1.2.0
github.com/onsi/ginkgo/v2 v2.23.3
github.com/onsi/gomega v1.37.0
github.com/prometheus/client_golang v1.22.0
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.34.1
@@ -46,7 +44,6 @@ 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.22.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -127,3 +124,6 @@ require (
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
// See: issues.k8s.io/135537
replace k8s.io/apimachinery => github.com/cozystack/apimachinery v0.0.0-20251219010959-1f91eabae46c

12
go.sum
View File

@@ -18,6 +18,8 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cozystack/apimachinery v0.0.0-20251219010959-1f91eabae46c h1:C2wIfH/OzhU9XOK/e6Ik9cg7nZ1z6fN4lf6a3yFdik8=
github.com/cozystack/apimachinery v0.0.0-20251219010959-1f91eabae46c/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -35,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.22.0 h1:EHWQH5ZWml7i8eZ/AMjm1jxid3j/PQ31p+hIwCt6crM=
github.com/fluxcd/pkg/apis/meta v1.22.0/go.mod h1:Kc1+bWe5p0doROzuV9XiTfV/oL3ddsemYXt8ZYWdVVg=
github.com/fluxcd/source-controller/api v1.6.2 h1:UmodAeqLIeF29HdTqf2GiacZyO+hJydJlepDaYsMvhc=
github.com/fluxcd/source-controller/api v1.6.2/go.mod h1:ZJcAi0nemsnBxjVgmJl0WQzNvB0rMETxQMTdoFosmMw=
github.com/fluxcd/source-watcher/api/v2 v2.0.2 h1:fWSxsDqYN7My2AEpQwbP7O6Qjix8nGBX+UE/qWHtZfM=
github.com/fluxcd/source-watcher/api/v2 v2.0.2/go.mod h1:Hs6ueayPt23jlkIr/d1pGPZ+OHiibQwWjxvU6xqljzg=
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=
@@ -146,6 +142,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
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=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -298,8 +296,6 @@ k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=

View File

@@ -48,7 +48,7 @@ echo " End: $RELEASE_END"
echo ""
# Loop through ALL optional repositories
for repo_name in talm boot-to-talos cozypkg cozy-proxy; do
for repo_name in talm boot-to-talos cozyhr cozy-proxy; do
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Checking repository: $repo_name"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

View File

@@ -12,19 +12,13 @@ command -V tar >/dev/null || exit $?
echo "Collecting Cozystack information..."
mkdir -p $REPORT_DIR/cozystack
kubectl get deploy -n cozy-system cozystack-operator cozystack-controller -o yaml > $REPORT_DIR/cozystack/deployments.yaml 2>&1
echo "Collecting platforms..."
kubectl get platforms.cozystack.io -A > $REPORT_DIR/cozystack/platforms.txt 2>&1
kubectl get platforms.cozystack.io -A -o yaml > $REPORT_DIR/cozystack/platforms.yaml 2>&1
echo "Collecting bundles..."
kubectl get bundles.cozystack.io -A > $REPORT_DIR/cozystack/bundles.txt 2>&1
kubectl get bundles.cozystack.io -A -o yaml > $REPORT_DIR/cozystack/bundles.yaml 2>&1
echo "Collecting applicationdefinitions..."
kubectl get applicationdefinitions.cozystack.io -A > $REPORT_DIR/cozystack/applicationdefinitions.txt 2>&1
kubectl get applicationdefinitions.cozystack.io -A -o yaml > $REPORT_DIR/cozystack/applicationdefinitions.yaml 2>&1
kubectl get deploy -n cozy-system cozystack -o jsonpath='{.spec.template.spec.containers[0].image}' > $REPORT_DIR/cozystack/image.txt 2>&1
kubectl get cm -n cozy-system --no-headers | awk '$1 ~ /^cozystack/' |
while read NAME _; do
DIR=$REPORT_DIR/cozystack/configs
mkdir -p $DIR
kubectl get cm -n cozy-system $NAME -o yaml > $DIR/$NAME.yaml 2>&1
done
# -- kubernetes module
@@ -62,36 +56,6 @@ kubectl get hr -A --no-headers | awk '$4 != "True"' | \
kubectl describe hr -n $NAMESPACE $NAME > $DIR/describe.txt 2>&1
done
echo "Collecting artifactgenerators..."
kubectl get artifactgenerators.source.extensions.fluxcd.io -A > $REPORT_DIR/kubernetes/artifactgenerators.txt 2>&1
kubectl get artifactgenerators.source.extensions.fluxcd.io -A --no-headers | awk '$4 != "True"' | \
while read NAMESPACE NAME _; do
DIR=$REPORT_DIR/kubernetes/artifactgenerators/$NAMESPACE/$NAME
mkdir -p $DIR
kubectl get artifactgenerators.source.extensions.fluxcd.io -n $NAMESPACE $NAME -o yaml > $DIR/artifactgenerator.yaml 2>&1
kubectl describe artifactgenerators.source.extensions.fluxcd.io -n $NAMESPACE $NAME > $DIR/describe.txt 2>&1
done
echo "Collecting ocirepositories..."
kubectl get ocirepositories.source.toolkit.fluxcd.io -A > $REPORT_DIR/kubernetes/ocirepositories.txt 2>&1
kubectl get ocirepositories.source.toolkit.fluxcd.io -A --no-headers | awk '$4 != "True"' | \
while read NAMESPACE NAME _; do
DIR=$REPORT_DIR/kubernetes/ocirepositories/$NAMESPACE/$NAME
mkdir -p $DIR
kubectl get ocirepositories.source.toolkit.fluxcd.io -n $NAMESPACE $NAME -o yaml > $DIR/ocirepository.yaml 2>&1
kubectl describe ocirepositories.source.toolkit.fluxcd.io -n $NAMESPACE $NAME > $DIR/describe.txt 2>&1
done
echo "Collecting gitrepositories..."
kubectl get gitrepositories.source.toolkit.fluxcd.io -A > $REPORT_DIR/kubernetes/gitrepositories.txt 2>&1
kubectl get gitrepositories.source.toolkit.fluxcd.io -A --no-headers | awk '$4 != "True"' | \
while read NAMESPACE NAME _; do
DIR=$REPORT_DIR/kubernetes/gitrepositories/$NAMESPACE/$NAME
mkdir -p $DIR
kubectl get gitrepositories.source.toolkit.fluxcd.io -n $NAMESPACE $NAME -o yaml > $DIR/gitrepository.yaml 2>&1
kubectl describe gitrepositories.source.toolkit.fluxcd.io -n $NAMESPACE $NAME > $DIR/describe.txt 2>&1
done
echo "Collecting pods..."
kubectl get pod -A -o wide > $REPORT_DIR/kubernetes/pods.txt 2>&1
kubectl get pod -A --no-headers | awk '$4 !~ /Running|Succeeded|Completed/' |

View File

@@ -72,7 +72,7 @@ EOF
kubectl wait --for=condition=TenantControlPlaneCreated kamajicontrolplane -n tenant-test kubernetes-${test_name} --timeout=4m
# Wait for Kubernetes resources to be ready (timeout after 2 minutes)
kubectl wait tcp -n tenant-test kubernetes-${test_name} --timeout=2m --for=jsonpath='{.status.kubernetesResources.version.status}'=Ready
kubectl wait tcp -n tenant-test kubernetes-${test_name} --timeout=5m --for=jsonpath='{.status.kubernetesResources.version.status}'=Ready
# Wait for all required deployments to be available (timeout after 4 minutes)
kubectl wait deploy --timeout=4m --for=condition=available -n tenant-test kubernetes-${test_name} kubernetes-${test_name}-cluster-autoscaler kubernetes-${test_name}-kccm kubernetes-${test_name}-kcsi-controller
@@ -87,7 +87,7 @@ EOF
# Set up port forwarding to the Kubernetes API server for a 200 second timeout
bash -c 'timeout 300s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
bash -c 'timeout 500s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
# Verify the Kubernetes version matches what we expect (retry for up to 20 seconds)
timeout 20 sh -ec 'until kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' version 2>/dev/null | grep -Fq "Server Version: ${k8s_version}"; do sleep 5; done'
@@ -124,6 +124,100 @@ EOF
exit 1
fi
kubectl --kubeconfig tenantkubeconfig-${test_name} apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: tenant-test
EOF
# Backend 1
kubectl apply --kubeconfig tenantkubeconfig-${test_name} -f- <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: "${test_name}-backend"
namespace: tenant-test
spec:
replicas: 1
selector:
matchLabels:
app: backend
backend: "${test_name}-backend"
template:
metadata:
labels:
app: backend
backend: "${test_name}-backend"
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 2
periodSeconds: 2
EOF
# LoadBalancer Service
kubectl apply --kubeconfig tenantkubeconfig-${test_name} -f- <<EOF
apiVersion: v1
kind: Service
metadata:
name: "${test_name}-backend"
namespace: tenant-test
spec:
type: LoadBalancer
selector:
app: backend
backend: "${test_name}-backend"
ports:
- port: 80
targetPort: 80
EOF
# Wait for pods readiness
kubectl wait deployment --kubeconfig tenantkubeconfig-${test_name} ${test_name}-backend -n tenant-test --for=condition=Available --timeout=90s
# Wait for LoadBalancer to be provisioned (IP or hostname)
timeout 90 sh -ec "
until kubectl get svc ${test_name}-backend --kubeconfig tenantkubeconfig-${test_name} -n tenant-test \
-o jsonpath='{.status.loadBalancer.ingress[0]}' | grep -q .; do
sleep 5
done
"
LB_ADDR=$(
kubectl get svc --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" \
-n tenant-test \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}'
)
if [ -z "$LB_ADDR" ]; then
echo "LoadBalancer address is empty" >&2
exit 1
fi
for i in $(seq 1 20); do
echo "Attempt $i"
curl --silent --fail "http://${LB_ADDR}" && break
sleep 3
done
if [ "$i" -eq 20 ]; then
echo "LoadBalancer not reachable" >&2
exit 1
fi
# Cleanup
kubectl delete deployment --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" -n tenant-test
kubectl delete service --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" -n tenant-test
# Wait for all machine deployment replicas to be ready (timeout after 10 minutes)
kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=10m --for=jsonpath='{.status.v1beta2.readyReplicas}'=2

View File

@@ -8,51 +8,23 @@
}
@test "Install Cozystack" {
# Create namespace & configmap required by installer
kubectl create namespace cozy-system --dry-run=client -o yaml | kubectl apply -f -
kubectl create configmap cozystack -n cozy-system \
--from-literal=bundle-name=paas-full \
--from-literal=ipv4-pod-cidr=10.244.0.0/16 \
--from-literal=ipv4-pod-gateway=10.244.0.1 \
--from-literal=ipv4-svc-cidr=10.96.0.0/16 \
--from-literal=ipv4-join-cidr=100.64.0.0/16 \
--from-literal=root-host=example.org \
--from-literal=api-server-endpoint=https://192.168.123.10:6443 \
--dry-run=client -o yaml | kubectl apply -f -
# Apply installer manifests from file
kubectl apply -f _out/assets/cozystack-installer.yaml
# Wait for the installer deployment to become available
kubectl wait deployment/cozystack-operator -n cozy-system --timeout=1m --for=condition=Available
# Wait for cozy-fluxcd namespace to be created
timeout 30 sh -ec 'until kubectl get namespace cozy-fluxcd >/dev/null 2>&1; do sleep 1; done'
# Wait for Flux deployment
timeout 30 sh -ec 'until kubectl get deployment/flux -n cozy-fluxcd >/dev/null 2>&1; do sleep 1; done'
kubectl wait deployment/flux -n cozy-fluxcd --timeout=1m --for=condition=Available
# Create Platform resource instead of configmap
kubectl apply -f - <<'EOF'
apiVersion: cozystack.io/v1alpha1
kind: Platform
metadata:
name: cozystack-platform
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
values:
bundles:
system:
type: "full"
networking:
podCIDR: "10.244.0.0/16"
podGateway: "10.244.0.1"
serviceCIDR: "10.96.0.0/16"
joinCIDR: "100.64.0.0/16"
publishing:
host: "example.org"
apiServerEndpoint: "https://192.168.123.10:6443"
EOF
# Wait for ArtifactGenerator for cozystack-packages
timeout 60 sh -ec 'until kubectl get artifactgenerators.source.extensions.fluxcd.io cozystack-packages -n cozy-system >/dev/null 2>&1; do sleep 1; done'
kubectl wait artifactgenerators.source.extensions.fluxcd.io/cozystack-packages -n cozy-system --for=condition=ready --timeout=5m
# Wait for bundle ArtifactGenerators
timeout 60 sh -ec 'until kubectl get artifactgenerators.source.extensions.fluxcd.io cozystack-system cozystack-iaas cozystack-paas cozystack-naas -n cozy-system >/dev/null 2>&1; do sleep 1; done'
kubectl wait artifactgenerators.source.extensions.fluxcd.io -n cozy-system --for=condition=ready --timeout=5m cozystack-system cozystack-iaas cozystack-paas cozystack-naas
kubectl wait deployment/cozystack -n cozy-system --timeout=1m --for=condition=Available
# Wait until HelmReleases appear & reconcile them
timeout 60 sh -ec 'until kubectl get hr -A -l cozystack.io/system-app=true | grep -q cozys; do sleep 1; done'
@@ -168,8 +140,9 @@ EOF
kubectl wait hr/seaweedfs-system -n tenant-root --timeout=2m --for=condition=ready
fi
# Expose Cozystack services through ingress
kubectl patch platform/cozystack-platform --type merge -p '{"spec":{"values":{"publishing":{"exposedServices":["api","dashboard","cdi-uploadproxy","vm-exportproxy","keycloak"]}}}}'
kubectl patch configmap/cozystack -n cozy-system --type merge -p '{"data":{"expose-services":"api,dashboard,cdi-uploadproxy,vm-exportproxy,keycloak"}}'
# NGINX ingress controller
timeout 60 sh -ec 'until kubectl get deploy root-ingress-controller -n tenant-root >/dev/null 2>&1; do sleep 1; done'
@@ -196,7 +169,7 @@ EOF
}
@test "Keycloak OIDC stack is healthy" {
kubectl patch platform/cozystack-platform --type merge -p '{"spec":{"values":{"authentication":{"oidc":{"enabled":true}}}}}'
kubectl patch configmap/cozystack -n cozy-system --type merge -p '{"data":{"oidc-enabled":"true"}}'
timeout 120 sh -ec 'until kubectl get hr -n cozy-keycloak keycloak keycloak-configure keycloak-operator >/dev/null 2>&1; do sleep 1; done'
kubectl wait hr/keycloak hr/keycloak-configure hr/keycloak-operator -n cozy-keycloak --timeout=10m --for=condition=ready

View File

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

View File

@@ -54,9 +54,5 @@ kube::codegen::gen_openapi \
$CONTROLLER_GEN object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=packages/system/cozystack-controller/crds
mv packages/system/cozystack-controller/crds/cozystack.io_applicationdefinitions.yaml \
packages/core/installer/crds/cozystack.io_applicationdefinitions.yaml
mv packages/system/cozystack-controller/crds/cozystack.io_bundles.yaml \
packages/core/installer/crds/cozystack.io_bundles.yaml
mv packages/system/cozystack-controller/crds/cozystack.io_platforms.yaml \
packages/core/installer/crds/cozystack.io_platforms.yaml
mv packages/system/cozystack-controller/crds/cozystack.io_cozystackresourcedefinitions.yaml \
packages/system/cozystack-resource-definition-crd/definition/cozystack.io_cozystackresourcedefinitions.yaml

View File

@@ -8,7 +8,6 @@ need yq; need jq; need base64
CHART_YAML="${CHART_YAML:-Chart.yaml}"
VALUES_YAML="${VALUES_YAML:-values.yaml}"
SCHEMA_JSON="${SCHEMA_JSON:-values.schema.json}"
CRD_DIR="../../core/platform/bundles/*/applicationdefinitions"
[[ -f "$CHART_YAML" ]] || { echo "No $CHART_YAML found"; exit 1; }
[[ -f "$SCHEMA_JSON" ]] || { echo "No $SCHEMA_JSON found"; exit 1; }
@@ -22,6 +21,8 @@ if [[ -z "$NAME" ]]; then
echo "Chart.yaml: .name is empty"; exit 1
fi
CRD_DIR="../../system/${NAME}-rd/cozyrds"
# Resolve icon path
# Accepts:
# /logos/foo.svg -> ./logos/foo.svg
@@ -54,71 +55,37 @@ fi
# Base64 (portable: no -w / -b options)
ICON_B64="$(base64 < "$ICON_PATH" | tr -d '\n' | tr -d '\r')"
# Find path to output CRD YAML
OUT="$(find $CRD_DIR -type f -name "${NAME}.yaml" | head -n 1)"
if [[ -z "$OUT" ]]; then
echo "Error: ApplicationDefinition file for '${NAME}' not found in ${CRD_DIR}"
echo "Please create the file first in one of the following directories:"
# Auto-detect existing directories
BASE_DIR="../../core/platform/bundles"
if [[ -d "$BASE_DIR" ]]; then
for bundle_dir in "$BASE_DIR"/*/applicationdefinitions; do
if [[ -d "$bundle_dir" ]]; then
bundle_name="$(basename "$(dirname "$bundle_dir")")"
echo " touch ${bundle_dir}/${NAME}.yaml # ${bundle_name}"
fi
done
else
# Fallback if base directory doesn't exist
echo " touch ../../core/platform/bundles/iaas/applicationdefinitions/${NAME}.yaml"
echo " touch ../../core/platform/bundles/paas/applicationdefinitions/${NAME}.yaml"
echo " touch ../../core/platform/bundles/naas/applicationdefinitions/${NAME}.yaml"
echo " touch ../../core/platform/bundles/system/applicationdefinitions/${NAME}.yaml"
fi
exit 1
fi
# Decide which HelmRepository name to use based on path
# .../apps/... -> cozystack-apps
# .../extra/... -> cozystack-extra
# default: cozystack-apps
SOURCE_NAME="cozystack-apps"
case "$PWD" in
*"/apps/"*) SOURCE_NAME="cozystack-apps" ;;
*"/extra/"*) SOURCE_NAME="cozystack-extra" ;;
esac
if [[ ! -s "$OUT" ]]; then
# If file doesn't exist, create a minimal skeleton
OUT="${OUT:-$CRD_DIR/$NAME.yaml}"
if [[ ! -f "$OUT" ]]; then
cat >"$OUT" <<EOF
apiVersion: cozystack.io/v1alpha1
kind: ApplicationDefinition
kind: CozystackResourceDefinition
metadata:
name: ${NAME}
spec:
release:
values:
_cozystack:
spec: {}
EOF
fi
# Determine package type (apps or extra) from current directory
CURRENT_DIR="$(pwd)"
PACKAGE_TYPE="apps" # default
if [[ "$CURRENT_DIR" == *"/packages/extra/"* ]]; then
PACKAGE_TYPE="extra"
elif [[ "$CURRENT_DIR" == *"/packages/apps/"* ]]; then
PACKAGE_TYPE="apps"
fi
# Extract bundle type (iaas, paas, naas, system) from OUT path
OUT_DIR="$(dirname "$OUT")"
BUNDLE_DIR="$(dirname "$OUT_DIR")"
BUNDLE_TYPE="$(basename "$BUNDLE_DIR")"
ARTIFACT_PREFIX="cozystack-${BUNDLE_TYPE}"
ARTIFACT_NAME="${ARTIFACT_PREFIX}-${NAME}"
# Export vars for yq env()
export RES_NAME="$NAME"
# For packages/extra, prefix should be empty; for packages/apps, prefix is "${NAME}-"
if [[ "$PACKAGE_TYPE" == "extra" ]]; then
export PREFIX="$NAME-"
if [ "$SOURCE_NAME" == "cozystack-extra" ]; then
export PREFIX=""
else
export PREFIX="${NAME}-"
fi
export DESCRIPTION="$DESC"
export ICON_B64="$ICON_B64"
export ARTIFACT_NAME="$ARTIFACT_NAME"
export SOURCE_NAME="$SOURCE_NAME"
export SCHEMA_JSON_MIN="$(jq -c . "$SCHEMA_JSON")"
# Generate keysOrder from values.yaml
@@ -148,12 +115,6 @@ export KEYS_ORDER="$(
'
)"
# Remove lines with cozystack.build-values before updating (Helm template syntax breaks yq parsing)
if [[ -f "$OUT" && -n "$OUT" ]]; then
# Use grep to filter out the line, more reliable than sed
grep -v 'cozystack\.build-values' "$OUT" > "${OUT}.tmp" && mv "${OUT}.tmp" "$OUT"
fi
# Update only necessary fields in-place
# - openAPISchema is loaded from file as a multi-line string (block scalar)
# - labels ensure cozystack.io/ui: "true"
@@ -161,26 +122,19 @@ fi
# - sourceRef derived from directory (apps|extra)
yq -i '
.apiVersion = (.apiVersion // "cozystack.io/v1alpha1") |
.kind = (.kind // "ApplicationDefinition") |
.kind = (.kind // "CozystackResourceDefinition") |
.metadata.name = strenv(RES_NAME) |
.spec.application.openAPISchema = strenv(SCHEMA_JSON_MIN) |
(.spec.application.openAPISchema style="literal") |
.spec.release.prefix = (strenv(PREFIX)) |
.spec.release.labels."cozystack.io/ui" = "true" |
del(.spec.release.chart) |
.spec.release.chartRef.sourceRef.kind = "ExternalArtifact" |
.spec.release.chartRef.sourceRef.name = strenv(ARTIFACT_NAME) |
.spec.release.chartRef.sourceRef.namespace = "cozy-system" |
.spec.release.chart.name = strenv(RES_NAME) |
.spec.release.chart.sourceRef.kind = "HelmRepository" |
.spec.release.chart.sourceRef.name = strenv(SOURCE_NAME) |
.spec.release.chart.sourceRef.namespace = "cozy-public" |
.spec.dashboard.description = strenv(DESCRIPTION) |
.spec.dashboard.icon = strenv(ICON_B64) |
.spec.dashboard.keysOrder = env(KEYS_ORDER)
' "$OUT"
# Add back the Helm template line after _cozystack
if [[ -f "$OUT" && -n "$OUT" ]]; then
HELM_TEMPLATE=' {{- include "cozystack.build-values" . | nindent 8 }}'
# Use awk for more reliable insertion
awk -v template="$HELM_TEMPLATE" '/_cozystack:/ {print; print template; next} {print}' "$OUT" > "${OUT}.tmp" && mv "${OUT}.tmp" "$OUT"
fi
echo "Updated $OUT"

View File

@@ -1,502 +0,0 @@
package controller
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"slices"
"sync"
"time"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/cozystack/cozystack/pkg/cozylib"
)
// +kubebuilder:rbac:groups=cozystack.io,resources=applicationdefinitions,verbs=get;list;watch
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;update;patch
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
type ApplicationDefinitionReconciler struct {
client.Client
Scheme *runtime.Scheme
Debounce time.Duration
mu sync.Mutex
lastEvent time.Time
lastHandled time.Time
CozystackAPIKind string
}
func (r *ApplicationDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("Reconciling ApplicationDefinitions", "request", req.NamespacedName)
// Get all ApplicationDefinitions
crdList := &cozyv1alpha1.ApplicationDefinitionList{}
if err := r.List(ctx, crdList); err != nil {
logger.Error(err, "failed to list ApplicationDefinitions")
return ctrl.Result{}, err
}
logger.Info("Found ApplicationDefinitions", "count", len(crdList.Items))
// Update HelmReleases for each CRD
for i := range crdList.Items {
crd := &crdList.Items[i]
logger.V(4).Info("Processing CRD", "crd", crd.Name, "hasValues", crd.Spec.Release.Values != nil)
if err := r.updateHelmReleasesForCRD(ctx, crd); err != nil {
logger.Error(err, "failed to update HelmReleases for CRD", "crd", crd.Name)
// Continue with other CRDs even if one fails
}
}
// Continue with debounced restart logic
return r.debouncedRestart(ctx)
}
func (r *ApplicationDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error {
if r.Debounce == 0 {
r.Debounce = 5 * time.Second
}
return ctrl.NewControllerManagedBy(mgr).
Named("applicationdefinition-controller").
Watches(
&cozyv1alpha1.ApplicationDefinition{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
r.mu.Lock()
r.lastEvent = time.Now()
r.mu.Unlock()
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Namespace: "cozy-system",
Name: "cozystack-api",
},
}}
}),
).
Watches(
&helmv2.HelmRelease{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
hr, ok := obj.(*helmv2.HelmRelease)
if !ok {
return nil
}
// Only watch HelmReleases with cozystack.io/ui=true label
if hr.Labels == nil || hr.Labels["cozystack.io/ui"] != "true" {
return nil
}
// Trigger reconciliation of all CRDs when a HelmRelease with the label is created/updated
r.mu.Lock()
r.lastEvent = time.Now()
r.mu.Unlock()
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Namespace: "cozy-system",
Name: "cozystack-api",
},
}}
}),
).
Complete(r)
}
type crdHashView struct {
Name string `json:"name"`
Spec cozyv1alpha1.ApplicationDefinitionSpec `json:"spec"`
}
func (r *ApplicationDefinitionReconciler) computeConfigHash(ctx context.Context) (string, error) {
list := &cozyv1alpha1.ApplicationDefinitionList{}
if err := r.List(ctx, list); err != nil {
return "", err
}
slices.SortFunc(list.Items, sortApplicationDefinitions)
views := make([]crdHashView, 0, len(list.Items))
for i := range list.Items {
views = append(views, crdHashView{
Name: list.Items[i].Name,
Spec: list.Items[i].Spec,
})
}
b, err := json.Marshal(views)
if err != nil {
return "", err
}
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:]), nil
}
func (r *ApplicationDefinitionReconciler) debouncedRestart(ctx context.Context) (ctrl.Result, error) {
logger := log.FromContext(ctx)
r.mu.Lock()
le := r.lastEvent
lh := r.lastHandled
debounce := r.Debounce
r.mu.Unlock()
if debounce <= 0 {
debounce = 5 * time.Second
}
if le.IsZero() {
return ctrl.Result{}, nil
}
if d := time.Since(le); d < debounce {
return ctrl.Result{RequeueAfter: debounce - d}, nil
}
if !lh.Before(le) {
return ctrl.Result{}, nil
}
newHash, err := r.computeConfigHash(ctx)
if err != nil {
return ctrl.Result{}, err
}
tpl, obj, patch, err := r.getWorkload(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"})
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
oldHash := tpl.Annotations["cozystack.io/config-hash"]
if oldHash == newHash && oldHash != "" {
r.mu.Lock()
r.lastHandled = le
r.mu.Unlock()
logger.Info("No changes in CRD config; skipping restart", "hash", newHash)
return ctrl.Result{}, nil
}
tpl.Annotations["cozystack.io/config-hash"] = newHash
if err := r.Patch(ctx, obj, patch); err != nil {
return ctrl.Result{}, err
}
r.mu.Lock()
r.lastHandled = le
r.mu.Unlock()
logger.Info("Updated cozystack-api podTemplate config-hash; rollout triggered",
"old", oldHash, "new", newHash)
return ctrl.Result{}, nil
}
func (r *ApplicationDefinitionReconciler) getWorkload(
ctx context.Context,
key types.NamespacedName,
) (tpl *corev1.PodTemplateSpec, obj client.Object, patch client.Patch, err error) {
if r.CozystackAPIKind == "Deployment" {
dep := &appsv1.Deployment{}
if err := r.Get(ctx, key, dep); err != nil {
return nil, nil, nil, err
}
obj = dep
tpl = &dep.Spec.Template
patch = client.MergeFrom(dep.DeepCopy())
} else {
ds := &appsv1.DaemonSet{}
if err := r.Get(ctx, key, ds); err != nil {
return nil, nil, nil, err
}
obj = ds
tpl = &ds.Spec.Template
patch = client.MergeFrom(ds.DeepCopy())
}
if tpl.Annotations == nil {
tpl.Annotations = make(map[string]string)
}
return tpl, obj, patch, nil
}
func sortApplicationDefinitions(a, b cozyv1alpha1.ApplicationDefinition) int {
if a.Name == b.Name {
return 0
}
if a.Name < b.Name {
return -1
}
return 1
}
// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from ApplicationDefinition
func (r *ApplicationDefinitionReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
logger := log.FromContext(ctx)
// Use application labels to find HelmReleases
// Labels: apps.cozystack.io/application.kind and apps.cozystack.io/application.group
applicationKind := crd.Spec.Application.Kind
applicationGroup := "apps.cozystack.io" // All applications use this group
// Build label selector for HelmReleases
// Only reconcile HelmReleases with cozystack.io/ui=true label
labelSelector := client.MatchingLabels{
"apps.cozystack.io/application.kind": applicationKind,
"apps.cozystack.io/application.group": applicationGroup,
"cozystack.io/ui": "true",
}
// List all HelmReleases with matching labels
hrList := &helmv2.HelmReleaseList{}
if err := r.List(ctx, hrList, labelSelector); err != nil {
logger.Error(err, "failed to list HelmReleases", "kind", applicationKind, "group", applicationGroup)
return err
}
logger.Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items), "hasValues", crd.Spec.Release.Values != nil)
if crd.Spec.Release.Values != nil {
logger.V(4).Info("CRD has values", "crd", crd.Name, "valuesSize", len(crd.Spec.Release.Values.Raw))
}
// Log each HelmRelease that will be updated
for i := range hrList.Items {
hr := &hrList.Items[i]
logger.V(4).Info("Processing HelmRelease", "name", hr.Name, "namespace", hr.Namespace, "kind", applicationKind)
}
// Update each HelmRelease
for i := range hrList.Items {
hr := &hrList.Items[i]
if err := r.updateHelmReleaseChart(ctx, hr, crd); err != nil {
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
continue
}
}
return nil
}
// updateHelmReleaseChart updates the chart/chartRef and values in HelmRelease based on ApplicationDefinition
func (r *ApplicationDefinitionReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.ApplicationDefinition) error {
logger := log.FromContext(ctx)
updated := false
hrCopy := hr.DeepCopy()
// Update based on Chart or ChartRef configuration
if crd.Spec.Release.Chart != nil {
// Using Chart (HelmRepository)
if hrCopy.Spec.Chart == nil {
// Need to create Chart spec
hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{
Spec: helmv2.HelmChartTemplateSpec{
Chart: crd.Spec.Release.Chart.Name,
SourceRef: helmv2.CrossNamespaceObjectReference{
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
Name: crd.Spec.Release.Chart.SourceRef.Name,
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
},
},
}
// Clear ChartRef if it exists
hrCopy.Spec.ChartRef = nil
updated = true
} else {
// Update existing Chart spec
if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name ||
hrCopy.Spec.Chart.Spec.SourceRef.Kind != crd.Spec.Release.Chart.SourceRef.Kind ||
hrCopy.Spec.Chart.Spec.SourceRef.Name != crd.Spec.Release.Chart.SourceRef.Name ||
hrCopy.Spec.Chart.Spec.SourceRef.Namespace != crd.Spec.Release.Chart.SourceRef.Namespace {
hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name
hrCopy.Spec.Chart.Spec.SourceRef = helmv2.CrossNamespaceObjectReference{
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
Name: crd.Spec.Release.Chart.SourceRef.Name,
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
}
// Clear ChartRef if it exists
hrCopy.Spec.ChartRef = nil
updated = true
}
}
} else if crd.Spec.Release.ChartRef != nil {
// Using ChartRef (ExternalArtifact)
expectedChartRef := &helmv2.CrossNamespaceSourceReference{
Kind: "ExternalArtifact",
Name: crd.Spec.Release.ChartRef.SourceRef.Name,
Namespace: crd.Spec.Release.ChartRef.SourceRef.Namespace,
}
if hrCopy.Spec.ChartRef == nil {
// Need to create ChartRef
hrCopy.Spec.ChartRef = expectedChartRef
// Clear Chart if it exists
hrCopy.Spec.Chart = nil
updated = true
} else {
// Update existing ChartRef
if hrCopy.Spec.ChartRef.Kind != expectedChartRef.Kind ||
hrCopy.Spec.ChartRef.Name != expectedChartRef.Name ||
hrCopy.Spec.ChartRef.Namespace != expectedChartRef.Namespace {
hrCopy.Spec.ChartRef = expectedChartRef
// Clear Chart if it exists
hrCopy.Spec.Chart = nil
updated = true
}
}
}
// Update Values from CRD if specified
var mergedValues *apiextensionsv1.JSON
var err error
if crd.Spec.Release.Values != nil {
logger.V(4).Info("Merging values from CRD", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
mergedValues, err = cozylib.MergeValuesWithCRDPriority(crd.Spec.Release.Values, hrCopy.Spec.Values)
if err != nil {
logger.Error(err, "failed to merge values", "name", hr.Name, "namespace", hr.Namespace)
return fmt.Errorf("failed to merge values: %w", err)
}
} else {
// Even if CRD has no values, we still need to ensure _namespace is set
mergedValues = hrCopy.Spec.Values
}
// Always inject namespace annotations (top-level _namespace field)
// This matches the behavior in cozystack-api and NamespaceHelmReconciler
namespace := &corev1.Namespace{}
if err := r.Get(ctx, client.ObjectKey{Name: hrCopy.Namespace}, namespace); err == nil {
mergedValues, err = cozylib.InjectNamespaceAnnotationsIntoValues(mergedValues, namespace)
if err != nil {
logger.Error(err, "failed to inject namespace annotations", "name", hr.Name, "namespace", hr.Namespace)
// Continue even if namespace annotations injection fails
}
}
// Always update values to ensure _cozystack and _namespace are applied
// This ensures that CRD values (especially _cozystack and _namespace) are always applied
// We always update to ensure CRD values are propagated, even if they appear equal
// This is important because JSON comparison might not catch all differences (e.g., field order)
if crd.Spec.Release.Values != nil || mergedValues != hrCopy.Spec.Values {
hrCopy.Spec.Values = mergedValues
updated = true
if crd.Spec.Release.Values != nil {
logger.Info("Updated values from CRD", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
} else {
logger.V(4).Info("Updated values with namespace labels", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
}
} else {
logger.V(4).Info("No values update needed", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
}
if !updated {
return nil
}
// Update the HelmRelease
patch := client.MergeFrom(hr.DeepCopy())
if err := r.Patch(ctx, hrCopy, patch); err != nil {
return err
}
logger.Info("Updated HelmRelease", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
return nil
}
// mergeHelmReleaseValues merges CRD default values with existing HelmRelease values
// All fields are merged except "_cozystack" and "_namespace" which are fully overwritten from CRD values
// Existing HelmRelease values (outside of _cozystack and _namespace) take precedence (user values override defaults)
func (r *ApplicationDefinitionReconciler) mergeHelmReleaseValues(crdValues, existingValues *apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
// If CRD has no values, preserve existing
if crdValues == nil || len(crdValues.Raw) == 0 {
return existingValues, nil
}
// If existing has no values, use CRD values
if existingValues == nil || len(existingValues.Raw) == 0 {
return crdValues, nil
}
var crdMap, existingMap map[string]interface{}
// Parse CRD values (defaults)
if err := json.Unmarshal(crdValues.Raw, &crdMap); err != nil {
return nil, fmt.Errorf("failed to unmarshal CRD values: %w", err)
}
// Parse existing HelmRelease values
if err := json.Unmarshal(existingValues.Raw, &existingMap); err != nil {
return nil, fmt.Errorf("failed to unmarshal existing values: %w", err)
}
// Start with existing values as base (user values take priority)
// Then merge CRD values on top, but _cozystack and _namespace from CRD completely overwrite
merged := deepMergeMaps(existingMap, crdMap)
// Explicitly handle "_cozystack" field: CRD values completely overwrite existing
// This ensures _cozystack field from CRD is always used, even if user modified it
if crdCozystack, exists := crdMap["_cozystack"]; exists {
merged["_cozystack"] = crdCozystack
}
// Explicitly handle "_namespace" field: CRD values completely overwrite existing
// This ensures _namespace field from CRD is always used, even if user modified it
if crdNamespace, exists := crdMap["_namespace"]; exists {
merged["_namespace"] = crdNamespace
}
mergedJSON, err := json.Marshal(merged)
if err != nil {
return nil, fmt.Errorf("failed to marshal merged values: %w", err)
}
return &apiextensionsv1.JSON{Raw: mergedJSON}, nil
}
// deepMergeMaps performs a deep merge of two maps
func deepMergeMaps(base, override map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
// Copy base map
for k, v := range base {
result[k] = v
}
// Merge override map
for k, v := range override {
if baseVal, exists := result[k]; exists {
// If both are maps, recursively merge
if baseMap, ok := baseVal.(map[string]interface{}); ok {
if overrideMap, ok := v.(map[string]interface{}); ok {
result[k] = deepMergeMaps(baseMap, overrideMap)
continue
}
}
}
// Override takes precedence
result[k] = v
}
return result
}
// valuesEqual compares two JSON values for equality
func valuesEqual(a, b *apiextensionsv1.JSON) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
// Simple byte comparison (could be improved with canonical JSON)
return string(a.Raw) == string(b.Raw)
}

View File

@@ -0,0 +1,189 @@
package controller
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"slices"
"sync"
"time"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/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/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
type CozystackResourceDefinitionReconciler struct {
client.Client
Scheme *runtime.Scheme
Debounce time.Duration
mu sync.Mutex
lastEvent time.Time
lastHandled time.Time
CozystackAPIKind string
}
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Only handle debounced restart logic
// HelmRelease reconciliation is handled by CozystackResourceDefinitionHelmReconciler
return r.debouncedRestart(ctx)
}
func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error {
if r.Debounce == 0 {
r.Debounce = 5 * time.Second
}
return ctrl.NewControllerManagedBy(mgr).
Named("cozystackresource-controller").
Watches(
&cozyv1alpha1.CozystackResourceDefinition{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
r.mu.Lock()
r.lastEvent = time.Now()
r.mu.Unlock()
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Namespace: "cozy-system",
Name: "cozystack-api",
},
}}
}),
).
Complete(r)
}
type crdHashView struct {
Name string `json:"name"`
Spec cozyv1alpha1.CozystackResourceDefinitionSpec `json:"spec"`
}
func (r *CozystackResourceDefinitionReconciler) computeConfigHash(ctx context.Context) (string, error) {
list := &cozyv1alpha1.CozystackResourceDefinitionList{}
if err := r.List(ctx, list); err != nil {
return "", err
}
slices.SortFunc(list.Items, sortCozyRDs)
views := make([]crdHashView, 0, len(list.Items))
for i := range list.Items {
views = append(views, crdHashView{
Name: list.Items[i].Name,
Spec: list.Items[i].Spec,
})
}
b, err := json.Marshal(views)
if err != nil {
return "", err
}
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:]), nil
}
func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Context) (ctrl.Result, error) {
logger := log.FromContext(ctx)
r.mu.Lock()
le := r.lastEvent
lh := r.lastHandled
debounce := r.Debounce
r.mu.Unlock()
if debounce <= 0 {
debounce = 5 * time.Second
}
if le.IsZero() {
return ctrl.Result{}, nil
}
if d := time.Since(le); d < debounce {
return ctrl.Result{RequeueAfter: debounce - d}, nil
}
if !lh.Before(le) {
return ctrl.Result{}, nil
}
newHash, err := r.computeConfigHash(ctx)
if err != nil {
return ctrl.Result{}, err
}
tpl, obj, patch, err := r.getWorkload(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"})
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
oldHash := tpl.Annotations["cozystack.io/config-hash"]
if oldHash == newHash && oldHash != "" {
r.mu.Lock()
r.lastHandled = le
r.mu.Unlock()
logger.Info("No changes in CRD config; skipping restart", "hash", newHash)
return ctrl.Result{}, nil
}
tpl.Annotations["cozystack.io/config-hash"] = newHash
if err := r.Patch(ctx, obj, patch); err != nil {
return ctrl.Result{}, err
}
r.mu.Lock()
r.lastHandled = le
r.mu.Unlock()
logger.Info("Updated cozystack-api podTemplate config-hash; rollout triggered",
"old", oldHash, "new", newHash)
return ctrl.Result{}, nil
}
func (r *CozystackResourceDefinitionReconciler) getWorkload(
ctx context.Context,
key types.NamespacedName,
) (tpl *corev1.PodTemplateSpec, obj client.Object, patch client.Patch, err error) {
if r.CozystackAPIKind == "Deployment" {
dep := &appsv1.Deployment{}
if err := r.Get(ctx, key, dep); err != nil {
return nil, nil, nil, err
}
obj = dep
tpl = &dep.Spec.Template
patch = client.MergeFrom(dep.DeepCopy())
} else {
ds := &appsv1.DaemonSet{}
if err := r.Get(ctx, key, ds); err != nil {
return nil, nil, nil, err
}
obj = ds
tpl = &ds.Spec.Template
patch = client.MergeFrom(ds.DeepCopy())
}
if tpl.Annotations == nil {
tpl.Annotations = make(map[string]string)
}
return tpl, obj, patch, nil
}
func sortCozyRDs(a, b cozyv1alpha1.CozystackResourceDefinition) int {
if a.Name == b.Name {
return 0
}
if a.Name < b.Name {
return -1
}
return 1
}

View File

@@ -0,0 +1,201 @@
package controller
import (
"context"
"fmt"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// +kubebuilder:rbac:groups=cozystack.io,resources=cozystackresourcedefinitions,verbs=get;list;watch
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;update;patch
// CozystackResourceDefinitionHelmReconciler reconciles CozystackResourceDefinitions
// and updates related HelmReleases when a CozyRD changes.
// This controller does NOT watch HelmReleases to avoid mutual reconciliation storms
// with Flux's helm-controller.
type CozystackResourceDefinitionHelmReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *CozystackResourceDefinitionHelmReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// Get the CozystackResourceDefinition that triggered this reconciliation
crd := &cozyv1alpha1.CozystackResourceDefinition{}
if err := r.Get(ctx, req.NamespacedName, crd); err != nil {
logger.Error(err, "failed to get CozystackResourceDefinition", "name", req.Name)
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Update HelmReleases related to this specific CozyRD
if err := r.updateHelmReleasesForCRD(ctx, crd); err != nil {
logger.Error(err, "failed to update HelmReleases for CRD", "crd", crd.Name)
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *CozystackResourceDefinitionHelmReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Named("cozystackresourcedefinition-helm-reconciler").
For(&cozyv1alpha1.CozystackResourceDefinition{}).
Complete(r)
}
// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from CozystackResourceDefinition
func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
logger := log.FromContext(ctx)
// Use application labels to find HelmReleases
// Labels: apps.cozystack.io/application.kind and apps.cozystack.io/application.group
applicationKind := crd.Spec.Application.Kind
// Validate that applicationKind is non-empty
if applicationKind == "" {
logger.V(4).Info("Skipping HelmRelease update: Application.Kind is empty", "crd", crd.Name)
return nil
}
applicationGroup := "apps.cozystack.io" // All applications use this group
// Build label selector for HelmReleases
// Only reconcile HelmReleases with cozystack.io/ui=true label
labelSelector := client.MatchingLabels{
"apps.cozystack.io/application.kind": applicationKind,
"apps.cozystack.io/application.group": applicationGroup,
"cozystack.io/ui": "true",
}
// List all HelmReleases with matching labels
hrList := &helmv2.HelmReleaseList{}
if err := r.List(ctx, hrList, labelSelector); err != nil {
logger.Error(err, "failed to list HelmReleases", "kind", applicationKind, "group", applicationGroup)
return err
}
logger.V(4).Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items))
// Update each HelmRelease
for i := range hrList.Items {
hr := &hrList.Items[i]
if err := r.updateHelmReleaseChart(ctx, hr, crd); err != nil {
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
continue
}
}
return nil
}
// expectedValuesFrom returns the expected valuesFrom configuration for HelmReleases
func expectedValuesFrom() []helmv2.ValuesReference {
return []helmv2.ValuesReference{
{
Kind: "Secret",
Name: "cozystack-values",
},
}
}
// valuesFromEqual compares two ValuesReference slices
func valuesFromEqual(a, b []helmv2.ValuesReference) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].Kind != b[i].Kind ||
a[i].Name != b[i].Name ||
a[i].ValuesKey != b[i].ValuesKey ||
a[i].TargetPath != b[i].TargetPath ||
a[i].Optional != b[i].Optional {
return false
}
}
return true
}
// updateHelmReleaseChart updates the chart and valuesFrom in HelmRelease based on CozystackResourceDefinition
func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error {
logger := log.FromContext(ctx)
hrCopy := hr.DeepCopy()
updated := false
// Validate Chart configuration exists
if crd.Spec.Release.Chart.Name == "" {
logger.V(4).Info("Skipping HelmRelease chart update: Chart.Name is empty", "crd", crd.Name)
return nil
}
// Validate SourceRef fields
if crd.Spec.Release.Chart.SourceRef.Kind == "" ||
crd.Spec.Release.Chart.SourceRef.Name == "" ||
crd.Spec.Release.Chart.SourceRef.Namespace == "" {
logger.Error(fmt.Errorf("invalid SourceRef in CRD"), "Skipping HelmRelease chart update: SourceRef fields are incomplete",
"crd", crd.Name,
"kind", crd.Spec.Release.Chart.SourceRef.Kind,
"name", crd.Spec.Release.Chart.SourceRef.Name,
"namespace", crd.Spec.Release.Chart.SourceRef.Namespace)
return nil
}
// Get version and reconcileStrategy from CRD or use defaults
version := ">= 0.0.0-0"
reconcileStrategy := "Revision"
// TODO: Add Version and ReconcileStrategy fields to CozystackResourceDefinitionChart if needed
// Build expected SourceRef
expectedSourceRef := helmv2.CrossNamespaceObjectReference{
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
Name: crd.Spec.Release.Chart.SourceRef.Name,
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
}
if hrCopy.Spec.Chart == nil {
// Need to create Chart spec
hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{
Spec: helmv2.HelmChartTemplateSpec{
Chart: crd.Spec.Release.Chart.Name,
Version: version,
ReconcileStrategy: reconcileStrategy,
SourceRef: expectedSourceRef,
},
}
updated = true
} else {
// Update existing Chart spec
if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name ||
hrCopy.Spec.Chart.Spec.SourceRef != expectedSourceRef {
hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name
hrCopy.Spec.Chart.Spec.SourceRef = expectedSourceRef
updated = true
}
}
// Check and update valuesFrom configuration
expected := expectedValuesFrom()
if !valuesFromEqual(hrCopy.Spec.ValuesFrom, expected) {
logger.V(4).Info("Updating HelmRelease valuesFrom", "name", hr.Name, "namespace", hr.Namespace)
hrCopy.Spec.ValuesFrom = expected
updated = true
}
if updated {
logger.V(4).Info("Updating HelmRelease chart", "name", hr.Name, "namespace", hr.Namespace)
if err := r.Update(ctx, hrCopy); err != nil {
return fmt.Errorf("failed to update HelmRelease: %w", err)
}
}
return nil
}

View File

@@ -14,7 +14,7 @@ import (
)
// ensureBreadcrumb creates or updates a Breadcrumb resource for the given CRD
func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
group, version, kind := pickGVK(crd)
lowerKind := strings.ToLower(kind)

View File

@@ -21,7 +21,7 @@ import (
//
// metadata.name: stock-namespace-<group>.<version>.<plural>
// spec.id: stock-namespace-/<group>/<version>/<plural>
func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) (controllerutil.OperationResult, error) {
func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (controllerutil.OperationResult, error) {
g, v, kind := pickGVK(crd)
plural := pickPlural(kind, crd)
// Details page segment uses lowercase kind, mirroring your example

View File

@@ -15,7 +15,7 @@ import (
)
// ensureCustomFormsOverride creates or updates a CustomFormsOverride resource for the given CRD
func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
g, v, kind := pickGVK(crd)
plural := pickPlural(kind, crd)
@@ -105,8 +105,26 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
"properties": map[string]any{},
}
// Check if there's a spec property
specProp, ok := props["spec"].(map[string]any)
if !ok {
return map[string]any{}, nil
}
specProps, ok := specProp["properties"].(map[string]any)
if !ok {
return map[string]any{}, nil
}
// Create spec.properties structure in schema
schemaProps := schema["properties"].(map[string]any)
specSchema := map[string]any{
"properties": map[string]any{},
}
schemaProps["spec"] = specSchema
// Process spec properties recursively
processSpecProperties(props, schema["properties"].(map[string]any))
processSpecProperties(specProps, specSchema["properties"].(map[string]any))
return schema, nil
}

View File

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

View File

@@ -16,7 +16,7 @@ import (
)
// ensureCustomFormsPrefill creates or updates a CustomFormsPrefill resource for the given CRD
func (m *Manager) ensureCustomFormsPrefill(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) (reconcile.Result, error) {
func (m *Manager) ensureCustomFormsPrefill(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (reconcile.Result, error) {
logger := log.FromContext(ctx)
app := crd.Spec.Application

View File

@@ -15,7 +15,7 @@ import (
)
// ensureFactory creates or updates a Factory resource for the given CRD
func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
g, v, kind := pickGVK(crd)
plural := pickPlural(kind, crd)
@@ -557,7 +557,7 @@ type factoryFlags struct {
// factoryFeatureFlags tries several conventional locations so you can evolve the API
// without breaking the controller. Defaults are false (hidden).
func factoryFeatureFlags(crd *cozyv1alpha1.ApplicationDefinition) factoryFlags {
func factoryFeatureFlags(crd *cozyv1alpha1.CozystackResourceDefinition) factoryFlags {
var f factoryFlags
f.Workloads = true

View File

@@ -23,7 +23,7 @@ type fieldInfo struct {
// pickGVK tries to read group/version/kind from the CRD. We prefer the "application" section,
// falling back to other likely fields if your schema differs.
func pickGVK(crd *cozyv1alpha1.ApplicationDefinition) (group, version, kind string) {
func pickGVK(crd *cozyv1alpha1.CozystackResourceDefinition) (group, version, kind string) {
// Best guess based on your examples:
if crd.Spec.Application.Kind != "" {
kind = crd.Spec.Application.Kind
@@ -41,7 +41,7 @@ func pickGVK(crd *cozyv1alpha1.ApplicationDefinition) (group, version, kind stri
}
// pickPlural prefers a field on the CRD if you have it; otherwise do a simple lowercase + "s".
func pickPlural(kind string, crd *cozyv1alpha1.ApplicationDefinition) string {
func pickPlural(kind string, crd *cozyv1alpha1.CozystackResourceDefinition) string {
// If you have crd.Spec.Application.Plural, prefer it. Example:
if crd.Spec.Application.Plural != "" {
return crd.Spec.Application.Plural

View File

@@ -56,7 +56,7 @@ func NewManager(c client.Client, scheme *runtime.Scheme) *Manager {
func (m *Manager) SetupWithManager(mgr ctrl.Manager) error {
if err := ctrl.NewControllerManagedBy(mgr).
Named("dashboard-reconciler").
For(&cozyv1alpha1.ApplicationDefinition{}).
For(&cozyv1alpha1.CozystackResourceDefinition{}).
Complete(m); err != nil {
return err
}
@@ -72,7 +72,7 @@ func (m *Manager) SetupWithManager(mgr ctrl.Manager) error {
func (m *Manager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
crd := &cozyv1alpha1.ApplicationDefinition{}
crd := &cozyv1alpha1.CozystackResourceDefinition{}
err := m.Get(ctx, types.NamespacedName{Name: req.Name}, crd)
if err != nil {
@@ -99,7 +99,7 @@ func (m *Manager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result,
// - ensureMarketplacePanel (implemented)
// - ensureSidebar (implemented)
// - ensureTableUriMapping (implemented)
func (m *Manager) EnsureForCRD(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) (reconcile.Result, error) {
func (m *Manager) EnsureForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (reconcile.Result, error) {
// Early return if crd.Spec.Dashboard is nil to prevent oscillation
if crd.Spec.Dashboard == nil {
return reconcile.Result{}, nil
@@ -148,7 +148,7 @@ func (m *Manager) InitializeStaticResources(ctx context.Context) error {
}
// addDashboardLabels adds standard dashboard management labels to a resource
func (m *Manager) addDashboardLabels(obj client.Object, crd *cozyv1alpha1.ApplicationDefinition, resourceType string) {
func (m *Manager) addDashboardLabels(obj client.Object, crd *cozyv1alpha1.CozystackResourceDefinition, resourceType string) {
labels := obj.GetLabels()
if labels == nil {
labels = make(map[string]string)
@@ -197,7 +197,7 @@ func (m *Manager) getStaticResourceSelector() client.MatchingLabels {
// CleanupOrphanedResources removes dashboard resources that are no longer needed
// This should be called after cache warming to ensure all current resources are known
func (m *Manager) CleanupOrphanedResources(ctx context.Context) error {
var crdList cozyv1alpha1.ApplicationDefinitionList
var crdList cozyv1alpha1.CozystackResourceDefinitionList
if err := m.List(ctx, &crdList, &client.ListOptions{}); err != nil {
return err
}
@@ -228,7 +228,7 @@ func (m *Manager) CleanupOrphanedResources(ctx context.Context) error {
}
// buildExpectedResourceSet creates a map of expected resource names by type
func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.ApplicationDefinition) map[string]map[string]bool {
func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.CozystackResourceDefinition) map[string]map[string]bool {
expected := make(map[string]map[string]bool)
// Initialize maps for each resource type

View File

@@ -16,7 +16,7 @@ import (
)
// ensureMarketplacePanel creates or updates a MarketplacePanel resource for the given CRD
func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) (reconcile.Result, error) {
func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (reconcile.Result, error) {
logger := log.FromContext(ctx)
mp := &dashv1alpha1.MarketplacePanel{}

View File

@@ -28,12 +28,12 @@ import (
// - Categories are ordered strictly as:
// Marketplace, IaaS, PaaS, NaaS, <others A→Z>, Resources, Administration
// - Items within each category: sort by Weight (desc), then Label (A→Z).
func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
// Build the full menu once.
// 1) Fetch all CRDs
var all []cozyv1alpha1.ApplicationDefinition
var crdList cozyv1alpha1.ApplicationDefinitionList
var all []cozyv1alpha1.CozystackResourceDefinition
var crdList cozyv1alpha1.CozystackResourceDefinitionList
if err := m.List(ctx, &crdList, &client.ListOptions{}); err != nil {
return err
}
@@ -228,7 +228,7 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
// upsertMultipleSidebars creates/updates several Sidebar resources with the same menu spec.
func (m *Manager) upsertMultipleSidebars(
ctx context.Context,
crd *cozyv1alpha1.ApplicationDefinition,
crd *cozyv1alpha1.CozystackResourceDefinition,
ids []string,
keysAndTags map[string]any,
menuItems []any,
@@ -335,7 +335,7 @@ func orderCategoryLabels[T any](cats map[string][]T) []string {
}
// safeCategory returns spec.dashboard.category or "Resources" if not set.
func safeCategory(def *cozyv1alpha1.ApplicationDefinition) string {
func safeCategory(def *cozyv1alpha1.CozystackResourceDefinition) string {
if def == nil || def.Spec.Dashboard == nil {
return "Resources"
}

View File

@@ -7,7 +7,7 @@ import (
)
// ensureTableUriMapping creates or updates a TableUriMapping resource for the given CRD
func (m *Manager) ensureTableUriMapping(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
func (m *Manager) ensureTableUriMapping(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
// Links are fully managed by the CustomColumnsOverride.
return nil
}

View File

@@ -1,170 +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 controller
import (
"context"
"encoding/json"
"fmt"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"github.com/cozystack/cozystack/pkg/cozylib"
)
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;update;patch
type NamespaceHelmReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// Reconcile processes namespace changes and updates HelmReleases with namespace labels
func (r *NamespaceHelmReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// Get the namespace
namespace := &corev1.Namespace{}
if err := r.Get(ctx, req.NamespacedName, namespace); err != nil {
logger.Error(err, "unable to fetch Namespace")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Extract namespace.cozystack.io/* annotations
namespaceLabels := cozylib.ExtractNamespaceAnnotations(namespace)
if len(namespaceLabels) == 0 {
// No namespace labels to process, skip
return ctrl.Result{}, nil
}
logger.Info("processing namespace labels", "namespace", namespace.Name, "labels", namespaceLabels)
// List all HelmReleases in this namespace
helmReleaseList := &helmv2.HelmReleaseList{}
if err := r.List(ctx, helmReleaseList, client.InNamespace(namespace.Name)); err != nil {
logger.Error(err, "unable to list HelmReleases in namespace", "namespace", namespace.Name)
return ctrl.Result{}, err
}
// Update each HelmRelease with namespace labels
updated := 0
for i := range helmReleaseList.Items {
hr := &helmReleaseList.Items[i]
if err := r.updateHelmReleaseWithNamespaceLabels(ctx, hr, namespaceLabels); err != nil {
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
continue
}
updated++
}
if updated > 0 {
logger.Info("updated HelmReleases with namespace labels", "namespace", namespace.Name, "count", updated)
}
return ctrl.Result{}, nil
}
// updateHelmReleaseWithNamespaceLabels updates HelmRelease values with namespace labels
func (r *NamespaceHelmReconciler) updateHelmReleaseWithNamespaceLabels(ctx context.Context, hr *helmv2.HelmRelease, namespaceLabels map[string]string) error {
logger := log.FromContext(ctx)
// Parse current values
var valuesMap map[string]interface{}
if hr.Spec.Values != nil && len(hr.Spec.Values.Raw) > 0 {
if err := json.Unmarshal(hr.Spec.Values.Raw, &valuesMap); err != nil {
return fmt.Errorf("failed to unmarshal HelmRelease values: %w", err)
}
} else {
valuesMap = make(map[string]interface{})
}
// Convert namespaceLabels from map[string]string to map[string]interface{}
namespaceLabelsMap := make(map[string]interface{})
for k, v := range namespaceLabels {
namespaceLabelsMap[k] = v
}
// Check if namespace labels need to be updated (top-level _namespace field)
needsUpdate := false
currentNamespace, exists := valuesMap["_namespace"]
if !exists {
needsUpdate = true
valuesMap["_namespace"] = namespaceLabelsMap
} else {
currentNamespaceMap, ok := currentNamespace.(map[string]interface{})
if !ok {
needsUpdate = true
valuesMap["_namespace"] = namespaceLabelsMap
} else {
// Compare and update if different
for k, v := range namespaceLabelsMap {
if currentVal, exists := currentNamespaceMap[k]; !exists || currentVal != v {
needsUpdate = true
currentNamespaceMap[k] = v
}
}
// Remove keys that are no longer in namespace labels
for k := range currentNamespaceMap {
if _, exists := namespaceLabelsMap[k]; !exists {
needsUpdate = true
delete(currentNamespaceMap, k)
}
}
if needsUpdate {
valuesMap["_namespace"] = currentNamespaceMap
}
}
}
if !needsUpdate {
// No changes needed
return nil
}
// Marshal back to JSON
mergedJSON, err := json.Marshal(valuesMap)
if err != nil {
return fmt.Errorf("failed to marshal values with namespace labels: %w", err)
}
// Update HelmRelease
patchTarget := hr.DeepCopy()
patchTarget.Spec.Values = &apiextensionsv1.JSON{Raw: mergedJSON}
patch := client.MergeFrom(hr)
if err := r.Patch(ctx, patchTarget, patch); err != nil {
return fmt.Errorf("failed to patch HelmRelease: %w", err)
}
logger.Info("updated HelmRelease with namespace labels", "name", hr.Name, "namespace", hr.Namespace)
return nil
}
// SetupWithManager sets up the controller with the Manager
func (r *NamespaceHelmReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Namespace{}).
Complete(r)
}

View File

@@ -1,352 +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"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
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)
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
// 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, decoder, 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, decoder, 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, decoder runtime.Decoder, 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.
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
}
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 || err != nil {
continue
}
// Update containers
containers, found, err := unstructured.NestedSlice(spec, "containers")
if found && err == nil {
containers = updateContainersEnv(containers, kubernetesHost, kubernetesPort)
if err := unstructured.SetNestedSlice(spec, containers, "containers"); err != nil {
continue
}
}
// Update initContainers
initContainers, found, err := unstructured.NestedSlice(spec, "initContainers")
if found && err == nil {
initContainers = updateContainersEnv(initContainers, kubernetesHost, kubernetesPort)
if err := unstructured.SetNestedSlice(spec, initContainers, "initContainers"); err != nil {
continue
}
}
// Update spec in the object
if err := unstructured.SetNestedMap(obj.Object, spec, "spec", "template", "spec"); err != nil {
continue
}
}
return nil
}
// 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
}

View File

@@ -4,28 +4,56 @@ import (
"fmt"
"strings"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
)
type appRef struct {
group string
kind string
}
type runtimeConfig struct {
appCRDMap map[appRef]*cozyv1alpha1.CozystackResourceDefinition
}
func (l *LineageControllerWebhook) initConfig() {
// No longer needed - we use labels directly from HelmRelease
l.initOnce.Do(func() {
if l.config.Load() == nil {
l.config.Store(&runtimeConfig{
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
})
}
})
}
// getApplicationLabel safely extracts an application label from HelmRelease
func getApplicationLabel(hr *helmv2.HelmRelease, key string) (string, error) {
if hr.Labels == nil {
return "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: labels are nil", hr.Namespace, hr.Name)
}
val, ok := hr.Labels[key]
if !ok {
return "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing %s label", hr.Namespace, hr.Name, key)
}
return val, nil
}
func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, string, error) {
// Extract application metadata from labels
appKind, ok := hr.Labels["apps.cozystack.io/application.kind"]
if !ok {
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing apps.cozystack.io/application.kind label", hr.Namespace, hr.Name)
appKind, err := getApplicationLabel(hr, "apps.cozystack.io/application.kind")
if err != nil {
return "", "", "", err
}
appGroup, ok := hr.Labels["apps.cozystack.io/application.group"]
if !ok {
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing apps.cozystack.io/application.group label", hr.Namespace, hr.Name)
appGroup, err := getApplicationLabel(hr, "apps.cozystack.io/application.group")
if err != nil {
return "", "", "", err
}
appName, ok := hr.Labels["apps.cozystack.io/application.name"]
if !ok {
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing apps.cozystack.io/application.name label", hr.Namespace, hr.Name)
appName, err := getApplicationLabel(hr, "apps.cozystack.io/application.name")
if err != nil {
return "", "", "", err
}
// Construct API version from group
@@ -35,5 +63,11 @@ func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string,
// HelmRelease name format: <prefix><application-name>
prefix := strings.TrimSuffix(hr.Name, appName)
// Validate the derived prefix
// This ensures correctness when appName appears multiple times in hr.Name
if prefix+appName != hr.Name {
return "", "", "", fmt.Errorf("cannot derive prefix from helm release %s/%s: name does not end with application name %s", hr.Namespace, hr.Name, appName)
}
return apiVersion, appKind, prefix, nil
}

View File

@@ -1,11 +1,44 @@
package lineagecontrollerwebhook
import (
"context"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// SetupWithManagerAsController is no longer needed since we don't watch ApplicationDefinitions
// +kubebuilder:rbac:groups=cozystack.io,resources=cozystackresourcedefinitions,verbs=list;watch;get
func (c *LineageControllerWebhook) SetupWithManagerAsController(mgr ctrl.Manager) error {
// No controller needed - we use labels directly from HelmRelease
return nil
return ctrl.NewControllerManagedBy(mgr).
For(&cozyv1alpha1.CozystackResourceDefinition{}).
Complete(c)
}
func (c *LineageControllerWebhook) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
crds := &cozyv1alpha1.CozystackResourceDefinitionList{}
if err := c.List(ctx, crds); err != nil {
l.Error(err, "failed reading CozystackResourceDefinitions")
return ctrl.Result{}, err
}
cfg := &runtimeConfig{
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
}
for _, crd := range crds.Items {
appRef := appRef{
"apps.cozystack.io",
crd.Spec.Application.Kind,
}
newRef := crd
if _, exists := cfg.appCRDMap[appRef]; exists {
l.Info("duplicate app mapping detected; ignoring subsequent entry", "key", appRef)
} else {
cfg.appCRDMap[appRef] = &newRef
}
}
c.config.Store(cfg)
return ctrl.Result{}, nil
}

View File

@@ -42,7 +42,7 @@ func matchName(ctx context.Context, name string, templateContext map[string]stri
return false
}
func matchResourceToSelector(ctx context.Context, name string, templateContext, l map[string]string, s *cozyv1alpha1.ApplicationDefinitionResourceSelector) bool {
func matchResourceToSelector(ctx context.Context, name string, templateContext, l map[string]string, s *cozyv1alpha1.CozystackResourceDefinitionResourceSelector) bool {
sel, err := metav1.LabelSelectorAsSelector(&s.LabelSelector)
if err != nil {
log.FromContext(ctx).Error(err, "failed to convert label selector to selector")
@@ -53,7 +53,7 @@ func matchResourceToSelector(ctx context.Context, name string, templateContext,
return labelMatches && nameMatches
}
func matchResourceToSelectorArray(ctx context.Context, name string, templateContext, l map[string]string, ss []*cozyv1alpha1.ApplicationDefinitionResourceSelector) bool {
func matchResourceToSelectorArray(ctx context.Context, name string, templateContext, l map[string]string, ss []*cozyv1alpha1.CozystackResourceDefinitionResourceSelector) bool {
for _, s := range ss {
if matchResourceToSelector(ctx, name, templateContext, l, s) {
return true
@@ -62,7 +62,7 @@ func matchResourceToSelectorArray(ctx context.Context, name string, templateCont
return false
}
func matchResourceToExcludeInclude(ctx context.Context, name string, templateContext, l map[string]string, resources *cozyv1alpha1.ApplicationDefinitionResources) bool {
func matchResourceToExcludeInclude(ctx context.Context, name string, templateContext, l map[string]string, resources *cozyv1alpha1.CozystackResourceDefinitionResources) bool {
if resources == nil {
return false
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/cozystack/cozystack/pkg/lineage"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -32,8 +33,8 @@ const (
ManagerNameKey = "apps.cozystack.io/application.name"
)
// getResourceSelectors returns the appropriate ApplicationDefinitionResources for a given GroupKind
func (h *LineageControllerWebhook) getResourceSelectors(gk schema.GroupKind, crd *cozyv1alpha1.ApplicationDefinition) *cozyv1alpha1.ApplicationDefinitionResources {
// getResourceSelectors returns the appropriate CozystackResourceDefinitionResources for a given GroupKind
func (h *LineageControllerWebhook) getResourceSelectors(gk schema.GroupKind, crd *cozyv1alpha1.CozystackResourceDefinition) *cozyv1alpha1.CozystackResourceDefinitionResources {
switch {
case gk.Group == "" && gk.Kind == "Secret":
return &crd.Spec.Secrets
@@ -87,16 +88,13 @@ func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Req
"name", req.Name,
"operation", req.Operation,
)
logger.Info("webhook called", "gvk", req.Kind.String(), "namespace", req.Namespace, "name", req.Name, "operation", req.Operation)
warn := make(admission.Warnings, 0)
obj := &unstructured.Unstructured{}
if err := h.decodeUnstructured(req, obj); err != nil {
logger.Error(err, "failed to decode object")
return admission.Errored(400, fmt.Errorf("decode object: %w", err))
}
logger.V(1).Info("decoded object", "labels", obj.GetLabels(), "ownerReferences", obj.GetOwnerReferences())
labels, err := h.computeLabels(ctx, obj)
for {
if err != nil && errors.Is(err, NoAncestors) {
@@ -119,10 +117,9 @@ func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Req
mutated, err := json.Marshal(obj)
if err != nil {
logger.Error(err, "failed to marshal mutated object")
return admission.Errored(500, fmt.Errorf("marshal mutated object: %w", err))
return admission.Errored(500, fmt.Errorf("marshal mutated pod: %w", err))
}
logger.Info("mutated object", "namespace", obj.GetNamespace(), "name", obj.GetName(), "labels", labels)
logger.V(1).Info("mutated pod", "namespace", obj.GetNamespace(), "name", obj.GetName())
return admission.PatchResponseFromRaw(req.Object.Raw, mutated).WithWarnings(warn...)
}
@@ -159,9 +156,21 @@ func (h *LineageControllerWebhook) computeLabels(ctx context.Context, o *unstruc
ManagerKindKey: obj.GetKind(),
ManagerNameKey: obj.GetName(),
}
// Resource selectors are no longer needed since we don't use ApplicationDefinitions
// Set tenant resource label to false by default (can be overridden by other logic if needed)
labels[corev1alpha1.TenantResourceLabelKey] = "false"
templateLabels := map[string]string{
"kind": strings.ToLower(obj.GetKind()),
"name": obj.GetName(),
"namespace": o.GetNamespace(),
}
cfg := h.config.Load().(*runtimeConfig)
crd := cfg.appCRDMap[appRef{gv.Group, obj.GetKind()}]
resourceSelectors := h.getResourceSelectors(o.GroupVersionKind().GroupKind(), crd)
labels[corev1alpha1.TenantResourceLabelKey] = func(b bool) string {
if b {
return corev1alpha1.TenantResourceLabelValue
}
return "false"
}(matchResourceToExcludeInclude(ctx, o.GetName(), templateLabels, o.GetLabels(), resourceSelectors))
return labels, err
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,541 +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"
"encoding/json"
"fmt"
"strings"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
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/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// PlatformReconciler reconciles Platform resources
type PlatformReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=cozystack.io,resources=platforms,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cozystack.io,resources=platforms/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;create;update;patch;delete
// +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 *PlatformReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
platform := &cozyv1alpha1.Platform{}
if err := r.Get(ctx, req.NamespacedName, platform); err != nil {
if apierrors.IsNotFound(err) {
// Cleanup orphaned resources
return r.cleanupOrphanedResources(ctx, req.NamespacedName)
}
return ctrl.Result{}, err
}
// Set defaults
if platform.Spec.Interval == nil {
platform.Spec.Interval = &metav1.Duration{Duration: 5 * 60 * 1000000000} // 5m
}
// Reconcile ArtifactGenerator
if err := r.reconcileArtifactGenerator(ctx, platform); err != nil {
logger.Error(err, "failed to reconcile ArtifactGenerator")
return ctrl.Result{}, err
}
// Reconcile HelmRelease
if err := r.reconcileHelmRelease(ctx, platform); err != nil {
logger.Error(err, "failed to reconcile HelmRelease")
return ctrl.Result{}, err
}
// Cleanup orphaned resources with platform label
if err := r.cleanupOrphanedPlatformResources(ctx, platform); err != nil {
logger.Error(err, "failed to cleanup orphaned platform resources")
// Don't return error, just log it - cleanup is best effort
}
return ctrl.Result{}, nil
}
// reconcileArtifactGenerator creates or updates the ArtifactGenerator for the platform
func (r *PlatformReconciler) reconcileArtifactGenerator(ctx context.Context, platform *cozyv1alpha1.Platform) error {
logger := log.FromContext(ctx)
// Use fixed namespace for cluster-scoped resource
namespace := "cozy-system"
// Get basePath with default values (already includes full path to platform)
basePath := r.getBasePath(platform)
// Build full path from basePath (basePath already contains the full path)
fullPath := r.buildSourcePath(platform.Spec.SourceRef.Name, basePath, "")
// Extract the last component for the artifact name
artifactPathParts := strings.Split(strings.Trim(basePath, "/"), "/")
artifactName := artifactPathParts[len(artifactPathParts)-1]
copyOps := []sourcewatcherv1beta1.CopyOperation{
{
From: fullPath + "/**",
To: fmt.Sprintf("@artifact/%s/", artifactName),
},
}
// Create ArtifactGenerator
ag := &sourcewatcherv1beta1.ArtifactGenerator{
ObjectMeta: metav1.ObjectMeta{
Name: platform.Name,
Namespace: namespace,
Labels: map[string]string{
"cozystack.io/platform": platform.Name,
},
},
Spec: sourcewatcherv1beta1.ArtifactGeneratorSpec{
Sources: []sourcewatcherv1beta1.SourceReference{
{
Alias: platform.Spec.SourceRef.Name,
Kind: platform.Spec.SourceRef.Kind,
Name: platform.Spec.SourceRef.Name,
Namespace: platform.Spec.SourceRef.Namespace,
},
},
OutputArtifacts: []sourcewatcherv1beta1.OutputArtifact{
{
Name: artifactName,
Copy: copyOps,
},
},
},
}
// Set ownerReference
ag.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: platform.APIVersion,
Kind: platform.Kind,
Name: platform.Name,
UID: platform.UID,
Controller: func() *bool { b := true; return &b }(),
},
}
logger.Info("reconciling ArtifactGenerator", "name", platform.Name, "namespace", namespace)
if err := r.createOrUpdate(ctx, ag); err != nil {
return fmt.Errorf("failed to reconcile ArtifactGenerator %s: %w", platform.Name, err)
}
logger.Info("reconciled ArtifactGenerator", "name", platform.Name, "namespace", namespace)
return nil
}
// reconcileHelmRelease creates or updates the HelmRelease for the platform
func (r *PlatformReconciler) reconcileHelmRelease(ctx context.Context, platform *cozyv1alpha1.Platform) error {
logger := log.FromContext(ctx)
// HelmRelease name is fixed: cozystack-platform
// Use fixed namespace for cluster-scoped resource
namespace := "cozy-system"
// Get artifact name (last component of basePath)
basePath := r.getBasePath(platform)
artifactPathParts := strings.Split(strings.Trim(basePath, "/"), "/")
artifactName := artifactPathParts[len(artifactPathParts)-1]
// Merge values with sourceRef
values := r.mergeValuesWithSourceRef(platform.Spec.Values, platform.Spec.SourceRef)
// Create HelmRelease
hr := &helmv2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: platform.Name,
Namespace: namespace,
Labels: map[string]string{
"cozystack.io/platform": platform.Name,
},
},
Spec: helmv2.HelmReleaseSpec{
Interval: *platform.Spec.Interval,
TargetNamespace: "cozy-system",
ReleaseName: "cozystack-platform",
ChartRef: &helmv2.CrossNamespaceSourceReference{
Kind: "ExternalArtifact",
Name: artifactName,
Namespace: namespace,
},
Values: values,
Install: &helmv2.Install{
Remediation: &helmv2.InstallRemediation{
Retries: -1,
},
},
Upgrade: &helmv2.Upgrade{
Remediation: &helmv2.UpgradeRemediation{
Retries: -1,
},
},
},
}
// Set ownerReference
hr.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: platform.APIVersion,
Kind: platform.Kind,
Name: platform.Name,
UID: platform.UID,
Controller: func() *bool { b := true; return &b }(),
},
}
logger.Info("reconciling HelmRelease", "name", platform.Name, "namespace", namespace)
if err := r.createOrUpdate(ctx, hr); err != nil {
return fmt.Errorf("failed to reconcile HelmRelease %s: %w", platform.Name, err)
}
logger.Info("reconciled HelmRelease", "name", platform.Name, "namespace", namespace)
return nil
}
// mergeValuesWithSourceRef merges platform values with sourceRef
func (r *PlatformReconciler) mergeValuesWithSourceRef(values *apiextensionsv1.JSON, sourceRef cozyv1alpha1.SourceRef) *apiextensionsv1.JSON {
// Build sourceRef map
sourceRefMap := map[string]interface{}{
"kind": sourceRef.Kind,
"name": sourceRef.Name,
"namespace": sourceRef.Namespace,
}
// If values is nil or empty, create new values with sourceRef
if values == nil || len(values.Raw) == 0 {
valuesMap := map[string]interface{}{
"sourceRef": sourceRefMap,
}
raw, _ := json.Marshal(valuesMap)
return &apiextensionsv1.JSON{Raw: raw}
}
// Parse existing values
var valuesMap map[string]interface{}
if err := json.Unmarshal(values.Raw, &valuesMap); err != nil {
// If unmarshal fails, create new values with sourceRef
valuesMap = map[string]interface{}{
"sourceRef": sourceRefMap,
}
raw, _ := json.Marshal(valuesMap)
return &apiextensionsv1.JSON{Raw: raw}
}
// Merge sourceRef into values (overwrite if exists)
valuesMap["sourceRef"] = sourceRefMap
// Marshal back to JSON
raw, err := json.Marshal(valuesMap)
if err != nil {
// If marshal fails, return original values
return values
}
return &apiextensionsv1.JSON{Raw: raw}
}
// getBasePath returns the basePath with default values based on source kind
func (r *PlatformReconciler) getBasePath(platform *cozyv1alpha1.Platform) string {
if platform.Spec.BasePath != "" {
return platform.Spec.BasePath
}
// Default values based on kind
if platform.Spec.SourceRef.Kind == "OCIRepository" {
return "core/platform" // Full path for OCI
}
// Default for GitRepository
return "packages/core/platform" // Full path for Git
}
// buildSourcePath builds the full source path from basePath and chart path
func (r *PlatformReconciler) buildSourcePath(sourceName, basePath, chartPath string) string {
// Remove leading/trailing slashes and combine
parts := []string{}
if basePath != "" {
parts = append(parts, strings.Trim(basePath, "/"))
}
if chartPath != "" {
parts = append(parts, strings.Trim(chartPath, "/"))
}
fullPath := strings.Join(parts, "/")
if fullPath == "" {
return fmt.Sprintf("@%s", sourceName)
}
return fmt.Sprintf("@%s/%s", sourceName, fullPath)
}
// cleanupOrphanedResources removes ArtifactGenerator and HelmRelease when Platform is deleted
func (r *PlatformReconciler) cleanupOrphanedResources(ctx context.Context, name client.ObjectKey) (ctrl.Result, error) {
logger := log.FromContext(ctx)
namespace := "cozy-system"
// Cleanup HelmReleases with the platform label that don't match
hrList := &helmv2.HelmReleaseList{}
if err := r.List(ctx, hrList, client.InNamespace(namespace), client.MatchingLabels{
"cozystack.io/platform": name.Name,
}); err != nil {
logger.Error(err, "failed to list HelmReleases for cleanup")
return ctrl.Result{}, err
}
for i := range hrList.Items {
hr := &hrList.Items[i]
// Check if this HelmRelease should exist (matches current Platform name)
// Since Platform is being deleted, all matching HelmReleases should be deleted
// OwnerReferences should handle this, but we'll also delete explicitly
if err := r.Delete(ctx, hr); err != nil {
if !apierrors.IsNotFound(err) {
logger.Error(err, "failed to delete orphaned HelmRelease", "name", hr.Name)
}
} else {
logger.Info("deleted orphaned HelmRelease", "name", hr.Name)
}
}
// Cleanup ArtifactGenerators with the platform label
agList := &sourcewatcherv1beta1.ArtifactGeneratorList{}
if err := r.List(ctx, agList, client.InNamespace(namespace), client.MatchingLabels{
"cozystack.io/platform": name.Name,
}); err != nil {
logger.Error(err, "failed to list ArtifactGenerators for cleanup")
return ctrl.Result{}, err
}
for i := range agList.Items {
ag := &agList.Items[i]
if err := r.Delete(ctx, ag); err != nil {
if !apierrors.IsNotFound(err) {
logger.Error(err, "failed to delete orphaned ArtifactGenerator", "name", ag.Name)
}
} else {
logger.Info("deleted orphaned ArtifactGenerator", "name", ag.Name)
}
}
return ctrl.Result{}, nil
}
// cleanupOrphanedPlatformResources removes HelmRelease and ArtifactGenerator resources
// that have the platform label but don't match the current Platform
func (r *PlatformReconciler) cleanupOrphanedPlatformResources(ctx context.Context, platform *cozyv1alpha1.Platform) error {
logger := log.FromContext(ctx)
namespace := "cozy-system"
platformName := platform.Name
// Cleanup orphaned HelmReleases
hrList := &helmv2.HelmReleaseList{}
if err := r.List(ctx, hrList, client.InNamespace(namespace), client.MatchingLabels{
"cozystack.io/platform": platformName,
}); err != nil {
return fmt.Errorf("failed to list HelmReleases: %w", err)
}
for i := range hrList.Items {
hr := &hrList.Items[i]
// Only delete if it doesn't match the current Platform name
// (in case Platform name changed)
if hr.Name != platformName {
logger.Info("deleting orphaned HelmRelease", "name", hr.Name, "expected", platformName)
if err := r.Delete(ctx, hr); err != nil {
if !apierrors.IsNotFound(err) {
logger.Error(err, "failed to delete orphaned HelmRelease", "name", hr.Name)
// Continue with other resources
}
}
}
}
// Cleanup orphaned ArtifactGenerators
agList := &sourcewatcherv1beta1.ArtifactGeneratorList{}
if err := r.List(ctx, agList, client.InNamespace(namespace), client.MatchingLabels{
"cozystack.io/platform": platformName,
}); err != nil {
return fmt.Errorf("failed to list ArtifactGenerators: %w", err)
}
for i := range agList.Items {
ag := &agList.Items[i]
// Only delete if it doesn't match the current Platform name
if ag.Name != platformName {
logger.Info("deleting orphaned ArtifactGenerator", "name", ag.Name, "expected", platformName)
if err := r.Delete(ctx, ag); err != nil {
if !apierrors.IsNotFound(err) {
logger.Error(err, "failed to delete orphaned ArtifactGenerator", "name", ag.Name)
// Continue with other resources
}
}
}
}
return nil
}
// createOrUpdate creates or updates a resource
func (r *PlatformReconciler) createOrUpdate(ctx context.Context, obj client.Object) error {
existing := obj.DeepCopyObject().(client.Object)
key := client.ObjectKeyFromObject(obj)
err := r.Get(ctx, key, existing)
if apierrors.IsNotFound(err) {
return r.Create(ctx, obj)
} else if err != nil {
return err
}
// Preserve resource version
obj.SetResourceVersion(existing.GetResourceVersion())
// Merge labels and annotations
labels := obj.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
for k, v := range existing.GetLabels() {
if _, ok := labels[k]; !ok {
labels[k] = v
}
}
obj.SetLabels(labels)
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
for k, v := range existing.GetAnnotations() {
if _, ok := annotations[k]; !ok {
annotations[k] = v
}
}
obj.SetAnnotations(annotations)
// For ArtifactGenerator, explicitly update Spec and ownerReferences
if ag, ok := obj.(*sourcewatcherv1beta1.ArtifactGenerator); ok {
if existingAG, ok := existing.(*sourcewatcherv1beta1.ArtifactGenerator); ok {
logger := log.FromContext(ctx)
logger.V(1).Info("updating ArtifactGenerator Spec", "name", ag.Name, "namespace", ag.Namespace)
existingAG.Spec = ag.Spec
existingAG.SetLabels(ag.GetLabels())
existingAG.SetAnnotations(ag.GetAnnotations())
// Always use ownerReferences from the new object (set in reconcileArtifactGenerator)
existingAG.SetOwnerReferences(ag.GetOwnerReferences())
obj = existingAG
}
}
// For HelmRelease, explicitly update Spec and ownerReferences
if hr, ok := obj.(*helmv2.HelmRelease); ok {
if existingHR, ok := existing.(*helmv2.HelmRelease); ok {
logger := log.FromContext(ctx)
logger.V(1).Info("updating HelmRelease Spec", "name", hr.Name, "namespace", hr.Namespace)
existingHR.Spec = hr.Spec
existingHR.SetLabels(hr.GetLabels())
existingHR.SetAnnotations(hr.GetAnnotations())
// Always use ownerReferences from the new object (set in reconcileHelmRelease)
existingHR.SetOwnerReferences(hr.GetOwnerReferences())
obj = existingHR
}
}
return r.Update(ctx, obj)
}
// SetupWithManager sets up the controller with the Manager
func (r *PlatformReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Named("cozystack-platform").
For(&cozyv1alpha1.Platform{}).
Watches(
&helmv2.HelmRelease{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
hr, ok := obj.(*helmv2.HelmRelease)
if !ok {
return nil
}
// Only watch HelmReleases with cozystack.io/platform label
platformName := hr.Labels["cozystack.io/platform"]
if platformName == "" {
return nil
}
return []reconcile.Request{
{
NamespacedName: client.ObjectKey{
Name: platformName,
// Cluster-scoped resource has no namespace
},
},
}
}),
builder.WithPredicates(
predicate.NewPredicateFuncs(func(obj client.Object) bool {
// Only watch resources with cozystack.io/platform label
labels := obj.GetLabels()
return labels != nil && labels["cozystack.io/platform"] != ""
}),
),
).
Watches(
&sourcewatcherv1beta1.ArtifactGenerator{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
ag, ok := obj.(*sourcewatcherv1beta1.ArtifactGenerator)
if !ok {
return nil
}
// Only watch ArtifactGenerators with cozystack.io/platform label
platformName := ag.Labels["cozystack.io/platform"]
if platformName == "" {
return nil
}
return []reconcile.Request{
{
NamespacedName: client.ObjectKey{
Name: platformName,
// Cluster-scoped resource has no namespace
},
},
}
}),
builder.WithPredicates(
predicate.NewPredicateFuncs(func(obj client.Object) bool {
// Only watch resources with cozystack.io/platform label
labels := obj.GetLabels()
return labels != nil && labels["cozystack.io/platform"] != ""
}),
),
).
Complete(r)
}

View File

@@ -11,13 +11,13 @@ import (
type Memory struct {
mu sync.RWMutex
data map[string]cozyv1alpha1.ApplicationDefinition
data map[string]cozyv1alpha1.CozystackResourceDefinition
primed bool
primeOnce sync.Once
}
func New() *Memory {
return &Memory{data: make(map[string]cozyv1alpha1.ApplicationDefinition)}
return &Memory{data: make(map[string]cozyv1alpha1.CozystackResourceDefinition)}
}
var (
@@ -30,7 +30,7 @@ func Global() *Memory {
return global
}
func (m *Memory) Upsert(obj *cozyv1alpha1.ApplicationDefinition) {
func (m *Memory) Upsert(obj *cozyv1alpha1.CozystackResourceDefinition) {
if obj == nil {
return
}
@@ -45,10 +45,10 @@ func (m *Memory) Delete(name string) {
m.mu.Unlock()
}
func (m *Memory) Snapshot() []cozyv1alpha1.ApplicationDefinition {
func (m *Memory) Snapshot() []cozyv1alpha1.CozystackResourceDefinition {
m.mu.RLock()
defer m.mu.RUnlock()
out := make([]cozyv1alpha1.ApplicationDefinition, 0, len(m.data))
out := make([]cozyv1alpha1.CozystackResourceDefinition, 0, len(m.data))
for _, v := range m.data {
out = append(out, v)
}
@@ -72,7 +72,7 @@ func (m *Memory) EnsurePrimingWithManager(mgr ctrl.Manager) error {
if ok := mgr.GetCache().WaitForCacheSync(ctx); !ok {
return nil
}
var list cozyv1alpha1.ApplicationDefinitionList
var list cozyv1alpha1.CozystackResourceDefinitionList
if err := mgr.GetClient().List(ctx, &list); err == nil {
for i := range list.Items {
m.Upsert(&list.Items[i])
@@ -87,11 +87,11 @@ func (m *Memory) EnsurePrimingWithManager(mgr ctrl.Manager) error {
return errOut
}
func (m *Memory) ListFromCacheOrAPI(ctx context.Context, c client.Client) ([]cozyv1alpha1.ApplicationDefinition, error) {
func (m *Memory) ListFromCacheOrAPI(ctx context.Context, c client.Client) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
if m.IsPrimed() {
return m.Snapshot(), nil
}
var list cozyv1alpha1.ApplicationDefinitionList
var list cozyv1alpha1.CozystackResourceDefinitionList
if err := c.List(ctx, &list); err != nil {
return nil, err
}

View File

@@ -5,7 +5,6 @@ import (
"context"
"fmt"
"net/http"
"sort"
"strings"
"time"
@@ -121,51 +120,17 @@ func (c *Collector) collect(ctx context.Context) {
clusterID := string(kubeSystemNS.UID)
// Get all Bundles
var bundleList cozyv1alpha1.BundleList
bundleNameStr := ""
bundleEnable := ""
bundleDisable := ""
oidcEnabled := "false"
if err := c.client.List(ctx, &bundleList); err != nil {
logger.Info(fmt.Sprintf("Failed to list Bundles: %v", err))
// Continue with empty bundle data instead of returning
} else {
// Collect bundle names (sorted alphabetically)
bundleNames := make([]string, 0, len(bundleList.Items))
for _, bundle := range bundleList.Items {
bundleNames = append(bundleNames, bundle.Name)
}
sort.Strings(bundleNames)
bundleNameStr = strings.Join(bundleNames, ",")
// Collect all packages from all bundles
var allEnabledPackages []string
var allDisabledPackages []string
for _, bundle := range bundleList.Items {
for _, pkg := range bundle.Spec.Packages {
if pkg.Disabled {
allDisabledPackages = append(allDisabledPackages, pkg.Name)
} else {
allEnabledPackages = append(allEnabledPackages, pkg.Name)
// Check if keycloak package is enabled
if pkg.Name == "keycloak" {
oidcEnabled = "true"
}
}
}
}
// Sort package lists alphabetically
sort.Strings(allEnabledPackages)
sort.Strings(allDisabledPackages)
bundleEnable = strings.Join(allEnabledPackages, ",")
bundleDisable = strings.Join(allDisabledPackages, ",")
var cozystackCM corev1.ConfigMap
if err := c.client.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack"}, &cozystackCM); err != nil {
logger.Info(fmt.Sprintf("Failed to get cozystack configmap in cozy-system namespace: %v", err))
return
}
oidcEnabled := cozystackCM.Data["oidc-enabled"]
bundle := cozystackCM.Data["bundle-name"]
bundleEnable := cozystackCM.Data["bundle-enable"]
bundleDisable := cozystackCM.Data["bundle-disable"]
// Get Kubernetes version from nodes
var nodeList corev1.NodeList
if err := c.client.List(ctx, &nodeList); err != nil {
@@ -178,41 +143,32 @@ func (c *Collector) collect(ctx context.Context) {
// Add Cozystack info metric
if len(nodeList.Items) > 0 {
k8sVersion := "unknown"
if version, err := c.discoveryClient.ServerVersion(); err == nil && version != nil {
k8sVersion = version.String()
}
k8sVersion, _ := c.discoveryClient.ServerVersion()
metrics.WriteString(fmt.Sprintf(
"cozy_cluster_info{cozystack_version=\"%s\",kubernetes_version=\"%s\",oidc_enabled=\"%s\",bundle_name=\"%s\",bundle_enable=\"%s\",bundle_disable=\"%s\"} 1\n",
"cozy_cluster_info{cozystack_version=\"%s\",kubernetes_version=\"%s\",oidc_enabled=\"%s\",bundle_name=\"%s\",bunde_enable=\"%s\",bunde_disable=\"%s\"} 1\n",
c.config.CozystackVersion,
k8sVersion,
oidcEnabled,
bundleNameStr,
bundle,
bundleEnable,
bundleDisable,
))
}
// Collect node metrics
if len(nodeList.Items) > 0 {
nodeOSCount := make(map[string]int)
kernelVersion := "unknown"
for _, node := range nodeList.Items {
key := fmt.Sprintf("%s (%s)", node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.OSImage)
nodeOSCount[key] = nodeOSCount[key] + 1
if kernelVersion == "unknown" && node.Status.NodeInfo.KernelVersion != "" {
kernelVersion = node.Status.NodeInfo.KernelVersion
}
}
for osKey, count := range nodeOSCount {
metrics.WriteString(fmt.Sprintf(
"cozy_nodes_count{os=\"%s\",kernel=\"%s\"} %d\n",
osKey,
kernelVersion,
nodeList.Items[0].Status.NodeInfo.KernelVersion,
count,
))
}
}
// Collect LoadBalancer services metrics
@@ -292,8 +248,9 @@ func (c *Collector) collect(ctx context.Context) {
var monitorList cozyv1alpha1.WorkloadMonitorList
if err := c.client.List(ctx, &monitorList); err != nil {
logger.Info(fmt.Sprintf("Failed to list WorkloadMonitors: %v", err))
// Continue without workload metrics instead of returning
} else {
return
}
for _, monitor := range monitorList.Items {
metrics.WriteString(fmt.Sprintf(
"cozy_workloads_count{uid=\"%s\",kind=\"%s\",type=\"%s\",version=\"%s\"} %d\n",
@@ -303,7 +260,6 @@ func (c *Collector) collect(ctx context.Context) {
monitor.Spec.Version,
monitor.Status.ObservedReplicas,
))
}
}
// Send metrics

View File

@@ -1,7 +1,12 @@
OUT=../../_out/repos/apps
CHARTS := $(shell find . -maxdepth 2 -name Chart.yaml | awk -F/ '{print $$2}')
include ../../hack/common-envs.mk
include ../../scripts/common-envs.mk
repo:
rm -rf "$(OUT)"
helm package -d "$(OUT)" $(CHARTS) --version $(COZYSTACK_VERSION)
helm repo index "$(OUT)"
fix-charts:
find . -maxdepth 2 -name Chart.yaml | awk -F/ '{print $$2}' | while read i; do sed -i -e "s/^name: .*/name: $$i/" -e "s/^version: .*/version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process/g" "$$i/Chart.yaml"; done

8
packages/apps/README.md Normal file
View File

@@ -0,0 +1,8 @@
### How to test packages local
```bash
cd packages/core/installer
make image-cozystack REGISTRY=YOUR_CUSTOM_REGISTRY
make apply
kubectl delete po -l app=source-controller -n cozy-fluxcd
```

View File

@@ -1,4 +1,4 @@
include ../../../hack/package.mk
include ../../../scripts/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md

View File

@@ -1,6 +1,4 @@
{{- $cozystack := .Values._cozystack | default dict }}
{{- $namespace := .Values._namespace | default dict }}
{{- $seaweedfs := dig "seaweedfs" "" $namespace }}
{{- $seaweedfs := .Values._namespace.seaweedfs }}
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketClaim
metadata:

View File

@@ -3,10 +3,15 @@ kind: HelmRelease
metadata:
name: {{ .Release.Name }}-system
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-bucket
namespace: cozy-system
chart:
spec:
chart: cozy-bucket
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
interval: 5m
timeout: 10m
install:
@@ -16,5 +21,8 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
values:
bucketName: {{ .Release.Name }}

View File

@@ -1,7 +1,7 @@
CLICKHOUSE_BACKUP_TAG = $(shell awk '$$0 ~ /^version:/ {print $$2}' Chart.yaml)
include ../../../hack/common-envs.mk
include ../../../hack/package.mk
include ../../../scripts/common-envs.mk
include ../../../scripts/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md

View File

@@ -1,5 +1,4 @@
{{- $cozystack := .Values._cozystack | default dict }}
{{- $clusterDomain := dig "networking" "clusterDomain" "cozy.local" $cozystack }}
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
{{- if .Values.clickhouseKeeper.enabled }}
apiVersion: "clickhouse-keeper.altinity.com/v1"

View File

@@ -1,5 +1,4 @@
{{- $cozystack := .Values._cozystack | default dict }}
{{- $clusterDomain := dig "networking" "clusterDomain" "cozy.local" $cozystack }}
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }}
{{- $passwords := dict }}
{{- $users := .Values.users }}

View File

@@ -1,4 +1,4 @@
include ../../../hack/package.mk
include ../../../scripts/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md

View File

@@ -50,13 +50,14 @@ spec:
postgresUID: 999
postgresGID: 999
enableSuperuserAccess: true
{{- $cozystack := .Values._cozystack | default dict }}
{{- $topologySpreadConstraints := dig "scheduling" "topologySpreadConstraints" (list) $cozystack }}
{{- if $topologySpreadConstraints }}
topologySpreadConstraints:
{{- range $topologySpreadConstraints }}
- {{- mergeOverwrite (dict "labelSelector" (dict "matchLabels" (dict "cnpg.io/cluster" (printf "%s-postgres" $.Release.Name)))) . | toYaml | nindent 6 | trim }}
{{- end }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- if $rawConstraints }}
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
labelSelector:
matchLabels:
cnpg.io/cluster: {{ .Release.Name }}-postgres
{{- end }}
{{- end }}
minSyncReplicas: {{ .Values.quorum.minSyncReplicas }}
maxSyncReplicas: {{ .Values.quorum.maxSyncReplicas }}

View File

@@ -1,4 +1,4 @@
include ../../../hack/package.mk
include ../../../scripts/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md

View File

@@ -1,5 +1,4 @@
{{- $cozystack := .Values._cozystack | default dict }}
{{- $clusterDomain := dig "networking" "clusterDomain" "cozy.local" $cozystack }}
{{- $clusterDomain := index .Values._cluster "cluster-domain" | default "cozy.local" }}
---
apiVersion: apps.foundationdb.org/v1beta2
kind: FoundationDBCluster

View File

@@ -1,7 +1,7 @@
NGINX_CACHE_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml)
include ../../../hack/common-envs.mk
include ../../../hack/package.mk
include ../../../scripts/common-envs.mk
include ../../../scripts/package.mk
image: image-nginx

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:31ebc09cfa11d8b438d2bbb32fa61b133aaf4b48b1a1282c9e59b5c127af61c1

View File

@@ -1,4 +1,4 @@
include ../../../hack/package.mk
include ../../../scripts/package.mk
PRESET_ENUM := ["nano","micro","small","medium","large","xlarge","2xlarge"]
generate:

View File

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

View File

@@ -1,14 +1,17 @@
KUBERNETES_VERSION = v1.33
KUBERNETES_PKG_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml)
include ../../../hack/common-envs.mk
include ../../../hack/package.mk
include ../../../scripts/common-envs.mk
include ../../../scripts/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
yq -o=json -i '.properties.version.enum = (load("files/versions.yaml") | keys)' values.schema.json
../../../hack/update-crd.sh
update:
hack/update-versions.sh
make generate
image: image-ubuntu-container-disk image-kubevirt-cloud-provider image-kubevirt-csi-driver image-cluster-autoscaler
image-ubuntu-container-disk:

View File

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

View File

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

View File

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

View File

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

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

View File

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

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:d25e567bc8b17b596e050f5ff410e36112c7966e33f4b372c752e7350bacc894

View File

@@ -1,8 +1,6 @@
{{- $cozystack := .Values._cozystack | default dict }}
{{- $namespace := .Values._namespace | default dict }}
{{- $etcd := dig "etcd" "" $namespace }}
{{- $ingress := dig "ingress" "" $namespace }}
{{- $host := dig "host" "" $namespace }}
{{- $etcd := .Values._namespace.etcd }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- $kubevirtmachinetemplateNames := list }}
{{- define "kubevirtmachinetemplate" -}}
spec:
@@ -32,11 +30,13 @@ spec:
{{- end }}
cluster.x-k8s.io/deployment-name: {{ $.Release.Name }}-{{ .groupName }}
spec:
{{- $cozystack := $.Values._cozystack | default dict }}
{{- $topologySpreadConstraints := dig "scheduling" "topologySpreadConstraints" (list) $cozystack }}
{{- if $topologySpreadConstraints }}
{{- range $topologySpreadConstraints }}
- {{- mergeOverwrite (dict "labelSelector" (dict "matchLabels" (dict "cluster.x-k8s.io/cluster-name" $.Release.Name))) . | toYaml | nindent 12 | trim }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- if $rawConstraints }}
{{- $rawConstraints | fromYaml | toYaml | nindent 10 }}
labelSelector:
matchLabels:
cluster.x-k8s.io/cluster-name: {{ $.Release.Name }}
{{- end }}
{{- end }}
domain:

View File

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

View File

@@ -8,10 +8,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: cert-manager-crds
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-cert-manager-crds
namespace: cozy-system
chart:
spec:
chart: cozy-cert-manager-crds
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig
@@ -40,8 +45,6 @@ spec:
- name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
{{- end }}
- name: {{ .Release.Name }}-cilium
namespace: {{ .Release.Namespace }}
{{- if .Values.addons.certManager.valuesOverride }}
---
apiVersion: v1

View File

@@ -8,10 +8,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: cert-manager
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-cert-manager
namespace: cozy-system
chart:
spec:
chart: cozy-cert-manager
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig

View File

@@ -22,10 +22,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: cilium
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-cilium
namespace: cozy-system
chart:
spec:
chart: cozy-cilium
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig

View File

@@ -13,10 +13,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: coredns
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-coredns
namespace: cozy-system
chart:
spec:
chart: cozy-coredns
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig

View File

@@ -8,10 +8,15 @@ metadata:
spec:
interval: 5m
releaseName: csi
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-kubevirt-csi-node
namespace: cozy-system
chart:
spec:
chart: cozy-kubevirt-csi-node
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig
@@ -32,6 +37,8 @@ spec:
dependsOn:
- name: {{ .Release.Name }}-vsnap-crd
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-cilium
namespace: {{ .Release.Namespace }}
{{- if lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" .Release.Namespace .Release.Name }}
- name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}

View File

@@ -8,10 +8,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: fluxcd-operator
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-fluxcd-operator
namespace: cozy-system
chart:
spec:
chart: cozy-fluxcd-operator
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig
@@ -51,10 +56,15 @@ metadata:
spec:
interval: 5m
releaseName: fluxcd
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-fluxcd
namespace: cozy-system
chart:
spec:
chart: cozy-fluxcd
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-kubeconfig

View File

@@ -8,10 +8,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: gateway-api-crds
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-gateway-api-crds
namespace: cozy-system
chart:
spec:
chart: cozy-gateway-api-crds
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig

View File

@@ -8,10 +8,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: gpu-operator
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-gpu-operator
namespace: cozy-system
chart:
spec:
chart: cozy-gpu-operator
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig

View File

@@ -27,10 +27,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: ingress-nginx
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-ingress-nginx
namespace: cozy-system
chart:
spec:
chart: cozy-ingress-nginx
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig

View File

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

View File

@@ -1,6 +1,4 @@
{{- $cozystack := .Values._cozystack | default dict }}
{{- $namespace := .Values._namespace | default dict }}
{{- $targetTenant := dig "monitoring" "" $namespace }}
{{- $targetTenant := .Values._namespace.monitoring }}
{{- if .Values.addons.monitoringAgents.enabled }}
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
@@ -11,10 +9,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: cozy-monitoring-agents
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-monitoring-agents
namespace: cozy-system
chart:
spec:
chart: cozy-monitoring-agents
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig
@@ -40,6 +43,10 @@ spec:
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-vertical-pod-autoscaler-crds
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-prometheus-operator-crds
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-metrics-server
namespace: {{ .Release.Namespace }}
values:
vmagent:
externalLabels:

View File

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

View File

@@ -8,10 +8,15 @@ metadata:
cozystack.io/target-cluster-name: {{ .Release.Name }}
spec:
releaseName: velero
chartRef:
kind: ExternalArtifact
name: cozystack-iaas-kubernetes-velero
namespace: cozy-system
chart:
spec:
chart: cozy-velero
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig

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