Compare commits

...

225 Commits

Author SHA1 Message Date
Andrei Kvapil
772fb4363a docs: add changelog for v1.0.2 (#2141)
This PR adds the changelog for release `v1.0.2`.

 Changelog has been automatically generated in
`docs/changelogs/v1.0.2.md`.

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

## Summary by CodeRabbit

* **Documentation**
  * Published v1.0.2 release notes.

* **Bug Fixes**
  * Fixed migration script to ensure all upgrade steps execute.
* Improved dashboard functionality for field clearing and secret
copying.
  * Restored sidebar navigation on namespace-level pages.
  * Updated proxy configurations for enhanced TLS handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 21:50:58 +01:00
Kirill Ilin
cb919f4c85 [dashboard] Add storageClass dropdown for stateful apps (#2131)
## What this PR does

Replaces the plain text input for `storageClass` fields with an
API-backed dropdown listing available StorageClasses from the cluster.
Follows the same pattern as the `instanceType` dropdown for VMInstance.

Affected applications:
- **Top-level `spec.storageClass`**: ClickHouse, Harbor, HTTPCache,
  Kubernetes, MariaDB, MongoDB, NATS, OpenBAO, Postgres, Qdrant,
  RabbitMQ, Redis, VMDisk
- **Nested `spec.storage.storageClass`**: FoundationDB
- **Nested `spec.kafka.storageClass` / `spec.zookeeper.storageClass`**:
Kafka

### Release note

```release-note
[dashboard] storageClass fields in stateful app forms now render as a
dropdown populated with available StorageClasses from the cluster,
instead of a free-text input.
```

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

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Storage class selection dropdowns now available in configuration forms
for multiple database, messaging, and storage services.

* **Tests**
* Added comprehensive test coverage for storage class configuration
handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 23:55:12 +05:00
cozystack-bot
aed46a8819 docs: add changelog for v1.0.2
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-03-02 18:45:19 +00:00
Andrei Kvapil
35e3eddeff [dashboard] fix: restore stock-instance sidebars for namespace-level pages (#2136)
## What this PR does

Restores `stock-instance-api-form`, `stock-instance-api-table`,
`stock-instance-builtin-form`, and `stock-instance-builtin-table`
sidebar
resources that were removed in #2106, and adds them back to the orphan
cleanup allowlist.

PR #2106 removed these sidebars to fix broken URLs on the main page
before
namespace selection (`default//api-table/...`). However,
`stock-instance-*`
sidebars are required by the frontend for namespace-level
api-table/api-form
pages. Without them and with `CUSTOMIZATION_SIDEBAR_FALLBACK_ID=""`, the
frontend cannot find a sidebar for pages like Backup Plans and renders
an
empty page where no interaction is possible.

The broken-URL bug is already fully fixed by
`CUSTOMIZATION_SIDEBAR_FALLBACK_ID=""`
in `web.yaml`. Re-adding `stock-instance-*` does not reintroduce it,
since
these sidebars are only shown when the user is on a namespace-level page
where the `{namespace}` placeholder is filled.

### Release note

```release-note
[dashboard] fix empty page on Backup Plans and other namespace-level api-table pages
```

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

## Summary by CodeRabbit

* **New Features**
* Added four new dashboard sidebar resources for stock instances: API
form, API table, built-in form, and built-in table views. These enable
expanded dashboard customization options for managing stock instance
configurations and data.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 19:28:18 +01:00
Andrei Kvapil
8d566a27ed [dashboard] fix: allow clearing instanceType and preserve secret copy newlines (#2135)
## What this PR does

Updates the openapi-k8s-toolkit integration in the dashboard to fix two
UX issues:

**1. Allow clearing the instanceType field in VMInstance form**

When `instanceType` has a default value, clearing the field in the form
UI would
silently revert to the default, making it impossible to explicitly send
an empty
value. This blocked use of custom KubeVirt resources without a named
instance type.

Adds `allowEmpty: true` to the instanceType listInput field so the BFF
preserves
an explicit empty value. Also introduces a generic `persistType` prop
(`'str' | 'number' | 'arr' | 'obj'`) to the listInput component, so the
allow-empty behaviour works correctly for any field type, not just
strings.

Updates openapi-k8s-toolkit to release/1.4.0 (d6b9e4ad), which already
includes
the FormListInput layout refactor — the previous
formlistinput-value-binding.diff
patch is no longer needed.

Upstream PR:
https://github.com/PRO-Robotech/openapi-k8s-toolkit/pull/340

**2. Preserve newlines when copying secrets with CMD+C**

Native `input[type=text]` strips newlines on copy. Adds an `onCopy`
handler to
the SecretBase64Plain component that intercepts the copy event and
writes the full
decoded value (including newlines) to the clipboard.

Upstream PR:
https://github.com/PRO-Robotech/openapi-k8s-toolkit/pull/339

### Release note

```release-note
[dashboard] Fix clearing instanceType in VMInstance form: explicit empty value
is now correctly sent to the API instead of falling back to the schema default.
Fix CMD+C copying of secrets stripping newlines.
```

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

## Summary by CodeRabbit

* **New Features**
  * Dropdown fields now support configuration to allow empty selections
* Enhanced empty value handling for form fields across multiple data
types (string, number, array, object)

* **Bug Fixes**
* Fixed secret field copy functionality to preserve plain-text format
when visible

* **Chores**
  * Updated base image dependencies for dashboard build

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 19:27:35 +01:00
Kirill Ilin
45b61f812d fix(dashboard): restore stock-instance sidebars for namespace-level pages
PR #2106 removed stock-instance-* sidebar resources to fix broken URLs
on the main page before namespace selection. However, these sidebars are
required for rendering namespace-level pages (api-table, api-form, etc.)
such as the Backup Plans page.

Without stock-instance-api-table, the frontend cannot find the sidebar
for namespace-scoped api-table pages and renders an empty page instead.

The original bug (broken URLs with empty namespace placeholder) is already
fixed by CUSTOMIZATION_SIDEBAR_FALLBACK_ID="" in web.yaml, so re-adding
stock-instance-* sidebars does not reintroduce it.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 23:13:48 +05:00
Kirill Ilin
99ee0e34bf fix(dashboard): preserve newlines when copying secrets with CMD+C
Add onCopy handler to SecretBase64Plain inputs to intercept native browser
copy events and explicitly write the full decoded text (including newlines)
to the clipboard. Without this, input[type=text] strips newlines on copy.

Upstream PR: https://github.com/PRO-Robotech/openapi-k8s-toolkit/pull/339

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 22:36:01 +05:00
Andrei Kvapil
0a461958ca [system] Fix Keycloak proxy configuration for v26.x (#2125)
## What this PR does

Replace deprecated `KC_PROXY=edge` with `KC_PROXY_HEADERS=xforwarded`
and `KC_HTTP_ENABLED=true` in the Keycloak StatefulSet template.

`KC_PROXY` was removed in Keycloak 26.x, causing "Non-secure context
detected" warnings and broken cookie handling when running behind a
reverse proxy with TLS termination.

### Release note

```release-note
[system] Fix Keycloak proxy headers configuration for compatibility with Keycloak 26.x
```

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

## Summary by CodeRabbit

## Release Notes

* **Chores**
* Updated system configuration to improve proxy header handling and
enable direct HTTP support for enhanced compatibility with reverse proxy
environments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 18:29:35 +01:00
Kirill Ilin
6e8ce65e49 feat(dashboard): set allowEmpty on instanceType and update openapi-ui toolkit
Update openapi-k8s-toolkit commit to d6b9e4ad (release/1.4.0) which
includes the FormListInput layout refactor, making formlistinput-value-binding.diff
obsolete.

Set allowEmpty: true on the VMInstance instanceType field so users can
explicitly clear the selection and override the default instance type.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 22:19:54 +05:00
Kirill Ilin
a3ccb4f87d feat(dashboard): allow clearing instanceType field in VMInstance form
Update openapi-k8s-toolkit to release/1.4.0 (d6b9e4ad). The previous
value-binding layout refactor is already included upstream, so drop the
formlistinput-value-binding.diff patch.

Add formlistinput-allow-empty.diff patch which introduces two props to
the listInput component:
- allowEmpty: when set, auto-persists the field so BFF sends an empty
  value instead of falling back to the schema default
- persistType: controls the type of empty value ('str' | 'number' | 'arr'
  | 'obj'), allowing the feature to work correctly for any field type

Set allowEmpty: true on the VMInstance instanceType field so users can
explicitly clear the selection and override the default instance type.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 22:18:53 +05:00
Andrei Kvapil
20c91f25da [platform] Fixed run-migrations script (#2126)
<!-- 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
[platform] Fixed off-by-one error where the first required migration was always skipped.
```

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

* **Bug Fixes**
* Corrected migration range handling so upgrade steps run for the
intended version window, preventing skipped or duplicated migrations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 17:53:41 +01:00
Andrei Kvapil
ee36c50d69 Disable private key rotation in CA certs (#2113)
<!-- 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
Disabled private key rotation in every CA cert in cozystack system packages to prevent trustchain problems when CA cert reissued
```

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

* **Chores**
* Disabled private-key rotation (set rotationPolicy: Never) for CA/root
certificates used by multiple system components (ingress-nginx, linstor,
linstor-scheduler, seaweedfs, victoria-metrics-operator,
kubeovn-webhook, lineage-controller-webhook, cozystack-api, etcd,
linstor API/internal, seaweedfs).
* Added patch application steps to relevant update workflows to ensure
the certificate template changes are applied during chart/update
operations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 17:48:21 +01:00
Andrei Kvapil
1de4bb39a8 [rabbitmq] update version selection (#2092)
<!-- 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
[rabbitmq] Added version selection to newly created RabbitMQ instances.
```

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

* **New Features**
* Configurable RabbitMQ major.minor version selector (v4.2, v4.1, v4.0,
v3.13), default v4.2; chart validates selection and uses it to pick the
runtime image.

* **Chores**
  * Default RabbitMQ image updated to 4.2.4.
* Added an automated version-update helper and a Makefile target to
refresh available versions and regenerate manifests.

* **Migration**
* Migration added to backfill the version field on existing RabbitMQ
resources.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 17:46:22 +01:00
Kirill Ilin
c2bf8cf56f feat(dashboard): add storageClass dropdown for all stateful apps
Replace plain text input with an API-backed listInput dropdown for
storageClass fields across all applications that expose them.

The dropdown fetches available StorageClasses from the cluster via
/api/clusters/{cluster}/k8s/apis/storage.k8s.io/v1/storageclasses,
following the same pattern as the instanceType dropdown for VMInstance.

Top-level spec.storageClass: ClickHouse, Harbor, HTTPCache, Kubernetes,
MariaDB, MongoDB, NATS, OpenBAO, Postgres, Qdrant, RabbitMQ, Redis, VMDisk.

Nested paths: FoundationDB (spec.storage.storageClass),
Kafka (spec.kafka.storageClass and spec.zookeeper.storageClass).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 21:26:51 +05:00
Andrei Kvapil
b455405402 fix(migration): suspend cozy-proxy if it conflicts with installer release (#2128)
## What this PR does

Adds a check in the migration script to detect and suspend the
`cozy-proxy`
HelmRelease if it has `releaseName: cozystack`, which conflicts with the
installer
release and causes cozystack-operator deletion during upgrade from v0.41
to v1.0.

### Release note

```release-note
[platform] Fix migration script to handle cozy-proxy releaseName conflict during v0.41→v1.0 upgrade.
```

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

## Summary by CodeRabbit

* **Chores**
* Enhanced the version 1.0 migration process with automatic conflict
detection and interactive guidance, prompting users to resolve issues
during the upgrade for a smoother migration experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 16:31:03 +01:00
Andrei Kvapil
14a9017932 fix(migration): suspend cozy-proxy if it conflicts with installer release
In v0.41.x, cozy-proxy HelmRelease was configured with
releaseName: cozystack, which collides with the installer helm release.
If not suspended before upgrade, the cozy-proxy HR reconciles and
overwrites the installer release, deleting cozystack-operator.

Add a check in the migration script that detects this conflict and
suspends the cozy-proxy HelmRelease before proceeding.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-03-02 12:59:36 +01:00
Myasnikov Daniil
dc5c3dc9bc [rabbitmq] Added app version selection
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-03-02 15:36:20 +05:00
Myasnikov Daniil
79c57874bb [platform] Fixed run-migrations script
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-03-02 15:34:13 +05:00
Kirill Ilin
14228aa0d7 fix(keycloak): replace deprecated KC_PROXY with KC_PROXY_HEADERS
KC_PROXY=edge was deprecated and removed in Keycloak 26.x, causing
"Non-secure context detected" warnings and broken cookie handling
behind reverse proxy. Replace with KC_PROXY_HEADERS=xforwarded and
KC_HTTP_ENABLED=true.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 14:35:04 +05:00
Myasnikov Daniil
013b5b0873 Replaced direct chart edits with patch files
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-03-02 12:35:14 +05:00
Andrei Kvapil
12fec9bb32 docs: add changelog for v1.0.1 (#2118)
This PR adds the changelog for release `v1.0.1`.

 Changelog has been automatically generated in
`docs/changelogs/v1.0.1.md`.

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

## Summary by CodeRabbit

* **Documentation**
* Published v1.0.1 release notes with platform, installer, and dashboard
bug fixes
* Updated website documentation: renamed "Bundles" to "Variants," added
new variant options, and updated cross-references
* Added upgrade protection instructions for system components prior to
upgrade

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-28 15:16:35 +01:00
cozystack-bot
f3b61291bb docs: add changelog for v1.0.1
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-28 11:06:39 +00:00
Andrei Kvapil
dec15016c7 fix(dashboard): add FlowSchema to exempt BFF from API throttling (#2121)
## Summary
- Add FlowSchema `cozy-dashboard-exempt` to exempt the dashboard BFF
service account (`incloud-web-web`) from API Priority and Fairness
throttling
- BFF falls under the default `service-accounts` FlowSchema →
`workload-low` priority level, which causes 429 responses under load

## Test plan
- [ ] Deploy to a cluster with dashboard enabled
- [ ] Verify FlowSchema is created: `kubectl get flowschema
cozy-dashboard-exempt`
- [ ] Verify BFF no longer receives 429 errors under load

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

## Summary by CodeRabbit

* **Chores**
* Added a new Kubernetes FlowSchema configuration for system resource
access management.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-28 11:55:12 +01:00
Andrei Kvapil
018aae75a0 fix(installer): add keep annotation to Namespace and update migration script (#2122)
## What this PR does

Adds `helm.sh/resource-policy: keep` annotation to the `cozy-system`
Namespace resource
in the installer helm chart. This prevents Helm from deleting the
namespace (and all
HelmReleases within it) when the installer release is removed.

Also updates the v1.0 migration script to annotate the `cozy-system`
namespace and
`cozystack-version` ConfigMap with the same policy before generating the
Package resource.

### Release note

```release-note
[platform] Add helm.sh/resource-policy=keep annotation to cozy-system Namespace in installer chart to prevent namespace deletion on HelmRelease removal. Update migration script to protect namespace and cozystack-version ConfigMap before migration.
```

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

## Summary by CodeRabbit

* **Chores**
* Enhanced migration process with an interactive step to safeguard
critical resources during system upgrades.
* Added resource protection mechanisms to prevent unintended removal
during Helm operations.
* Improved control flow in the upgrade script with explicit user
confirmation prompts.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-28 11:52:51 +01:00
Andrei Kvapil
c83e41ea14 fix(installer): add keep annotation to Namespace and update migration script
Add helm.sh/resource-policy=keep annotation to the cozy-system Namespace
in the installer helm chart. This prevents Helm from deleting the
namespace when the HelmRelease is removed, which would otherwise destroy
all other HelmReleases within it.

Update the migration script to annotate the cozy-system namespace and
cozystack-version ConfigMap with helm.sh/resource-policy=keep before
generating the Package resource.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-28 11:46:09 +01:00
Andrei Kvapil
161b5be8c2 fix(dashboard): add FlowSchema to exempt BFF from API throttling
The dashboard BFF service account (incloud-web-web) falls under the
default "service-accounts" FlowSchema which maps to the "workload-low"
priority level. Under load, this causes API Priority and Fairness to
return 429 (Too Many Requests) responses to the BFF, resulting in 500
errors for dashboard users.

Add a FlowSchema that maps the BFF service account to the "exempt"
priority level to prevent APF throttling of dashboard API requests.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-28 11:31:05 +01:00
Andrei Kvapil
daa3905b67 [ci] Debug improvements (#2111)
<!-- 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
[ci] Added more debug information to ci tests
```

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

## Summary by CodeRabbit

* **Chores**
* Enhanced error handling and diagnostic output in development testing
infrastructure.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-27 18:24:07 +01:00
Andrei Kvapil
3862e7c145 feat(seaweedfs): add storage pools support for tiered storage (#2097)
## What this PR does

- Add `volume.pools` (Simple topology) and `volume.zones[name].pools`
(MultiZone topology) for creating separate Volume StatefulSets per disk
type (SSD/HDD/NVMe)
- Add `nodeSelector`, `storageClass`, and `dataCenter` overrides for
zones in MultiZone topology
- Create per-pool `BucketClass` and `BucketAccessClass` COSI resources
(including WORM and readonly variants)
- Bump seaweedfs-cosi-driver to v0.3.0 (adds `disk` parameter support in
BucketClass)
- Add `volume.diskType` field to tag default volume servers with a disk
type

### How It Works

#### Simple Topology

Each storage pool in `volume.pools` creates an additional Volume
StatefulSet alongside the default one. All pods (default + pool) may run
on the same nodes. SeaweedFS distinguishes storage via the
`-disk=<type>` flag on volume servers.

```yaml
volume:
  replicas: 2
  size: 10Gi
  diskType: ""
  pools:
    ssd:
      diskType: ssd
      size: 50Gi
      storageClass: local-nvme
```

#### MultiZone Topology

Pools are defined per-zone in `volume.zones[name].pools`. A StatefulSet
is created for each **zone × pool** combination (e.g., `us-east-ssd`,
`us-west-ssd`), inheriting nodeSelector and dataCenter from its parent
zone.

```yaml
volume:
  replicas: 2
  size: 10Gi
  zones:
    us-east:
      replicas: 2
      size: 100Gi
      # nodeSelector defaults to: topology.kubernetes.io/zone: us-east
      pools:
        ssd:
          diskType: ssd
          size: 50Gi
    us-west:
      replicas: 3
```

In Simple topology, `volume.pools` is used. In MultiZone,
`volume.zones[name].pools` is used — `volume.pools` is explicitly
blocked to prevent BucketClasses without backing StatefulSets.

### Zone Overrides (MultiZone)

Zones now support:
- `nodeSelector` — YAML string, defaults to
`topology.kubernetes.io/zone: <zoneName>`
- `storageClass` — defaults to `volume.storageClass`
- `dataCenter` — SeaweedFS data center name, defaults to zone name

### COSI Resources

Each unique pool name generates 4 cluster-scoped COSI resources:
- `<namespace>-<pool>` BucketClass (Delete policy, `disk: <type>`)
- `<namespace>-<pool>-worm` BucketClass (Retain policy, object lock)
- `<namespace>-<pool>` BucketAccessClass (readwrite)
- `<namespace>-<pool>-readonly` BucketAccessClass (readonly)

### Validation

- Pool names must be valid DNS labels (no dots)
- Pool names must not end with `-worm` or `-readonly` (reserved COSI
suffixes)
- `diskType` is required and must be lowercase alphanumeric
- Pool `diskType` must differ from `volume.diskType`
- Pool name + zone name composed names must not collide with existing
zone names
- `volume.pools` is blocked in Client and MultiZone topologies
- All replicas have `minimum: 1` in JSON schema

### Inheritance Chain

| Field | Pool fallback (Simple) | Pool fallback (MultiZone) |
| ------------ | -------------------------------- |
---------------------------------------- |
| `replicas` | pool → volume | pool → zone → volume |
| `size` | pool → volume | pool → zone → volume |
| `storageClass` | pool → volume | pool → zone → volume |
| `resources` | pool → volume | pool → volume (zone resources inherited)
|

### Backward Compatibility

- Default `volume.pools: {}` produces identical output to current chart
- Default `volume.diskType: ""` adds no extra flags
- Existing default BucketClass remains unchanged
- No migration needed — pools create new StatefulSets

### Test plan

- [x] `helm template` with empty pools — output identical to current
- [x] `helm template` with Simple + volume.pools — additional volume
StatefulSets, BucketClasses, WorkloadMonitors
- [x] `helm template` with MultiZone + zone.pools — zone × pool
cross-product StatefulSets
- [x] `helm template` with `volume.diskType: hdd` — extraArgs includes
`-disk=hdd`
- [x] `helm template` with Client + volume.pools — fails with validation
error
- [x] `helm template` with MultiZone + volume.pools — fails with
validation error
- [x] `helm template` with reserved pool name suffix — fails with
validation
- [x] Deploy to test cluster and verify volume servers register with
correct disk types

### Release note

```release-note
[seaweedfs] add storage pools support for tiered storage with per-pool COSI resources
```

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

## Summary by CodeRabbit

* **New Features**
* Added support for multiple storage pools with configurable disk types
and resource allocation.
* Introduced per-pool bucket and access classes for storage management.
  * Added zone-aware pool configurations for multi-zone deployments.
  * Enhanced topology-driven resource monitoring and allocation.

* **Documentation**
* Updated service documentation with expanded configuration parameters
and improved formatting.

* **Chores**
  * Updated container image to latest version.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-27 16:38:57 +01:00
Andrei Kvapil
3c108c3353 feat(bucket): add readonly S3 access credentials (#2105)
## Summary

- Add a readonly `BucketAccessClass` to the seaweedfs COSI chart with
`accessPolicy: "readonly"` parameter
- Each bucket now automatically creates two sets of S3 credentials:
readWrite (existing, for UI) and readonly
- Update dashboard RBAC and ApplicationDefinition to expose the readonly
credentials secret

## Test plan

- [ ] Verify seaweedfs chart templates render both `BucketAccessClass`
resources (readWrite and readonly)
- [ ] Verify bucket app templates render `BucketClaim` + 2
`BucketAccess` (readWrite + readonly)
- [ ] Deploy a bucket and confirm both credential secrets are created by
COSI driver
- [ ] Confirm readonly credentials can only read/list objects, not
write/delete

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

## Summary by CodeRabbit

* **New Features**
* Introduced read-only bucket access capabilities. Users can now
configure read-only permissions for bucket storage resources,
complementing existing access control options. New read-only access
classes and configurations provide enhanced security controls and
finer-grained permission management. This enables improved data
protection while maintaining flexibility for various access requirements
across applications and storage infrastructure.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-27 16:36:13 +01:00
Myasnikov Daniil
47d81f70d7 Disabled private key rotation in CA certs
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-27 19:09:10 +05:00
Andrei Kvapil
ac48228440 [cert-manager] Update cert-manager to v1.19.3 (#2070)
<!-- 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
[cert-manager] Updated cert-manager to v1.19.3
```

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

* **New Features**
* Global nodeSelector and hostUsers (pod user-namespace isolation) added
* New/updated CRDs for cert-manager resources (Certificate,
CertificateRequest, Order, etc.)

* **Documentation**
* Revised chart docs and installation guidance; added deprecation/notice
about private-key rotation
* Removed legacy CRD README and schema files from the CRD package
(documentation consolidated)

* **Chores**
  * Upgraded cert-manager to v1.19.3
* Moved CRDs into a dedicated CRD package; ServiceMonitor targetPort
default renamed to "http-metrics"
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-27 13:03:55 +01:00
Andrei Kvapil
c393cf92d9 [platform] Prevent version cm from deletion (#2112)
<!-- 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
[platform] Prevent cozystack-version configmap from deletion 
```

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

## Summary by CodeRabbit

* **Chores**
* Updated deployment resource configuration to improve system
reliability by ensuring critical components are properly retained and
protected during system operations and maintenance activities.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-27 13:03:33 +01:00
Andrei Kvapil
022ddf73a8 [apps] Add OpenBAO as a managed secrets management service (#2059)
## What this PR does

Adds OpenBAO (open-source Vault fork) as a new managed PaaS application
in Cozystack.

**Structure follows existing app patterns (qdrant, nats):**
- System chart with vendored upstream `openbao/openbao` (chart v0.25.3,
appVersion v2.5.0)
- App chart with standalone/HA mode switching based on replicas count
- TLS via cert-manager self-signed certificates per instance
- ApplicationDefinition, PackageSource, PaaS bundle entry
- E2E test with init/unseal workflow

**Key design decisions:**
- `replicas: 1` → standalone mode with file storage; `replicas > 1` → HA
with Raft integrated storage and retry_join with TLS peer verification
- TLS enabled by default — each instance gets a self-signed Certificate
with DNS SANs covering services and pod addresses
- `disable_mlock = true` in HCL config since default security context
drops IPC_LOCK capability
- Injector and CSI provider disabled (cluster-scoped components, not
safe per-tenant)
- No auto-init/unseal — OpenBAO requires manual initialization by design
- E2E test performs full lifecycle: deploy, wait for certificate + API,
init, unseal, verify readiness, cleanup

### Release note

```release-note
[apps] Add OpenBAO as a managed secrets management service with standalone and HA Raft modes, TLS enabled by default
```

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

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added OpenBAO managed secrets management service with
high-availability and standalone deployment options
  * Integrated monitoring and dashboards for operational visibility
  * Enabled configurable external access and web UI
  * Added automated snapshot backup capability

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-27 11:11:59 +01:00
Andrey Kolkov
182562829c fix(dashboard): added dropdown for backupClasses (#2104)
<!-- 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
- added dropdown for selection backupClasses in Plan/BackupJob creation form
```
2026-02-27 13:22:17 +04:00
Myasnikov Daniil
3bf43312aa (ci) Removed cozytest output trimming in non-tty run
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-27 12:43:00 +05:00
Myasnikov Daniil
fd6d0c3603 (ci) Added extra debug commands for k8s startup
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-27 12:41:40 +05:00
Andrey Kolkov
f0e8e11042 fix(dashboard): added dropdown for backupClasses
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-02-27 11:04:46 +04:00
Kirill Ilin
b0b0dad305 refactor(seaweedfs): move storage pools from top-level to volume.pools and zone.pools
Redesign storage pools architecture:
- Move storagePools map from top-level into volume.pools (Simple topology)
  and volume.zones[name].pools (MultiZone topology)
- Add nodeSelector, storageClass, dataCenter overrides for zones
- Add reserved suffix validation (-worm, -readonly) for pool names
- Block volume.pools usage in MultiZone (must use zone.pools instead)
- Use ternary/hasKey pattern for all optional replicas to handle 0 correctly
- Fix nodeSelector rendering for multiline values using indent
- Use disk: parameter (not diskType:) for COSI driver v0.3.0 BucketClass
- Bump seaweedfs-cosi-driver tag to v0.3.0
- Add minimum: 1 constraint for volume/zone/pool replicas in schema
- Regenerate README, CRD, and openAPISchema

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-27 11:53:26 +05:00
Kirill Ilin
275ab71fee fix(seaweedfs): address review blocking issues
- Remove dots from pool name regex (K8s resources don't allow dots)
- Add zone×pool name collision validation for MultiZone topology
- Use conditional storageClass rendering to omit empty values
- Fix README resourcesPreset default value

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-27 11:53:26 +05:00
Kirill Ilin
e244412871 fix(seaweedfs): address review comments
- Document MultiZone fallback chain for pool replicas and size
- Move `-volume` WorkloadMonitor reference inside Simple topology block in dashboard-resourcemap.yaml (it is only created for Simple)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-27 11:53:26 +05:00
Kirill Ilin
7684f41ef7 feat(seaweedfs): add per-pool COSI resources and optional storageClass inheritance
Make storageClass optional in storagePools — pools inherit from
volume.storageClass when not explicitly set. Add full COSI resource set
per storage pool: BucketClass, BucketClass-worm (Retain + object lock),
BucketAccessClass readwrite, and BucketAccessClass readonly.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-27 11:53:26 +05:00
Kirill Ilin
492b24dcad feat(seaweedfs): add storage pools support for tiered storage
Add optional storagePools configuration that creates separate Volume
StatefulSets per disk type (SSD/HDD/NVMe), enabling tiered storage
within a single SeaweedFS tenant. Each pool gets its own BucketClass
and BucketAccessClass to prepare infrastructure for COSI driver
integration.

Supported in both Simple and MultiZone topologies:
- Simple: one StatefulSet per pool
- MultiZone: one StatefulSet per zone×pool combination

Also adds volume.diskType field for tagging default volume servers.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-27 11:53:25 +05:00
Myasnikov Daniil
c05dd5e7b1 [platform] Prevent version cm from deletion
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-26 23:11:36 +05:00
Andrei Kvapil
75f2e231cd docs: add changelog for v1.0.0 (#2109)
This PR adds the changelog for release `v1.0.0`.

 Changelog has been automatically generated in
`docs/changelogs/v1.0.0.md`.

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

## Summary by CodeRabbit

* **Documentation**
* Added comprehensive v1.0.0 release notes documenting feature
highlights, improvements, and fixes across all platform components
* Included breaking changes and step-by-step upgrade guide for v0.x to
v1.0.0 migration
  * Listed 33 incremental migrations and contributor credits

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-26 17:39:23 +01:00
Andrei Kvapil
ab26d71cc7 Release v1.0.0 (#2108)
This PR prepares the release `v1.0.0`.

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

## Summary by CodeRabbit

* **Chores**
* Updated container image versions from pre-release candidate tags to
stable v1.0.0 releases across core, system, and extra packages.
* Updated all associated container image digests to reflect the stable
release builds.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-26 17:39:08 +01:00
cozystack-bot
00f0c7bc41 docs: add changelog for v1.0.0
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-26 15:29:21 +00:00
cozystack-bot
66a61bd63e Prepare release v1.0.0
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-26 15:17:04 +00:00
Myasnikov Daniil
4734f65f90 [cert-manager] Added cleanup before crds upgrade
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-26 18:32:35 +05:00
Myasnikov Daniil
4df456fdcd [cert-manager] Updated cert-manager to 1.19.3 with new crd packaging
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-26 15:28:19 +03:00
Andrey Kolkov
f887e34206 fix(dashboard): hide sidebar on cluster-level pages when no tenant selected (#2106)
<!-- 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

In the current version, the sidebar incorrectly shows namespace-scoped
menu items on cluster-level pages (before a tenant is selected).
Clicking these items produces broken URLs with double `//` (e.g.
`default//api-table/backups.cozystack.io/...`) because the `{namespace}`
placeholder resolves to an empty string.

This PR fixes the issue by:
- Removing stock-instance-* sidebar resources that were populated with
the same namespace-scoped menu as stock-project-* sidebars
- Clearing the `CUSTOMIZATION_SIDEBAR_FALLBACK_ID` env var so the
frontend renders no sidebar when no matching sidebar resource exists
- Removing stock-instance-* from the expected resource set so orphan
cleanup removes stale instances on upgrade

Screenshot after changes
<img width="2560" height="1327" alt="dashboard screenshot with no tenant
selected"
src="https://github.com/user-attachments/assets/e0d795f7-55e9-471b-99b8-593b6fc145d8"
/>


### Test plan

- [x] On cluster list page (no tenant selected), sidebar is empty
- [x] After selecting a tenant, sidebar shows full menu
- [x] No double `//` in sidebar URLs
- [x] Existing tests pass: `go test ./internal/controller/dashboard/...`

### 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
[dashboard] fix: hide sidebar on cluster-level pages when no tenant selected
```

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

## Summary by CodeRabbit

* **Refactor**
* Streamlined sidebar resource management by reducing the number of
static sidebar configurations generated by the system.
* Removed sidebar fallback behavior, resulting in simplified sidebar
customization defaults.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-26 15:26:55 +04:00
Kirill Ilin
7a107296e5 fix(dashboard): hide sidebar on cluster-level pages when no tenant selected
Remove stock-instance-* sidebars that were populated with namespace-scoped
menu items, causing the sidebar to incorrectly appear on cluster-level pages.
Clear the sidebar fallback ID so the frontend gracefully renders no sidebar
when no matching sidebar resource exists.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-26 13:43:07 +05:00
Andrei Kvapil
e16d987403 [dashboard] Hide Ingresses/Services/Secrets tabs when no selectors defined (#2087)
## What this PR does

Hide Ingresses, Services, and Secrets tabs in the dashboard when the
ApplicationDefinition has no resource selectors (Include/Exclude) for
the corresponding type. Previously all tabs were always hardcoded as
visible.

### Release note

```release-note
[dashboard] Hide Ingresses/Services/Secrets tabs when ApplicationDefinition has no resource selectors defined
```

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

* **Bug Fixes**
* Dashboard tab visibility refined: Workloads tab remains always
visible; Ingresses, Services, and Secrets tabs now appear only when
corresponding resource selectors are configured, reducing clutter and
showing relevant tabs based on configured resources.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 21:58:29 +01:00
IvanHunters
0ab7cc780a feat(bucket): add readonly S3 access credentials
Add a readonly BucketAccessClass to the seaweedfs COSI chart and a
second fixed BucketAccess per bucket so each bucket automatically
gets both readWrite and readonly S3 credentials.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-02-25 23:51:42 +03:00
Andrei Kvapil
c63fcf50b3 docs: add changelog for v1.0.0-rc.2 (#2100)
This PR adds the changelog for release `v1.0.0-rc.2`.

 Changelog has been automatically generated in
`docs/changelogs/v1.0.0-rc.2.md`.

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

## Summary by CodeRabbit

* **Documentation**
* Added release notes for v1.0.0-rc.2, documenting features,
improvements, bug fixes, and contributor acknowledgments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 19:14:55 +01:00
Andrei Kvapil
4417cc35a0 Release v1.0.0-rc.2 (#2099)
This PR prepares the release `v1.0.0-rc.2`.

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

* **Chores**
* Updated numerous container images to v1.0.0-rc.2 across platform
services, controllers, dashboard components, migration/e2e tooling,
storage and networking components.
* Refreshed several image digests (including kubevirt CSI, s3manager and
Linstor components) and other image references.
  * Updated default tenant branding text used by the dashboard.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 19:12:46 +01:00
cozystack-bot
78cc4c0955 docs: add changelog for v1.0.0-rc.2
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-25 16:18:20 +00:00
cozystack-bot
65c6936e95 Prepare release v1.0.0-rc.2
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-25 16:09:50 +00:00
Andrei Kvapil
cd3643b8cc fix(dashboard): backupjobs creation form fixes and category idetifier… (#2103)
… in sidebar

<!-- 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
- fixed dashboard backupjobs creation form 
- fixed dashboard sidebar backup category id
```

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

## Summary by CodeRabbit

* **Updates**
  * Updated dashboard backups menu structure for improved organization
* Enhanced backup job management interface with new form configuration
including Name, Namespace, Plan Name, Application details, and Backup
Class selection

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 17:02:39 +01:00
Andrei Kvapil
2024ec3a8b feat(keycloak): allow custom Ingress hostname via values (#2101)
## What this PR does

Add `ingress.host` field to cozy-keycloak values, allowing users to
override the default
`keycloak.<root-host>` Ingress hostname. The custom hostname is applied
to both the Ingress
resource and the `KC_HOSTNAME` environment variable in the StatefulSet.
When left empty,
behavior is unchanged (backward compatible).

### Release note

```release-note
[system] Add `ingress.host` option to cozy-keycloak for custom Ingress hostname override
```

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

## Summary by CodeRabbit

* **New Features**
* Keycloak ingress hostname is now configurable and automatically
defaults to "keycloak.<root-host>" when not explicitly specified.

* **Chores**
* Refactored hostname configuration across Keycloak templates to use
dynamic variable resolution instead of hard-coded values for improved
consistency and flexibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 16:57:53 +01:00
Andrei Kvapil
f282f19c1b fix(platform): clean up orphaned -rd HelmReleases after application renames (#2102)
## What this PR does

After application renames (ferretdb→mongodb, mysql→mariadb,
virtual-machine→vm-disk+vm-instance),
the system-level `-rd` HelmReleases in `cozy-system` were left orphaned.
They reference
ExternalArtifacts that no longer exist, causing persistent
reconciliation failures:

- `ferretdb-rd` → no longer exists (replaced by `mongodb-rd`)
- `mysql-rd` → no longer exists (replaced by `mariadb-rd`)
- `virtual-machine-rd` → no longer exists (replaced by `vm-disk-rd` +
`vm-instance-rd`)

Migrations 28 and 29 handled user-facing HelmReleases but missed the
system-level `-rd` ones.

**Changes:**
- Add cleanup of `mysql-rd` to migration 28
- Add cleanup of `virtual-machine-rd` to migration 29
- Add migration 33 as a safety net for users who already passed
migrations 28/29
- Bump `targetVersion` from 33 to 34

### Release note

```release-note
[platform] Fix orphaned ferretdb-rd, mysql-rd, and virtual-machine-rd HelmReleases
that persist after upgrading, referencing non-existent ExternalArtifacts.
```

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved cleanup procedures in platform migrations to properly remove
orphaned system resources. This enhancement helps maintain system
stability and prevents potential resource conflicts during platform
updates.

* **Chores**
  * Updated migration version target to latest.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 16:56:45 +01:00
Andrey Kolkov
7427bbdaa3 fix(dashboard): backupjobs creation form fixes and category idetifier in sidebar
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-02-25 19:56:13 +04:00
Andrei Kvapil
da89203a32 fix(platform): add migration 33 to clean up orphaned -rd HelmReleases
Add a safety-net migration for users who already passed migrations 28/29
and still have orphaned -rd HelmReleases in cozy-system:

- ferretdb-rd (replaced by mongodb-rd, never had a dedicated migration)
- mysql-rd (migration 28 only handled user HRs)
- virtual-machine-rd (migration 29 only handled user HRs)

These HRs reference ExternalArtifacts that no longer exist after the
application renames (ferretdb→mongodb, mysql→mariadb,
virtual-machine→vm-disk+vm-instance), causing persistent errors.

Bump targetVersion from 33 to 34.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-25 16:02:10 +01:00
Andrei Kvapil
e0dfc8a321 fix(platform): clean up orphaned -rd HelmReleases in migrations 28 and 29
Migrations 28 (mysql→mariadb) and 29 (virtual-machine→vm-disk+vm-instance)
only handled user-facing HelmReleases but left the system-level -rd
HelmReleases (mysql-rd, virtual-machine-rd) orphaned in cozy-system.
These HRs reference ExternalArtifacts that no longer exist, causing
persistent reconciliation failures.

Add cleanup steps to delete the orphaned -rd HRs and their Helm secrets.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-25 16:01:58 +01:00
Kirill Ilin
9cbd948b08 feat(keycloak): allow custom Ingress hostname via values
Add ingress.host field to values.yaml for cozy-keycloak. When set,
it overrides the default "keycloak.<root-host>" hostname in both the
Ingress resource and the KC_HOSTNAME environment variable. When left
empty, behavior is unchanged for backward compatibility.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-25 17:36:53 +05:00
Andrey Kolkov
e5f7bc5c53 fix(dashboard): updated legacy templating (#2093)
<!-- 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
fixed dashboard sidebar links to Backups and External IPs
```

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

* **Refactor**
* Standardized cluster identifier usage across dashboard menu links,
administration links, and API request paths for consistent link
generation.
* **Bug Fixes**
* Resolved issues causing incorrect or inconsistent link targets and
ensured backup-class options load correctly in the UI.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 14:46:25 +04:00
Andrei Kvapil
e89ba43c39 fix(platform): fix upgrade issues in migrations, etcd timeout, and migration script (#2096)
## What this PR does

Fixes multiple upgrade issues discovered during v0.41.1 → v1.0 upgrade
testing.

**Migration 26 (monitoring → monitoring-system):**
- Use `cozystack.io/ui=true` label with
`--field-selector=metadata.name=monitoring` instead of
`apps.cozystack.io/application.kind=Monitoring` to find monitoring
HelmReleases — the old label is guaranteed to exist on v0.41.1 clusters,
while the new one depends on migration 22 having run
- Add `delete_helm_secrets` function with fallback deletion by secret
name pattern and post-deletion verification

**Migrations 28 and 29 (mysql→mariadb, virtual-machine split):**
- Wrap `grep` in pipes with `{ ... || true; }` to prevent `pipefail`
exit when grep filters out all lines
- Fix reconcile annotation in migration 29 to use RFC3339 timestamp
format instead of Unix epoch
- Remove protection-webhook handling from migration 29 — it is an
external component and should not be managed by cozystack migrations

**Migration 27 (piraeus CRD ownership):**
- Skip CRDs that don't exist instead of failing the entire migration
- Add name-pattern fallback for helm secret deletion

**etcd HelmRelease:**
- Increase timeout from 10m to 30m to accommodate TLS cert rotation hook

**migrate-to-version-1.0.sh:**
- Add missing ConfigMap → Package field mappings: `bundle-disable`,
`bundle-enable`, `expose-ingress`, `expose-services`
- Remove redundant bundle enabled flags — the variant already determines
them via its values file

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

## Summary by CodeRabbit

* **New Features**
* Package generation now supports disabled/enabled package lists,
ingress name, and exposed services for customized publishing.

* **Bug Fixes**
* More robust secret cleanup with fallback deletions and post-deletion
verification.
  * Guarded pipelines to avoid failures when no resources match.
  * Reconciliation timestamps now use RFC3339 UTC.
  * Suspension failures are no longer silently suppressed.

* **Chores**
* Increased etcd upgrade timeout; improved namespace discovery,
relabeling behavior, and user-facing messaging.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 10:29:45 +01:00
Aleksei Sviridkin
a20951def3 chore: add @sircthulhu to CODEOWNERS (#2098)
## Summary

- Add @sircthulhu to the global CODEOWNERS list

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

## Summary by CodeRabbit

* **Chores**
  * Updated repository maintenance configuration.

---

**Note:** This release contains only internal repository updates with no
user-facing changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 12:08:17 +03:00
Kirill Ilin
4c73ac54a0 chore: add @sircthulhu to CODEOWNERS
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-25 13:09:21 +05:00
Andrei Kvapil
cfb5914cdd fix(platform): remove protection-webhook handling from migration 29
The protection-webhook is not part of the cozystack platform and should
not be managed by the migration script. Old services are now deleted
directly instead of being batched through the webhook disable/enable cycle.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-25 00:30:54 +01:00
Andrei Kvapil
948346ef6d fix(platform): use original cozystack.io/ui label in migration 26 and simplify migration script
Migration 26 was using apps.cozystack.io/application.kind=Monitoring label
which is added by migration 22 and may not be present on v0.41.1 clusters.
Switch to cozystack.io/ui=true (guaranteed on old HRs) with field-selector
for exact name match.

Also remove redundant bundle enabled flags from migrate-to-version-1.0.sh
since the variant already determines them via its values file.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-24 23:49:43 +01:00
Andrei Kvapil
da597225d1 fix(platform): add missing field mappings in migrate-to-version-1.0.sh
Add ConfigMap fields that were not converted to Package values:
- bundle-disable → bundles.disabledPackages
- bundle-enable → bundles.enabledPackages
- expose-ingress → publishing.ingressName
- expose-services → publishing.exposedServices

Remove incorrect bundles.system.type field that is not part of the
Package values schema.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-24 23:34:53 +01:00
Andrei Kvapil
7871d425dd fix(etcd): increase HelmRelease timeout to 30m for cert rotation
The post-upgrade hook deletes TLS certificates and etcd pods to trigger
cert-manager regeneration. With 3 replicas and startup probes allowing
up to 25 minutes per pod, the previous 10m timeout was insufficient.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-24 23:34:47 +01:00
Andrei Kvapil
a9adda5e88 fix(platform): make migration 27 skip missing CRDs and add secret cleanup fallback
Migration 27 failed with set -e when Piraeus CRDs did not exist on
clusters without linstor. Add existence check before annotating CRDs.
Also add name-pattern fallback for helm secret deletion, consistent
with migration 26.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-24 23:34:40 +01:00
Andrei Kvapil
880b99f3f7 fix(platform): wrap grep in migrations 28 and 29 to prevent pipefail exits
grep returns exit code 1 when no lines match. With set -euo pipefail,
this kills the script when all secrets are helm-release secrets or when
no matching resources exist. Wrap grep calls with { ... || true; }.

Also fix reconcile annotation in migration 29 to use RFC3339 timestamp
format instead of Unix epoch, which Flux v2 expects.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-24 23:34:34 +01:00
Andrei Kvapil
c7290f3521 fix(platform): make migration 26 helm secret deletion robust
Migration 26 silently skipped namespace processing when kubectl
queries failed, leaving helm release secrets intact. This caused
helm to diff old vs new chart manifests during upgrade, deleting
VLogs/CNPG resources and their PVCs.

- Remove silent error suppression (2>/dev/null || true) from
  namespace discovery and HR suspend commands
- Add fallback secret deletion by name pattern when label selector
  does not match
- Add verification that all helm release secrets are deleted

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-24 23:05:03 +01:00
Andrey Kolkov
2b1b5e8fa9 fix(dashboard): updated legacy templating
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-02-24 17:54:08 +04:00
Andrei Kvapil
4f2578a32b fix(dashboard): check only Include selectors for tab visibility
Only show Ingresses/Services/Secrets tabs when Include selectors are
defined. Exclude selectors alone don't make resources visible as tenant
resources, so tabs would be empty.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-23 20:20:03 +01:00
Andrei Kvapil
d8f5083c6d fix(monitoring-agents): use global.clusterDomain for FQDN resolution (#2086)
## What this PR does

PR #2075 introduced `_cluster.cluster-domain` references in
monitoring-agents `values.yaml` for FQDN resolution in tenant clusters.
This broke the fluent-bit subchart because `_cluster` values are not
accessible from the Helm subchart context — only `global` values are
shared with subcharts.

This PR replaces `_cluster` references with a new `global.clusterDomain`
variable:
- Empty by default (management cluster uses short DNS names like
`service.namespace.svc`)
- Set to the management cluster domain (e.g. `cozy.local`) for tenant
clusters, enabling FQDN resolution for cross-cluster service discovery

Fixes #2084

### Release note

```release-note
[system] Fix monitoring-agents installation failure caused by inaccessible _cluster values in fluent-bit subchart context. Introduce global.clusterDomain for proper FQDN resolution in tenant workload clusters.
```

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

* **Chores**
* Monitoring agent configuration updated to support configurable cluster
domain names for greater flexibility.
* Remote write and log-forwarding endpoints adjusted to align with
cluster domain handling, improving compatibility when deploying across
different cluster DNS setups.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-23 20:16:36 +01:00
Andrei Kvapil
211e01bd87 fix(dashboard): hide Ingresses/Services/Secrets tabs when no selectors defined
Show these tabs only when the ApplicationDefinition has non-empty
Include or Exclude resource selectors for the corresponding type.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-23 16:33:27 +01:00
Andrei Kvapil
d8bb3527de fix(monitoring-agents): revert _cluster FQDN and use cluster-domain in tenant HelmRelease
PR #2075 added _cluster.cluster-domain references to monitoring-agents
values.yaml for FQDN resolution. This broke fluent-bit because it is
a subchart where _cluster values are not accessible (only global
values are shared with subcharts), causing "index of untyped nil".

Revert values.yaml to short DNS names (.svc) for the management
cluster where they resolve locally, and add FQDN with cluster-domain
suffix in the tenant kubernetes HelmRelease where _cluster values are
available and cross-cluster DNS resolution is needed.

Closes #2084

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-23 16:15:36 +01:00
Andrei Kvapil
1fd1da45b9 docs: add changelog for v1.0.0-rc.1 (#2082)
This PR adds the changelog for release `v1.0.0-rc.1`.

 Changelog has been automatically generated in
`docs/changelogs/v1.0.0-rc.1.md`.

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

## Summary by CodeRabbit

* **Documentation**
* Added comprehensive release notes for v1.0.0-rc.1, documenting new
features, improvements, bug fixes, breaking changes, and upgrade
guidance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-22 08:04:29 +01:00
Andrei Kvapil
2c82b22c5e docs(changelogs): add missing changelogs for v0.40.5-v0.40.7 and v0.41.4-v0.41.9 (#2083)
## What this PR does

Add 9 missing patch release changelogs:
- **v0.40.x**: v0.40.5, v0.40.6, v0.40.7
- **v0.41.x**: v0.41.4, v0.41.5, v0.41.6, v0.41.7, v0.41.8, v0.41.9

All PR authors were verified via `gh pr view` (not from commit authors).
Format matches existing changelog files in the repository.

### Release note

```release-note
[docs] Add missing changelogs for v0.40.5-v0.40.7 and v0.41.4-v0.41.9
```
2026-02-22 08:04:08 +01:00
Andrei Kvapil
b61dc7c988 Release v1.0.0-rc.1 (#2081)
This PR prepares the release `v1.0.0-rc.1`.

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

## Summary by CodeRabbit

## Release Notes

* **Chores**
* Upgraded container image versions from v1.0.0-beta.6 to v1.0.0-rc.1
across the entire platform. Updates span multiple critical components
including the core operator, platform migrations, dashboard and API
services, backup and storage controllers, networking components, and
various additional system infrastructure services. All updated images
include new SHA256 digests for ensuring integrity and verification.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-22 08:03:40 +01:00
Andrei Kvapil
473ac87d70 docs(changelogs): add missing changelogs for v0.40.5-v0.40.7 and v0.41.4-v0.41.9
Add 9 missing patch release changelogs covering changes from
January to February 2026. All PR authors verified via GitHub CLI.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-21 22:17:08 +01:00
cozystack-bot
9fa311e5ac docs: add changelog for v1.0.0-rc.1
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-21 21:07:45 +00:00
cozystack-bot
7994976052 Prepare release v1.0.0-rc.1
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-21 20:58:57 +00:00
Andrei Kvapil
e3a5933f7b [kubevirt] Update kubevirt and CDI (#1833)
## What this PR does
Updates kubevirt to v1.6.3 and CDI to v1.64.0.
Please note that VMs would be live-migrated as a part of the update.

### Release note
```release-note
Updated kubevirt to v1.6.3 and CDI to v1.64.0
```

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

* **New Features**
* VM synchronization support with synchronization ports and address
reporting
  * Cluster profiler and synchronization controller developer options

* **Updates**
* CDI operator bumped to v1.64.0; filesystem overhead default increased
  * KubeVirt operator bumped to v1.6.3
  * Added liveness/readiness probes and health/metrics ports
  * Expanded operator tolerations for control-plane/master nodes
* Expanded operator permissions for synchronization and webhook-related
resources
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-21 21:43:57 +01:00
Andrei Kvapil
7dfb819a9c fix(kubevirt): disable serial console log to fix VM pod initialization
KubeVirt v1.6.x has a known issue (#15989) where the guest-console-log
init container blocks virt-launcher pods from starting. The container
runs virt-tail as a long-running sidecar but fails to properly function
as a Kubernetes native sidecar, causing all VM pods to get stuck in
PodInitializing state indefinitely.

Disable serial console logging globally via the KubeVirt CR to prevent
the problematic init container from being created.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-21 20:46:31 +01:00
Aleksei Sviridkin
d95ea930b6 [kamaji] Revert premature update to post-edge-26.2.4 (#2080)
## What this PR does

Reverts #2079 — the Kamaji update was merged prematurely while marked
with `do-not-merge` label. The upstream commit (`309d9889`) has not been
released as an edge tag yet.

### Release note
```release-note
NONE
```
2026-02-21 20:27:06 +03:00
Aleksei Sviridkin
8dbd6d5167 Revert "[kamaji] Update to 309d9889 (post edge-26.2.4), drop disable-datastore-check patch"
This reverts commit 2c372ae378.
2026-02-21 20:26:18 +03:00
Aleksei Sviridkin
2c372ae378 [kamaji] Update to 309d9889 (post edge-26.2.4), drop disable-datastore-check patch
Upstream PR clastix/kamaji#1087 (refactor!: datastore conditions) removed
the startup datastore existence check that our disable-datastore-check.diff
patch was working around. Update to the merge commit and drop the now
redundant patch.

Remaining patches:
- fix-kubelet-config-compat.diff (pending upstream PR #1084)
- increase-startup-probe-threshold.diff (no upstream fix)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-21 19:09:09 +03:00
Andrei Kvapil
02064888a4 feat(platform): make cluster issuer name and ACME solver configurable (#2077)
<!-- 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

Previously `_cluster.clusterissuer` controlled the ACME solver type
using
  values `http01` / `cloudflare`, and every ingress template hardcoded 
`cert-manager.io/cluster-issuer: letsencrypt-prod` with no way to
override it.

  This PR adds new parameters in platform chart:
  - `publishing.certificates.solver` (default `http01`)
  - `publishing.certificates.issuerName` (default: `letsencrypt-prod`)
  instead of single parameter before
  - `publishing.certificates.issuerType`
  
Previous `certificates.issuerType` was renamed to `certificates.solver`;
Also its possible value
`cloudflare` was renamed to `dns01` to use standard ACME terminology.

New `certificates.issuerName` (default: `letsencrypt-prod`) — propagated
as
`_cluster.issuer-name` to all packages via `cozystack-values` then its
value appears in
`cert-manager.io/cluster-issuer` annotation across 8 templates of
ingresses in system applications.

`publishing.certificates.solver` can be set empty to clearly support
`selfsigned-cluster-issuer`,
   or have any value, but it can be a bit confusing.

  Operators can now point ingresses at any ClusterIssuer (custom ACME,
  self-signed, internal CA) by setting `certificates.issuerName` without
  touching individual package templates.

  ## Breaking changes

  | What changed | Before | After |
  |---|---|---|
  | Solver key | `certificates.issuerType` | `certificates.solver` |
| Cloudflare solver value | `issuerType: cloudflare` | `solver: dns01` |

  This changes handled by migration when upgrading cozystack from v1 
  or by `migration-to-v1.0.sh` script (also checked by migration later)
  No actions from user needed.

### 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
[platform] Added publishing.certificates.solver (http01/dns01) and 
  publishing.certificates.issuerName fields to allow configuring ACME challenge 
  type and ClusterIssuer per installation, replacing the old implicit issuerType field
[platform] Migration script and upgrade hook (migration 32) convert old
  clusterissuer/issuerType fields to the new solver/issuerName fields
```

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

## Summary by CodeRabbit

* **Chores**
* Migrated certificate issuer configuration from legacy `issuerType`
field to new `solver` and `issuerName` fields system-wide.
* Automated migration script converts existing configurations, mapping
legacy values (cloudflare, http01) to new format.
* Updated all certificate-related templates to use new configurable
solver and issuer settings with sensible defaults.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-20 23:09:12 +01:00
Andrei Kvapil
4c3766a555 [system] Fix monitoring-agents FQDN resolution for tenant workload clusters (#2075)
## What this PR does

Monitoring agents (vmagent, fluent-bit) in tenant workload clusters
failed to deliver metrics and logs because service addresses used short
DNS names (e.g. `vminsert-longterm.tenant-root.svc`) without the cluster
domain suffix. Tenant CoreDNS could not resolve these names across
cluster boundaries.

This PR appends the configured cluster domain from
`_cluster.cluster-domain` to all vmagent remoteWrite URLs and fluent-bit
output hosts, with a fallback to `cluster.local` when not set.

### Release note

```release-note
[system] Fix monitoring-agents endpoints to use FQDN with configured cluster domain, resolving metrics and logs delivery failures in tenant workload clusters.
```
2026-02-20 20:41:25 +01:00
nbykov0
7bc93c5045 [kubevirt-operator] Update to v1.6.4
Co-authored-by: Andrei Kvapil <kvapss@gmail.com>
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-20 20:16:34 +01:00
nbykov0
d2f7c9ab82 [cdi-operator] Update to v1.64.0
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2026-02-20 20:16:33 +01:00
Andrei Kvapil
d856775961 feat(kubernetes): update supported versions to v1.30-v1.35 (#2073)
## What this PR does

Updates Kubernetes version support to match current release landscape
and Talos 1.12 compatibility:

- Update Kamaji from `edge-25.4.1` to `edge-26.2.4` (adds K8s 1.35
support)
- Update Kubernetes version matrix: v1.30, v1.31, v1.32, v1.33, v1.34,
v1.35
- Drop EOL versions v1.28 and v1.29
- Remove merged-upstream patch (992.diff — label preservation fix)
- Regenerate disable-datastore-check.diff for new Kamaji version

Changes:

- Default Kubernetes version is now v1.35
- E2E tests will validate v1.35 (latest) and v1.34 (previous)
- Patch versions updated to latest available (v1.35.0, v1.34.4, v1.33.8,
v1.32.12, v1.31.14, v1.30.14)

### Release note

```release-note
[kubernetes] Update supported Kubernetes versions to v1.30-v1.35
```


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

* **New Features**
* Added a Kamaji CRDs Helm chart with DataStore and KubeconfigGenerator
resources, plus deployment templates and configurable
kubeconfigGenerator settings
* DataStore now supports multiple backends (etcd, MySQL, PostgreSQL,
NATS) with TLS/auth validations and status tracking (observedGeneration)

* **Chores**
* Bumped default Kubernetes version from v1.33 to v1.35 (added v1.34;
removed v1.28–v1.29)
* Updated charts, packaging metadata, README/docs and helm
ignore/Makefile entries; updated builder base image and chart
dependencies
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-20 20:12:56 +01:00
Myasnikov Daniil
17c2ea0e9c feat(platform): Added migration to convert issuer configuration
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-20 17:03:23 +05:00
Myasnikov Daniil
c98b6203a7 fix(platform): fix migrate script to account clusterissuer parameter
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-20 16:41:41 +05:00
Aleksei Sviridkin
376e4d1fd3 [kamaji] Fix kubelet-config compatibility for Kubernetes < 1.35
Kamaji edge-26.2.4 is compiled against Kubernetes 1.35 libraries.
SetDefaults_KubeletConfiguration() from 1.35 injects two fields
gated by feature gates that are not enabled in earlier versions:
- crashLoopBackOff.maxContainerRestartPeriod (KubeletCrashLoopBackOffMax)
- imagePullCredentialsVerificationPolicy (KubeletEnsureSecretPulledImages)

Kubelets < 1.35 reject these fields during configuration validation,
causing worker nodes to fail to join the tenant cluster.

Add a Go patch that clears these fields from the kubelet-config
ConfigMap when the target Kubernetes version is below 1.35.

See: https://github.com/clastix/kamaji/issues/1062

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-20 12:55:11 +03:00
Andrei Kvapil
1c05999812 fix(cozystack-basics) Deny resourcequotas deletion for tenant admin (#2076)
<!-- 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
Fixed cozy:tenant:admin:base ClusterRole to deny deletion of tenant ResourceQuotas for the tenant admin and superadmin
```

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

## Summary by CodeRabbit

* **Bug Fixes**
* Removed resource quota management permissions from tenant admin role
to reduce unnecessary administrative access.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-20 10:24:32 +01:00
Myasnikov Daniil
356070615c feat(platform): Changed ingress annotation rendering for http01 solver
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-20 11:45:04 +05:00
Myasnikov Daniil
4aa1f03321 feat(platform): Added parameters to override ClusterIssuer
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-20 10:43:48 +05:00
Aleksei Sviridkin
8f1e52690d test(e2e): fix kubernetes-previous retry failures
- Kill stale port-forward processes before starting a new one;
  on retries, the previous attempt's port-forward still holds the
  port, causing all kubectl commands to get "connection refused"
- Use -ge 2 instead of -eq 2 for node count check; MachineHealthCheck
  may create a 3rd VM, leading to 3 nodes joining the tenant cluster
  which would never satisfy the exact equality check
- Increase node join timeout from 5m to 8m; QEMU VMs with v1.34 need
  more time to boot and join when running after kubernetes-latest

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-20 03:23:50 +03:00
Aleksei Sviridkin
00ab6e792c test(e2e): increase worker node join timeout to 5 minutes
When running kubernetes-latest and kubernetes-previous E2E tests
simultaneously, worker VMs compete for resources in the sandbox
environment. 3 minutes was insufficient for nodes to boot and
join the tenant cluster under load. Increase to 5 minutes.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-20 01:30:10 +03:00
Aleksei Sviridkin
3d89d3732c fix(kubernetes): pin konnectivity version for K8s v1.35
Kamaji auto-derives the konnectivity proxy image tag as v0.{minor}.0
from the Kubernetes version. For K8s v1.35, this produces v0.35.0,
but the kas-network-proxy/proxy-server:v0.35.0 image does not exist
in registry.k8s.io yet, causing ImagePullBackOff on new TCP pods.

Add konnectivity-versions.yaml mapping to explicitly override the
konnectivity version when the auto-derived tag is unavailable.
For v1.35, pin to v0.34.0 (latest available).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 23:46:50 +03:00
Aleksei Sviridkin
e39ba9fb8c fix(kubernetes): bump v1.35 patch version to v1.35.1
v1.35.1 was released on 2026-02-10.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 23:27:11 +03:00
Aleksei Sviridkin
5c5a170589 chore(kamaji): update Go builder image to 1.26
Go 1.26 was released on 2026-02-10 and is fully compatible with
Kamaji edge-26.2.4 (which requires go 1.25.0 in go.mod).
Verified by local build.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 23:27:05 +03:00
Andrei Kvapil
a6b498d7ec feat(dashboard) VMInstance dropdowns for disks and instanceType (#2071)
<!-- 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

- Add API-backed dropdown selects for VMInstance form: `instanceType`
fetches from `VirtualMachineClusterInstancetype` resources,
`disks[].name` fetches from `VMDisk`
resources in the same namespace
- Default value for `instanceType` is read dynamically from the
ApplicationDefinition's OpenAPI schema
- Fix a bug in the upstream `FormListInput` component where Ant Design's
`Form.Item` couldn't pass `value`/`onChange` to `Select` because of an
intermediate `Flex` wrapper

Details

The dashboard renders forms from OpenAPI schemas using the
openapi-k8s-toolkit library. To turn a plain text field into an
API-backed dropdown, the CustomFormsOverride resource's schema field is
used with type: "listInput" and customProps containing the API endpoint
URL.

  Controller changes (customformsoverride.go):
- applyListInputOverrides() — injects listInput schema overrides for
VMInstance kind
- parseOpenAPIProperties() — extracts top-level properties from OpenAPI
schema to read defaults
- ensureSchemaPath() / ensureArrayItemProps() — helpers to build nested
schema structures safely

  Frontend patch (formlistinput-value-binding.diff):
- Moves `<Flex>` outside `<ResetedFormItem>` so `<Select>` becomes the
direct child — required for Ant Design's `Form.Item` to inject
`value`/`onChange` via `React.cloneElement`


Instance type example:
<img width="1143" height="1091" alt="instance type example"
src="https://github.com/user-attachments/assets/6c401916-b531-4da6-ae27-ca54e6b0bd04"
/>
VMDisks example:

<img width="875" height="323" alt="vmdisks example"
src="https://github.com/user-attachments/assets/18918115-c08a-40bb-b932-536419d6f2c1"
/>


### 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
[vminstance] Add dropdowns for `instanceType` and `disk[].name` using `VirtualMachineClusterInstancetype` cluster resources and `VMDisk` from current namespace
```

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

## Summary by CodeRabbit

* **New Features**
* Custom form overrides now support auto-population of dropdown field
defaults from API schemas for enhanced user workflows.
* Improved layout and visual alignment of form list input controls for
better usability and responsiveness.

* **Tests**
* Added comprehensive test coverage for custom form override
functionality and API schema integration, including edge cases and
default value handling scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-19 20:43:42 +01:00
Aleksei Sviridkin
d18e6d1c24 fix(capi-provider): update Kamaji CAPI provider to v0.16.0
CAPI Kamaji provider v0.15.0 is incompatible with Kamaji edge-26.2.4
due to the new dataStoreUsername field with XValidation rule. The
provider's CreateOrUpdate drops the field (not in its Go types),
triggering "unsetting the dataStoreUsername is not supported" error.

This results in KamajiControlPlane staying INITIALIZED=false even
though the underlying TenantControlPlane reaches Ready.

v0.16.0 includes support for DataStoreUsername (PR #243 in v0.15.4)
and updated Kamaji types compatible with edge-26.2.4.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 22:12:33 +03:00
Andrei Kvapil
8162e3828e [linstor] Fix DRBD+LUKS+STORAGE resource creation failure (#2072)
## What this PR does

Adds the `skip-adjust-when-device-inaccessible.diff` patch (upstream
[LINBIT/linstor-server#477](https://github.com/LINBIT/linstor-server/pull/477))
which:

- Skips DRBD adjust and .res file regeneration when child layer devices
are inaccessible (fixes encrypted resource deletion)
- Skips lsblk when device path doesn't physically exist yet (fixes race
condition after drbdadm adjust)
- Only checks child devices when disk access is actually needed (allows
network reconnect from StandAlone)
- Fixes missing `setExists(true)` in `LuksLayer` — the root cause of all
new DRBD+LUKS+STORAGE resources failing with "not defined in your
config"

### Release note

```release-note
[linstor] Fix DRBD+LUKS+STORAGE resource creation: all new encrypted volumes were failing because the DRBD .res file was never written due to a missing exists flag in the LUKS layer
```

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of inaccessible storage devices by adding pre-checks
before performing operations
* Operations are now skipped when underlying storage devices are
unavailable, preventing unnecessary failures
* Enhanced error recovery during storage adjustments when devices are
temporarily inaccessible

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-19 19:15:58 +01:00
Andrei Kvapil
def8a5c835 refactor(installer): remove CRDs from Helm chart, delegate to operator --install-crds (#2074)
## What this PR does

Removes CRDs from the cozy-installer Helm chart `crds/` directory and
delegates
CRD lifecycle management entirely to the operator via `--install-crds`
flag.

The operator already applies embedded CRDs via server-side apply on
every startup,
making the Helm `crds/` directory redundant. Helm only installs CRDs on
initial
`helm install` and never updates or deletes them on upgrade/uninstall,
which causes
CRDs to become stale over time.

Changes:
- Remove `packages/core/installer/crds/` (Packages and PackageSources
CRDs)
- Remove `templates/packagesource.yaml` Helm template — PackageSource is
now
  created by the operator at startup using server-side apply
- Add `installPlatformPackageSource()` function to operator with SSA
- Move variant validation from deleted template to
`cozystack-operator.yaml`
- Simplify `update-codegen.sh` to use single CRD destination
- Update Makefile to source CRDs from `internal/crdinstall/manifests/`
- Update E2E tests to wait for operator-managed CRDs and PackageSource
- Add unit tests for PackageSource creation

### Release note

```release-note
[installer] CRDs are no longer shipped in the Helm chart crds/ directory. The operator now manages CRD lifecycle via --install-crds flag, ensuring CRDs stay up to date on every startup.
```


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

* **New Features**
* Operator now installs CRDs on startup and auto-creates the platform
PackageSource.

* **Improvements**
* Installer waits for CRDs to be established and for the platform
PackageSource to be present before proceeding.
* Deployment variant selection now has stricter validation to prevent
invalid choices.

* **Chores**
* Cleaned up legacy CRD templates and updated build/install scripting
paths.

* **Tests**
* Added comprehensive tests covering platform PackageSource installation
and URL/ref parsing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-19 18:52:34 +01:00
Aleksei Sviridkin
4e5455c72c fix(e2e): poll for CRD existence before waiting for Established condition
kubectl wait fails immediately with NotFound if the CRD does not exist
yet. The operator creates CRDs asynchronously on startup, so wrap the
wait in a retry loop that tolerates the initial absence.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 19:24:19 +03:00
Myasnikov Daniil
d4cb47b58b fix(cozystack-basics) deny resourcequotas deletion for tenant admin
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-19 20:43:19 +05:00
Aleksei Sviridkin
4843a617bc fix(operator): skip PackageSource on empty URL, add comprehensive tests
Guard PackageSource creation on platformSourceURL != "" to avoid
dangling SourceRef when no Flux source resource exists.

Add tests covering all new and modified functions:
- parsePlatformSourceURL (OCI, HTTPS, SSH, empty, unsupported scheme)
- parseRefSpec (single/multi values, whitespace, equals in value,
  missing equals, empty key/value, trailing comma)
- validateOCIRef / validateGitRef (valid keys, invalid keys, format)
- generateOCIRepository / generateGitRepository (with ref, no ref,
  invalid ref)
- installPlatformPackageSource variant valuesFiles validation
- installPlatformPackageSource custom name

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 18:31:49 +03:00
IvanHunters
0738fae56d fix(monitoring-agents): use FQDN with cluster domain for metrics and logs endpoints
Monitoring agents in tenant workload clusters failed to deliver metrics
and logs because service addresses used short DNS names without the
cluster domain suffix. Tenant CoreDNS could not resolve these names
across cluster boundaries.

Append the configured cluster domain from _cluster.cluster-domain to
all vmagent remoteWrite URLs and fluent-bit output hosts, falling back
to cluster.local when not set.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-02-19 18:30:20 +03:00
Aleksei Sviridkin
8b9a11360e fix(operator): skip PackageSource creation when platform source URL is empty
When platformSourceURL is empty no Flux source resource is created, so
creating a PackageSource that references it would leave a dangling
SourceRef in a permanent error state. Guard the creation block on
platformSourceURL != "".

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 18:29:55 +03:00
Aleksei Sviridkin
d0a6ddd782 refactor(operator): reduce variant duplication in installPlatformPackageSource
Replace repetitive Variant struct literals with a loop over variant
data, making it easier to add new variants and reducing copy-paste
errors.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 18:25:14 +03:00
Aleksei Sviridkin
8c6c69cdab fix(kamaji): update Go builder image to 1.25 and fix unused context import
Kamaji edge-26.2.4 requires Go >= 1.25.0, update base image accordingly.
Also remove unused "context" import from disable-datastore-check patch,
since removing the CheckExists call was the only usage of that package.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 18:13:41 +03:00
Aleksei Sviridkin
4821f025fc fix(operator): correct default platformSourceName, gate PackageSource on installCRDs
Change default --platform-source-name from "cozystack-packages" to
"cozystack-platform" to match the value passed by the Helm template.

Gate PackageSource creation on --install-crds flag: when the operator
does not manage CRDs, the PackageSource CRD may not exist yet, so
skip creation and let an external process handle it.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 18:04:50 +03:00
Aleksei Sviridkin
dbfdbc8298 fix(installer): check parsePlatformSourceURL error, wait for PackageSource in E2E
Explicitly check error from parsePlatformSourceURL instead of relying on
the implicit guarantee that installPlatformSourceResource already checked
it. This prevents latent bugs if startup order is ever restructured.

Add wait for platform PackageSource existence in E2E test before creating
Package resource, preventing flaky failures when operator startup is slow.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 17:57:45 +03:00
Aleksei Sviridkin
58e2b646be fix(operator): use SSA for PackageSource, separate context, unconditional creation
Switch installPlatformPackageSource to server-side apply (SSA) with
field manager, matching the pattern used in crdinstall. SSA is idempotent
and preserves metadata fields managed by other controllers.

Create PackageSource unconditionally (not only when platformSourceURL is
set), matching the previous Helm template behavior where PackageSource
was always created regardless of source URL configuration.

Use a dedicated context with its own 2-minute timeout for PackageSource
creation, separate from the platform source resource installation.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 17:54:33 +03:00
Aleksei Sviridkin
8450830f06 fix(installer): add CRD wait in E2E, unit tests for PackageSource creation
Add explicit CRD wait (kubectl wait crd --for=condition=Established) in
E2E test before creating Package resources, preventing race condition
between operator CRD installation and resource creation.

Add unit tests for installPlatformPackageSource covering create, update,
and GitRepository sourceRef kind scenarios.

Document that hardcoded variant list is an intentional design choice
matching the previous Helm template behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 17:50:09 +03:00
Aleksei Sviridkin
0e8b6515af fix(installer): handle parsePlatformSourceURL error, restore variant validation
Check error from parsePlatformSourceURL instead of discarding it, preventing
silent fallback to OCIRepository on invalid URLs.

Move variant validation from deleted packagesource.yaml to
cozystack-operator.yaml template so invalid cozystackOperator.variant
values still fail at helm template/install time.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 17:42:31 +03:00
Aleksei Sviridkin
655133b81c fix(installer): move PackageSource creation from Helm template to operator
Replace the Helm hook approach with programmatic PackageSource creation
in the operator startup sequence. Helm hooks are unsuitable for persistent
resources like PackageSource because before-hook-creation policy causes
cascade deletion of owned ArtifactGenerators during upgrades.

The operator now creates the platform PackageSource after installing CRDs
and the Flux source resource, using the same create-or-update pattern as
installPlatformSourceResource(). The sourceRef.kind is derived from the
platform source URL (OCIRepository for oci://, GitRepository for git).

Also fix stale comment in e2e test referencing deleted crds/ directory.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 17:38:23 +03:00
Aleksei Sviridkin
668ddc552e refactor(installer): remove CRDs from Helm chart, rely on operator --install-crds
Remove the crds/ directory from the cozy-installer Helm chart. The operator
already installs embedded CRDs via server-side apply on every startup with
the --install-crds=true flag, making the Helm crds/ directory redundant.

Convert templates/packagesource.yaml to a Helm post-install/post-upgrade
hook so it is applied after the operator has started and installed CRDs.

Update codegen to write CRDs only to internal/crdinstall/manifests/ (single
source of truth) and update the Makefile to source build assets from there.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 17:23:37 +03:00
Aleksei Sviridkin
7a5eb76b6a [kubernetes] Update supported versions to v1.30-v1.35
Update Kubernetes version matrix to match Talos 1.12 support range:
- Add v1.35.0 (latest) and v1.34.4
- Update existing patch versions (v1.33.8, v1.32.12)
- Drop EOL versions v1.28 and v1.29
- Set default version to v1.35

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 15:58:42 +03:00
Aleksei Sviridkin
a6e66a021a [kamaji] Update to edge-26.2.4 with Kubernetes 1.35 support
Update Kamaji from edge-25.4.1 to edge-26.2.4 which adds support for
Kubernetes 1.35 (KubeadmVersion bumped from v1.33.0 to v1.35.0).

- Update Dockerfile VERSION to edge-26.2.4
- Update vendored Helm charts from upstream
- Remove 992.diff patch (label preservation fix merged upstream)
- Regenerate disable-datastore-check.diff for new version

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 15:56:58 +03:00
Andrei Kvapil
6437abb35d fix(linstor): add skip-adjust patch and fix LUKS exists flag
Add skip-adjust-when-device-inaccessible.diff patch (upstream PR #477)
which prevents DRBD adjust and res file regeneration when child layer
devices are inaccessible (e.g. during encrypted resource deletion).

This patch also includes a fix for a missing setExists(true) call in
LuksLayer, which caused all new DRBD+LUKS+STORAGE resources to fail
with "not defined in your config" errors because the DRBD .res file
was never written.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-19 13:45:31 +01:00
Kirill Ilin
989686624c feat(dashboard): add VMDisk dropdown for VMInstance
Add API-backed listInput dropdown for disks[].name in VMInstance form,
listing available VMDisk resources from the same namespace.

Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-19 16:10:51 +05:00
Kirill Ilin
4387a3e95f fix(dashboard): patch FormListInput to fix value binding
The Flex wrapper between ResetedFormItem and Select prevented Ant
Design's Form.Item from injecting value/onChange into the Select,
causing the dropdown to appear empty even when the form store had a
value. Move Flex outside ResetedFormItem so Select is its direct child.

Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-19 15:58:18 +05:00
Kirill Ilin
db1425a8de feat(dashboard): add API-backed dropdown for VMInstance instanceType
Override spec.instanceType field with listInput type in schema so the
dashboard renders it as a select dropdown populated from
VirtualMachineClusterInstancetype resources. Default value is read
dynamically from the ApplicationDefinition's OpenAPI schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-19 14:33:56 +05:00
Andrei Kvapil
16db457536 fix(cozystack-basics) Preserve existing HelmRelease values during reconciliations (#2068)
<!-- 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

Bug: Changes to Tenant `tenant-root` will be dropped on next force (or
upgrade) reconciliation of `cosystack-basics` HelmRelease. This may lead
to data loss and outage of service

This PR fixes such behavior preserving values, applied by the user

### 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-basics] Preserve existing `HelmRelease` values of `tenant-root` during reconciliations
```

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

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed cluster configuration to preserve existing settings during
updates instead of overwriting them. The system now properly merges new
configuration with prior values, ensuring no settings are unexpectedly
lost.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-19 07:48:17 +01:00
Kirill Ilin
b5b2f95c3e [cozystack-basics] Preserve existing HelmRelease values during reconciliations
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-18 21:52:51 +05:00
Andrei Kvapil
7ff5b2ba23 [harbor] Add managed Harbor container registry (#2058)
## What this PR does

Adds Harbor v2.14.2 as a managed tenant-level container registry service
in the PaaS bundle.

**Architecture:**

- Wrapper chart (`apps/harbor`) — HelmRelease, Ingress,
WorkloadMonitors, BucketClaim, dashboard RBAC
- Vendored upstream chart (`system/harbor`) from helm.goharbor.io
v1.18.2
- System chart (`system/harbor`) provisions PostgreSQL via CloudNativePG
and Redis via redis-operator
- ApplicationDefinition (`system/harbor-rd`) for dynamic `Harbor` CRD
registration
- PackageSource and paas.yaml bundle entry for platform integration

**Key design decisions:**

- Database and Redis provisioned via CPNG and redis-operator (not
internal Helm-based instances) for reliable day-2 operations
- Registry image storage uses S3 via COSI BucketClaim/BucketAccess from
namespace SeaweedFS
- Trivy vulnerability scanner cache uses PVC (S3 not supported by
vendored chart)
- Token CA key/cert persisted across upgrades via Secret lookup
- Per-component resource configuration (core, registry, jobservice,
trivy)
- Ingress with TLS via cert-manager, cloudflare issuer type handling,
proxy timeouts for large image pushes
- Auto-generated admin credentials persisted across upgrades

**E2E test:** Creates Harbor instance, verifies HelmRelease readiness,
deployment availability, credentials secret, service port, then cleans
up.

### Release note

```release-note
[harbor] Add managed Harbor container registry as a tenant-level service
```

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

## Summary by CodeRabbit

* **New Features**
* Added Harbor container registry deployment with integrated Kubernetes
support, including database and cache layers.
  * Enabled metrics monitoring via Prometheus integration.
  * Configured dashboard management interface for Harbor administration.

* **Tests**
  * Added end-to-end testing for Harbor deployment and verification.

* **Chores**
  * Integrated Harbor into the platform's application package bundle.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-18 13:54:26 +01:00
Aleksei Sviridkin
199ffe319a fix(harbor): set UTF-8 encoding and locale for CNPG database
Add encoding, localeCollate, and localeCType to initdb bootstrap
configuration to ensure fulltext search (ilike) works correctly.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 11:26:58 +03:00
Andrei Kvapil
681b2cef54 docs: add changelog for v1.0.0-beta.6 (#2067)
This PR adds the changelog for release `v1.0.0-beta.6`.

 Changelog has been automatically generated in
`docs/changelogs/v1.0.0-beta.6.md`.

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

## Summary by CodeRabbit

* **New Features**
  * Cilium-Kilo networking variant for enhanced multi-location setup
  * NATS monitoring dashboards with Prometheus integration
  * DNS validation for Application names
  * Operator auto-installation of CRDs at startup

* **Bug Fixes**
  * Fixed HelmRelease adoption during platform migration
  * Improved test reliability and timeout handling

* **Documentation**
  * Enhanced Azure autoscaling troubleshooting guidance
  * Updated multi-location configuration docs

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-18 00:29:45 +01:00
Andrei Kvapil
5aa53d14c8 Release v1.0.0-beta.6 (#2066)
This PR prepares the release `v1.0.0-beta.6`.

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

## Summary by CodeRabbit

* **Chores**
* Updated container image references from v1.0.0-beta.5 to v1.0.0-beta.6
across the platform, including updates to operators, controllers,
dashboards, storage components, and other services with corresponding
digest updates.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-18 00:28:50 +01:00
Aleksei Sviridkin
daf1b71e7c fix(harbor): add explicit CNPG bootstrap configuration
Specify initdb bootstrap with database and owner names explicitly
instead of relying on CNPG defaults which may change between versions.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 02:23:00 +03:00
Aleksei Sviridkin
0198c9896a fix(harbor): use standard hostname pattern without double prefix
Follow the same hostname pattern as kubernetes and vpn apps: use
Release.Name directly (which already includes the harbor- prefix)
instead of adding an extra harbor. subdomain.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 02:17:59 +03:00
Aleksei Sviridkin
78f8ee2deb fix(harbor): enable database TLS and fix token key/cert check
Change sslmode from disable to require for CNPG PostgreSQL connection,
as CNPG supports TLS out of the box. Fix token key/cert preservation to
verify both values are present before passing to Harbor core.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 02:12:11 +03:00
Aleksei Sviridkin
87d0390256 fix(harbor): include tenant domain in default hostname and add E2E cleanup
Use tenant base domain in default hostname construction (harbor.RELEASE.DOMAIN)
to match the pattern used by other apps (kubernetes, vpn). Remove unused $ingress
variable from harbor.yaml. Add cleanup of stale resources from previous failed
E2E runs.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 02:07:38 +03:00
cozystack-bot
96467cdefd docs: add changelog for v1.0.0-beta.6
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-17 22:44:01 +00:00
cozystack-bot
bff5468b52 Prepare release v1.0.0-beta.6
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-17 22:36:40 +00:00
Andrei Kvapil
3b267d6882 refactor(e2e): use helm install instead of kubectl apply for cozystack installation (#2060)
## Summary

- Replace pre-rendered static YAML application (`kubectl apply`) with
direct `helm upgrade --install` of the `packages/core/installer` chart
in E2E tests
- Remove CRD/operator artifact upload/download from CI workflow — the
chart with correct values is already present in the sandbox via
workspace copy and `pr.patch`
- Remove `copy-installer-manifest` Makefile target and its dependencies

## Test plan

- [ ] CI build job completes without uploading CRD/operator artifacts
- [ ] E2E `install-cozystack` step succeeds with `helm upgrade
--install`
- [ ] All existing E2E app tests pass

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

* **Chores**
* PR workflows now only keep the primary disk asset; publishing/fetching
of auxiliary operator and CRD artifacts removed.
* CRD manifests are produced by concatenation and a verify-crds check
was added to unit tests; file-write permissions for embedded manifests
tightened.

* **New Features**
* Operator can install CRDs at startup to ensure resources exist before
reconcile.
  * E2E install now uses the chart-based installer flow.

* **Tests**
* Added comprehensive tests for CRD-install handling and manifest
writing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-17 23:31:08 +01:00
Aleksei Sviridkin
e7ffc21743 feat(harbor): switch registry storage to S3 via COSI BucketClaim
Replace PVC-based registry storage with S3 via COSI BucketClaim/BucketAccess.
The system chart parses BucketInfo secret and creates a registry-s3 Secret
with REGISTRY_STORAGE_S3_* env vars that override Harbor's ConfigMap values.

- Add bucket-secret.yaml to system chart (BucketInfo parser)
- Remove storageType/size from registry config (S3 is now the only option)
- Use Harbor's existingSecret support for S3 credentials injection
- Add objectstorage-controller to PackageSource dependencies
- Update E2E test with COSI bucket provisioning waits and diagnostics

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 01:23:02 +03:00
Aleksei Sviridkin
5bafdfd453 ci: trigger workflow re-run
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:59:10 +03:00
Andrei Kvapil
3e1981bc24 Add monitoring for NATs (#1381)
<!-- 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
[nats] add monitoring
```

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

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Added Grafana dashboards for NATS JetStream and Server monitoring
* Added support for specifying container image digests and full image
names

* **Documentation**
* Enhanced NATS Helm chart documentation with container resource
configuration guidance

* **Chores**
  * Updated NATS application version and component image versions
* Improved Kubernetes graceful shutdown and Prometheus exporter
configuration

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-17 22:54:55 +01:00
kklinch0
7ac989923d Add monitoring for NATs
Co-authored-by: Andrei Kvapil <kvapss@gmail.com>
Signed-off-by: kklinch0 <kklinch0@gmail.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-17 22:54:12 +01:00
Andrei Kvapil
affd91dd41 fix(platform): adopt tenant-root into cozystack-basics during migration (#2065)
## What this PR does

Adds migration 31 to adopt existing `tenant-root` Namespace and
HelmRelease
into the `cozystack-basics` Helm release during upgrade from v0.41.x to
v1.0.

In v0.41.x these resources were applied via `kubectl apply` (from the
platform
chart `apps.yaml`) with no Helm release tracking. In v1.0 they are
created by
the `cozystack-basics` chart. Without Helm ownership annotations, `helm
install`
of `cozystack-basics` fails because the resources already exist.

The migration adds:
- `meta.helm.sh/release-name` and `meta.helm.sh/release-namespace`
annotations
  for Helm adoption
- `app.kubernetes.io/managed-by: Helm` label
- `helm.sh/resource-policy: keep` annotation on the HelmRelease to
prevent
  accidental deletion
- `sharding.fluxcd.io/key: tenants` label required by flux sharding in
v1.0

This follows the same pattern as migrations 22 and 27 (CRD adoption).

Related: #2063

### Release note

```release-note
[platform] adopt tenant-root resources into cozystack-basics Helm release during migration
```
2026-02-17 22:52:46 +01:00
Andrei Kvapil
26178d97be fix(platform): adopt tenant-root into cozystack-basics during migration
In v0.41.x the tenant-root Namespace and HelmRelease were applied via
kubectl apply with no Helm release tracking. In v1.0 these resources
are managed by the cozystack-basics Helm release. Without proper Helm
ownership annotations the install of cozystack-basics fails because
the resources already exist.

Add migration 31 that annotates and labels both the Namespace and
HelmRelease so Helm can adopt them, matching the pattern established
in migrations 22 and 27.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-17 22:51:00 +01:00
Aleksei Sviridkin
efb9bc70b3 fix(harbor): use Release.Name for default host to avoid conflicts
Multiple Harbor instances in the same namespace would get the same
default hostname when derived from namespace host. Use Release.Name
instead for unique hostnames per instance.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:13 +03:00
Aleksei Sviridkin
0e6ae28bb8 fix(harbor): resolve Redis nil pointer on first install
The vendored Harbor chart does an unsafe `lookup` of the Redis auth
Secret at template rendering time to extract the password. On first
install, the Secret doesn't exist yet (created by the same chart),
causing a nil pointer error. Failed installs are rolled back, deleting
the Secret, so retries also fail — creating an infinite failure loop.

Fix by generating the Redis password in the wrapper chart (same
pattern as admin password), storing it in the credentials Secret,
and injecting it via HelmRelease valuesFrom with targetPath. This
bypasses the vendored chart's lookup entirely — it uses the password
value directly instead of looking up the Secret.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:12 +03:00
Aleksei Sviridkin
0f2ba5aba2 fix(harbor): add diagnostic output on E2E system HelmRelease timeout
Dump HelmRelease status, pods, events, and ExternalArtifact info
when harbor-test-system fails to become ready, to diagnose the
root cause of the persistent timeout.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:12 +03:00
Aleksei Sviridkin
490faaf292 fix(harbor): add operator dependencies, fix persistence rendering, increase E2E timeout
Add postgres-operator and redis-operator to PackageSource dependsOn
to ensure CRDs are available before Harbor system chart deploys.

Make persistentVolumeClaim conditional to avoid empty YAML mapping
when using S3 storage without Trivy.

Increase E2E system HelmRelease timeout from 300s to 600s to account
for CPNG + Redis + Harbor bootstrap time on QEMU.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:12 +03:00
Aleksei Sviridkin
cea57f62c8 [harbor] Make registry storage configurable: S3 or PVC
Add registry.storageType parameter (pvc/s3) to let users choose
between PVC storage and S3 via COSI BucketClaim. Default is pvc,
which works without SeaweedFS in the tenant namespace.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:12 +03:00
Aleksei Sviridkin
c815725bcf [harbor] Fix E2E test: use correct HelmRelease name with prefix
ApplicationDefinition has prefix "harbor-", so CR name "harbor" produces
HelmRelease "harbor-harbor". Use name="test" and release="harbor-test"
to correctly reference all resources.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:12 +03:00
Aleksei Sviridkin
6c447b2fcb [harbor] Improve ingress template: quote hosts, handle cloudflare issuer
Add | quote to host values in ingress for proper YAML escaping.
Add cloudflare issuer type handling following bucket/dashboard pattern.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:12 +03:00
Aleksei Sviridkin
0c85639fed [harbor] Move to apps/, use S3 via BucketClaim for registry storage
Move Harbor from packages/extra/ to packages/apps/ as it is a
self-sufficient end-user application, not a singleton tenant module.
Update bundle entry from system to paas accordingly.

Replace registry PVC storage with S3 via COSI BucketClaim/BucketAccess,
provisioned from the namespace's SeaweedFS instance. S3 credentials are
injected into the HelmRelease via valuesFrom with targetPath.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:12 +03:00
Aleksei Sviridkin
2dd3c03279 [harbor] Use CPNG and redis-operator instead of internal databases
Replace Harbor's internal PostgreSQL with CloudNativePG operator and
internal Redis with redis-operator (RedisFailover), following established
Cozystack patterns from seaweedfs and redis apps.

Additional fixes from code review:
- Fix registry resources nesting level (registry.registry/controller)
- Persist token CA across upgrades to prevent JWT invalidation
- Update values schema and ApplicationDefinition

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:11 +03:00
Aleksei Sviridkin
305495d023 [harbor] Fix YAML quoting for Go template values in ApplicationDefinition
Quote resourceNames values starting with {{ to prevent YAML parser
from interpreting them as flow mappings.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:11 +03:00
Aleksei Sviridkin
543ce6e5fd [harbor] Add managed Harbor container registry application
Add Harbor v2.14.2 as a tenant-level managed service with per-component
resource configuration, ingress with TLS termination, and internal
PostgreSQL/Redis.

Includes:
- extra/harbor wrapper chart with HelmRelease, WorkloadMonitors, Ingress
- system/harbor with vendored upstream chart (helm.goharbor.io v1.18.2)
- harbor-rd ApplicationDefinition for dynamic CRD registration
- PackageSource and system.yaml bundle entry
- E2E test with Secret and Service verification

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:50:11 +03:00
Aleksei Sviridkin
09805ff382 fix(manifestutil): check apiVersion in CollectCRDNames for consistent GVK matching
CollectCRDNames now requires both apiVersion "apiextensions.k8s.io/v1"
and kind "CustomResourceDefinition", consistent with the validation in
crdinstall.Install.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:57 +03:00
Aleksei Sviridkin
92d261fc1e fix: address review findings in operator and tests
- Remove duplicate "Starting controller manager" log before install
  phases, keep only the one before mgr.Start()
- Rename misleading test "document without kind returns error" to
  "decoder rejects document without kind" to match actual behavior
- Document Helm uninstall CRD behavior in deployment template comment
- Use --health-probe-bind-address=0 consistently with metrics-bind
- Exclude all dotfiles in verify-crds diff, not just .gitattributes

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:57 +03:00
Aleksei Sviridkin
9eb13fdafe fix(controller): update workload test to use current label name
The workload reconciler was refactored to use the label
workloads.cozystack.io/monitor but the test still used the old
workloadmonitor.cozystack.io/name label, causing the reconciler to
delete the workload instead of keeping it.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:57 +03:00
Aleksei Sviridkin
cecc5861af fix(operator): validate CRD apiVersion, respect SIGTERM during install
- Check both apiVersion and kind when validating embedded CRD manifests
  to prevent applying objects with wrong API group
- Move ctrl.SetupSignalHandler() before install phases so CRD and Flux
  installs respect SIGTERM instead of blocking for up to 2 minutes
- Replace custom contains/searchString helpers with strings.Contains

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:56 +03:00
Aleksei Sviridkin
abd644122f fix(crdinstall): reject non-CRD objects in embedded manifests
Validate that all parsed objects are CustomResourceDefinition before
applying with force server-side apply. This prevents accidental
application of arbitrary resources if a non-CRD file is placed in
the manifests directory.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:56 +03:00
Aleksei Sviridkin
3fbce0dba5 refactor(operator): extract shared manifest utils from crdinstall and fluxinstall
Move duplicated YAML parsing (ReadYAMLObjects, ParseManifestFile) and
CRD readiness check (WaitForCRDsEstablished, CollectCRDNames) into a
shared internal/manifestutil package. Both crdinstall and fluxinstall
now import from manifestutil instead of maintaining identical copies.

Replace fluxinstall's time.Sleep(2s) after CRD apply with proper
WaitForCRDsEstablished polling, matching the crdinstall behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:56 +03:00
Aleksei Sviridkin
b3fe6a8c4a fix(crdinstall): hardcode CRD GVK, add timeout test, document dual install
- Use explicit apiextensions.k8s.io/v1 CRD GVK in waitForCRDsEstablished
  instead of fragile objects[0].GroupVersionKind()
- Add TestInstall_crdNotEstablished for context timeout path
- Add --recursive to diff in verify-crds Makefile target
- Document why both crds/ and --install-crds exist in deployment template

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:56 +03:00
Aleksei Sviridkin
962f8e96f4 ci(makefile): add CRD sync verification between Helm crds/ and operator embed
Add verify-crds target that diffs packages/core/installer/crds/ and
internal/crdinstall/manifests/ to catch accidental divergence. Include
it in unit-tests target so it runs in CI.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:56 +03:00
Aleksei Sviridkin
20d122445d fix(crdinstall): add CRD readiness check, Install tests, fix fluxinstall
- Wait for CRDs to have Established condition after server-side apply,
  instead of returning immediately
- Add TestInstall with fake client and interceptor to simulate CRD
  establishment
- Add TestInstall_noManifests and TestInstall_writeManifestsFails for
  error paths
- Fix fluxinstall/manifests.embed.go: use filepath.Join for OS paths
  and restrict permissions from 0666 to 0600 (same fix as crdinstall)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:56 +03:00
Aleksei Sviridkin
4187b5ed94 fix(crdinstall): use filepath for OS paths, restrict permissions, add tests
- Use filepath.Join instead of path.Join for OS file paths
- Restrict extracted manifest permissions from 0666 to 0600
- Add unit tests for readYAMLObjects, parseManifests, and
  WriteEmbeddedManifests including permission verification

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:56 +03:00
Aleksei Sviridkin
1558fb428a build(codegen): sync CRDs to operator embed directory
After generating CRDs to packages/core/installer/crds/, copy them to
internal/crdinstall/manifests/ so the operator binary embeds the latest
CRD definitions.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:56 +03:00
Aleksei Sviridkin
879b10b777 feat(installer): enable --install-crds in operator deployment
Add --install-crds=true to cozystack-operator container args so the
operator applies embedded CRD manifests on startup via server-side apply.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:55 +03:00
Aleksei Sviridkin
1ddbe68bc2 feat(operator): add --install-crds flag with embedded CRD manifests
Embed Package and PackageSource CRDs in the operator binary using Go
embed, following the same pattern as --install-flux. The operator applies
CRDs at startup using server-side apply, ensuring they are updated on
every operator restart/upgrade.

This addresses the CRD lifecycle concern: Helm crds/ directory handles
initial install, while the operator manages updates on subsequent
deployments.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:55 +03:00
Aleksei Sviridkin
d8dd5adbe0 fix(testing): remove broken test-cluster target
The test-cluster target references non-existent hack/e2e-cluster.bats
file. Remove it and its dependency from the test target.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:55 +03:00
Aleksei Sviridkin
55cd8fc0e1 refactor(installer): move CRDs to crds/ directory for proper Helm install ordering
Helm installs crds/ contents before processing templates, resolving the
chicken-and-egg problem where PackageSource CR validation fails because
its CRD hasn't been registered yet.

- Move definitions/ to crds/ in the installer chart
- Remove templates/crds.yaml (Helm auto-installs from crds/)
- Update codegen script to write CRDs to crds/
- Replace helm template with cat for static CRD manifest generation
- Remove pre-apply CRD workaround from e2e test

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:55 +03:00
Aleksei Sviridkin
58dfc97201 fix(e2e): apply CRDs before helm install to resolve dependency ordering
Helm cannot validate PackageSource CR during install because the CRD
is part of the same chart. Pre-apply CRDs via helm template + kubectl
apply --server-side before running helm upgrade --install.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:55 +03:00
Aleksei Sviridkin
153d2c48ae refactor(e2e): use helm install instead of kubectl apply for cozystack installation
Replace pre-rendered static YAML application with direct helm chart
installation in e2e tests. The chart directory with correct values is
already present in the sandbox after pr.patch application.

- Remove CRD/operator artifact upload/download from CI workflow
- Remove copy-installer-manifest target from testing Makefile
- Use helm upgrade --install from local chart in e2e-install-cozystack.bats

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:55 +03:00
Andrei Kvapil
39b95107a5 feat(platform): add cilium-kilo networking variant (#2064)
## Summary

Add a new `cilium-kilo` networking variant that combines Cilium as the
CNI with Kilo as the WireGuard mesh overlay. This replaces the
standalone kilo PackageSource with a unified variant under the
networking source.

## Changes

- Add `cilium-kilo` variant to `networking.yaml` PackageSource with
proper component ordering and dependencies
- Add `values-kilo.yaml` for Cilium to disable host firewall when used
with Kilo
- Remove standalone `kilo.yaml` PackageSource (now integrated into
networking source)
- Switch Kilo image to official
`ghcr.io/cozystack/cozystack/kilo:v0.8.2`
- Remove unused `podCIDR`/`serviceCIDR` options and `--service-cidr`
flag from Kilo chart
2026-02-17 22:18:06 +01:00
Andrei Kvapil
b3b7307105 fix(kilo): use official kilo image and clean up cilium-kilo config
Switch kilo image to official ghcr.io/cozystack/cozystack/kilo:v0.8.2,
remove unnecessary enable-ipip-termination from cilium-kilo values,
and update platform source digest.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-17 22:17:38 +01:00
Andrei Kvapil
bf1e49d34b fix(kilo): remove podCIDR passed as --service-cidr
The podCIDR was incorrectly passed as --service-cidr to prevent
masquerading on pod traffic. This is unnecessary for multi-location
mesh and was a leftover from single-cluster assumptions.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-17 20:16:01 +01:00
Andrei Kvapil
96ba3b9ca5 fix(kilo): remove service-cidr option from chart
The --service-cidr flag prevents masquerading for service IPs, but
service CIDRs are cluster-local and not useful for multi-location
mesh routing. Remove the serviceCIDR value and its template usage.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-17 20:14:34 +01:00
Andrei Kvapil
536766cffc feat(platform): add cilium-kilo networking variant
Add a new networking variant that integrates Kilo with Cilium
pre-configured. Cilium is deployed with host firewall disabled and
enable-ipip-termination enabled, which are required for correct IPIP
encapsulation through Cilium's overlay.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-17 19:32:19 +01:00
Andrei Kvapil
8cc8e52d15 chore(platform): remove standalone kilo PackageSource
Kilo is now integrated into the cilium-kilo networking variant instead
of being a separate package that users install manually.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-17 19:31:57 +01:00
Aleksei Sviridkin
dd4723386f test(openbao): add E2E test for standalone mode
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-17 21:23:27 +03:00
Andrei Kvapil
6c431d0857 fix(codegen): add gen_client to update-codegen.sh and regenerate applyconfiguration (#2061)
## What this PR does

Fix build error in `pkg/generated/applyconfiguration/utils.go` caused by
a reference to `testing.TypeConverter` which was removed in client-go
v0.34.1.

The root cause was that `hack/update-codegen.sh` called `gen_helpers`
and
`gen_openapi` but never called `gen_client`, so the applyconfiguration
code
was never regenerated after the client-go upgrade.

Changes:
- Fix `THIS_PKG` from `k8s.io/sample-apiserver` template leftover to
correct module path
- Add `kube::codegen::gen_client` call with `--with-applyconfig` flag
- Regenerate applyconfiguration (now uses `managedfields.TypeConverter`)
- Add tests for `ForKind` and `NewTypeConverter` functions

### Release note

```release-note
[maintenance] Regenerate applyconfiguration code for client-go v0.34.1 compatibility
```


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

## Summary by CodeRabbit

* **Documentation**
* Updated backup class definitions example to reference MariaDB instead
of MySQL.

* **Chores**
* Updated code generation tooling and module dependencies to support
enhanced functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-17 18:21:39 +01:00
Andrei Kvapil
c6090c554c fix(e2e): make kubernetes test retries effective by cleaning up stale resources (#2062)
## Summary

- Add pre-creation cleanup of backend deployment/service and NFS pod/PVC
in `run-kubernetes.sh`, so E2E test retries start fresh instead of
reusing stuck resources from a failed attempt
- Increase the tenant deployment wait timeout from 90s to 300s to handle
CI resource pressure, aligning with other timeouts in the same function
(control plane 4m, TCP 5m, NFS pod 5m)

## Context

When `kubernetes-previous` (or `kubernetes-latest`) E2E test fails at
the `kubectl wait deployment --timeout=90s` step, `set -eu` causes
immediate exit before cleanup. On retry, `kubectl apply` sees the stuck
deployment as "unchanged", making retries 2 and 3 guaranteed to fail
against the same stuck pod.

This was observed in [run
22076291555](https://github.com/cozystack/cozystack/actions/runs/22076291555)
where `kubernetes-previous` failed 3/3 attempts while passing in 5/6
other recent runs.

## Test plan

- [ ] E2E tests pass (kubernetes-latest and kubernetes-previous)
- [ ] First clean run: `--ignore-not-found` cleanup commands are no-ops,
no side effects
- [ ] Retry after failure: stale deployment is deleted before
re-creation, fresh pod is scheduled

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

* **Chores**
* Improved test deployment reliability with more robust pre/post-test
resource cleanup (including storage-related resources) to prevent
leftovers from prior runs.
* Made environment configuration handling more consistent to avoid
misreads of cluster credentials.
* Increased initialization and load balancer wait timeouts to reduce
flaky failures during provisioning and validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-17 18:03:10 +01:00
Aleksei Sviridkin
088bc0ffe2 feat(openbao): add resource definition, PackageSource, and PaaS bundle entry
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-17 20:03:02 +03:00
Aleksei Sviridkin
da59efec21 feat(openbao): add application chart with standalone and HA modes
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-17 20:03:01 +03:00
Aleksei Sviridkin
32aff887eb feat(openbao): add system chart with vendored upstream
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-17 20:03:01 +03:00
Aleksei Sviridkin
8b7813fdeb [platform] Add DNS-1035 validation for Application names (#1771)
## What this PR does

Add DNS-1035 validation for Application names to prevent creation of
resources with invalid names that would fail Kubernetes resource
creation.

### Changes

- Add `ValidateApplicationName()` function using standard
`IsDNS1035Label` from `k8s.io/apimachinery`
- Call validation in REST API `Create()` method (skipped on `Update` —
names are immutable)
- Add name length validation: `prefix + name` must fit within Helm
release name limit (53 chars)
- Remove rootHost-based length validation — the underlying label limit
issue is tracked in #2002
- Remove `parseRootHostFromSecret` and `cozystack-values` Secret reading
at API server startup
- Add unit tests for both DNS-1035 format and length validation

### DNS-1035 Requirements

- Start with a lowercase letter `[a-z]`
- Contain only lowercase alphanumeric or hyphens `[-a-z0-9]`
- End with an alphanumeric character `[a-z0-9]`

Fixes #1538
Closes #2001

### Release note

```release-note
[platform] Add DNS-1035 validation for Application names to prevent invalid tenant names
```

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

## Summary by CodeRabbit

* **New Features**
* Enforced DNS-1035 name format and Helm-compatible length limits for
applications during creation; name constraints surfaced early and
reflected in API validation.

* **Documentation**
* Updated tenant naming rules to DNS-1035, clarified hyphen guidance,
and noted maximum length considerations.

* **Tests**
* Added comprehensive tests covering format and length validation,
including many invalid and boundary cases.

* **API**
* OpenAPI/Swagger schemas updated to include name pattern and max-length
validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-17 14:33:02 +03:00
Aleksei Sviridkin
a52da8dd8d style(e2e): consistently quote kubeconfig variable references
Quote all tenantkubeconfig-${test_name} references in run-kubernetes.sh
for consistent shell scripting style. The only exception is line 195
inside a sh -ec "..." double-quoted string where inner quotes would
break the outer quoting.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-17 02:00:44 +03:00
Aleksei Sviridkin
315e5dc0bd fix(e2e): make kubernetes test retries effective by cleaning up stale resources
When the kubernetes E2E test fails at the deployment wait step, set -eu
causes immediate exit before cleanup. On retry, kubectl apply outputs
"unchanged" for the stuck deployment, making retries 2 and 3 guaranteed
to fail against the same stuck pod.

Add pre-creation cleanup of backend deployment/service and NFS test
resources using --ignore-not-found, so retries start fresh. Also
increase the deployment wait timeout from 90s to 300s to handle CI
resource pressure, aligning with other timeouts in the same function.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-17 01:58:13 +03:00
Aleksei Sviridkin
75e25fa977 fix(codegen): add gen_client to update-codegen.sh and regenerate applyconfiguration
The applyconfiguration code referenced testing.TypeConverter from
k8s.io/client-go/testing, which was removed in client-go v0.34.1.

Root cause: hack/update-codegen.sh called gen_helpers and gen_openapi
but not gen_client, so applyconfiguration was never regenerated after
the client-go upgrade.

Changes:
- Fix THIS_PKG from sample-apiserver template leftover to correct
  module path
- Add kube::codegen::gen_client call with --with-applyconfig flag
- Regenerate applyconfiguration (now uses managedfields.TypeConverter)
- Add tests for ForKind and NewTypeConverter functions

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-16 23:01:38 +03:00
Aleksei Sviridkin
73b8946a7e chore(codegen): regenerate stale deepcopy and CRD definitions
Run make generate to bring generated files up to date with current
API types. This was pre-existing staleness unrelated to any code change.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-16 23:01:22 +03:00
Andrei Kvapil
f131eb109a Release v1.0.0-beta.5 (#2056)
This PR prepares the release `v1.0.0-beta.5`.

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

## Summary by CodeRabbit

* **Chores**
* Updated all platform and system components to v1.0.0-beta.5, including
core services, controllers, dashboard, and networking utilities. Changes
include refreshed container image references across installer, platform
migrations, API services, backup systems, and object storage components
for improved stability and consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-16 17:54:52 +01:00
Andrei Kvapil
961da56e96 docs: add changelog for v1.0.0-beta.5 (#2057)
This PR adds the changelog for release `v1.0.0-beta.5`.

 Changelog has been automatically generated in
`docs/changelogs/v1.0.0-beta.5.md`.

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

## Summary by CodeRabbit

* **New Features**
  * Added generic Kubernetes deployment support
  * Added Cilium-compatible kilo variant

* **Improvements**
  * Enhanced cluster autoscaler with enforced node group minimum sizes
  * Upgraded dashboard to version 1.4.0

* **Breaking Changes**
* Updated VPC subnet configuration structure; automated migration
provided

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-16 17:54:36 +01:00
cozystack-bot
bfba9fb5e7 docs: add changelog for v1.0.0-beta.5
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-16 16:08:18 +00:00
cozystack-bot
bae70596fc Prepare release v1.0.0-beta.5
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-02-16 16:01:30 +00:00
Andrei Kvapil
84b2fa90dd feat(installer): add variant-aware templates for generic Kubernetes support (#2010)
## Summary

- Extend the installer chart to support generic and hosted Kubernetes
deployments via the existing `cozystackOperator.variant` parameter
(introduced in #2034)
- For `variant=generic`: render ConfigMaps (`cozystack`,
`cozystack-operator-config`) and an optional Platform Package CR —
resources previously required to be created manually before deploying on
non-Talos clusters
- Add variant validation in `packagesource.yaml` to fail fast on typos
- Publish the installer chart as an OCI Helm artifact

## Motivation

Deploying Cozystack on generic Kubernetes (k3s, kubeadm, RKE2) currently
requires manually creating ConfigMaps before applying the rendered
operator manifest. This change makes the installer chart variant-aware
so that:

1. `helm template -s` workflow continues to produce correct rendered
manifests
2. `helm install --set cozystackOperator.variant=generic` becomes a
viable single-command deployment path for generic clusters
3. Required ConfigMaps and optional Platform Package CR are generated
from values, eliminating manual steps

## OCI Helm chart publishing

The installer chart is now packaged and pushed to the OCI registry as
part of the `image` build target via `make chart`. A `.helmignore` file
ensures only chart-relevant files are included in the published
artifact.

## Test plan

- [ ] `helm template` with `variant=talos` (default) renders: operator +
PackageSource
- [ ] `helm template` with `variant=generic` renders: operator + 2
ConfigMaps + PackageSource
- [ ] `helm template` with `variant=generic` + `platform.enabled=true`
renders: + Package CR
- [ ] `helm template` with `variant=hosted` renders: operator +
PackageSource
- [ ] Invalid variant value produces a clear error message
- [ ] `make manifests` generates all asset files
- [ ] `helm package` produces a clean chart without build artifacts
2026-02-16 16:49:49 +01:00
Aleksei Sviridkin
7ca6e5ce9e feat(installer): add variant-aware templates for generic Kubernetes support
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-16 18:47:04 +03:00
Andrei Kvapil
e092047630 feat(kilo): add Cilium compatibility variant (#2055)
## Summary
- Add a new `cilium` variant to the kilo PackageSource
- When selected, kilo is deployed with `--compatibility=cilium` flag
- This enables Cilium-aware IPIP encapsulation where outer packets are
routed through Cilium's VxLAN overlay instead of the host network

## Changes
- `packages/core/platform/sources/kilo.yaml` — new `cilium` variant with
`values-cilium.yaml`
- `packages/system/kilo/values-cilium.yaml` — sets `kilo.compatibility:
cilium`
- `packages/system/kilo/templates/kilo.yaml` — conditional
`--compatibility` flag

## Test plan
- [x] Tested on a cluster with Cilium networking and kilo
`--compatibility=cilium`
- [x] Verified IPIP tunneling works through Cilium's VxLAN overlay
- [x] Confirmed `kubectl exec` connectivity to worker nodes across
WireGuard mesh
2026-02-16 16:39:07 +01:00
Andrei Kvapil
956d9cc2a0 Merge branch 'main' into feat/kilo-cilium-variant
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-16 16:38:58 +01:00
Andrei Kvapil
ef040c2ed2 feat(kilo): add Cilium compatibility variant
Add a new "cilium" variant to the kilo PackageSource that deploys kilo
with --compatibility=cilium flag. This enables Cilium-aware IPIP
encapsulation, routing outer packets through Cilium's VxLAN overlay
instead of the host network.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-16 15:32:02 +01:00
Andrei Kvapil
d658850578 [dashboard] Upgrade dashboard to version 1.4.0 (#2051)
<!-- 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

Upgrade dashboard to version 1.4.0.

- Upgrade CRDs in CozyStack dashboard controller
- Add new CRD for CFOMapping
- Increase Ingress proxy timeouts, so that websocket won't be terminated
after 10 seconds of idle
- Add patch in Dockerfile to fix temporary 404 "Factory not found" error
while loading factories on page open

<img width="2560" height="1333" alt="dashboard 1 4 0"
src="https://github.com/user-attachments/assets/4bc19d0e-58a6-4506-b571-07cc6bf4880a"
/>

Notable changes:

- cluster selector is not in the sidebar
- namespace selector moved from navigation to table block

### 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
[dashboard] Upgrade dashboard to version 1.4.0
```

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

* **New Features**
* Added a new customization-mapping resource and integrated it into
dashboard navigation and static bootstrapping.

* **Improvements**
* Better routing between factory and detail pages; navigation sync
added.
  * Unified dashboard link/URL placeholders for consistency.
  * YAML editor now includes explicit API group/version context.
  * Increased ingress/proxy timeouts for stability.
* Expanded frontend configuration for navigation, factories, and
customization behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-16 10:20:57 +01:00
Andrei Kvapil
b6dec6042d [vpc] Migrate subnets definition from map to array format (#2052)
## What this PR does

Migrates VPC subnets definition from map format (`map[string]Subnet`) to
array format (`[]Subnet`) with an explicit `name` field. This aligns VPC
subnet definitions with the vm-instance format.

Before:
```yaml
subnets:
  mysubnet0:
    cidr: "172.16.0.0/24"
```

After:
```yaml
subnets:
  - name: mysubnet0
    cidr: "172.16.0.0/24"
```

Subnet ID generation remains unchanged — the sha256 input
(`namespace/vpcId/subnetName`) is the same, so existing resources are
not affected.

Includes a migration script (migration 30) that automatically converts
existing VPC HelmRelease values Secrets from map to array format. The
migration is idempotent and skips subnets that are already arrays or
null.

### Release note

```release-note
[vpc] Migrate VPC subnets from map to array format with automatic data migration
```

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

## Summary by CodeRabbit

* **Breaking Changes**
* VPC subnet configuration format changed from object-based to
array-based structure
* **Migration**
* Automatic migration included to convert existing VPC configurations to
new format

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-16 10:04:28 +01:00
Andrei Kvapil
12fb9ce7dd Update kilo v0.8.0 (#2053)
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
[]
```

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

## Summary by CodeRabbit

* **Chores**
* Updated Kilo container version from v0.7.1 to v0.8.0 with
corresponding digest update.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-16 10:03:13 +01:00
Andrei Kvapil
cf505c580d Update kilo v0.8.0
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-15 22:50:24 +01:00
Andrei Kvapil
9031de0538 feat(platform): add migration 30 for VPC subnets map-to-array conversion
Add migration script that converts VPC HelmRelease values from map
format to array format. The script discovers all VirtualPrivateCloud
HelmReleases, reads their values Secrets, and converts subnets using
yq. Idempotent: skips if subnets are already an array or null.

Bumps migration targetVersion from 30 to 31.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-15 21:34:26 +01:00
Andrei Kvapil
8e8bea039f feat(vpc): migrate subnets definition from map to array format
Change VPC subnets from map[string]Subnet to []Subnet with explicit
name field, aligning with the vm-instance subnet format.

Map format:  subnets: {mysubnet: {cidr: "x"}}
Array format: subnets: [{name: mysubnet, cidr: "x"}]

Subnet ID generation (sha256 of namespace/vpcId/subnetName) remains
unchanged — subnetName now comes from .name field instead of map key.
ConfigMap output format stays the same.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-15 21:34:19 +01:00
Kirill Ilin
fa55b5f41f [dashboard] Upgrade dashboard to version 1.4.0
- Upgrade CRDs in CozyStack dashboard controller
- Add Ingress proxy timeouts for WebSocket to work without terminations
- Add CFOMapping custom resource

Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-15 19:49:19 +05:00
Aleksei Sviridkin
fb8157ef9b refactor(api): remove rootHost-based name length validation
Root-host validation for Tenant names is no longer needed here.
The underlying issue (namespace.cozystack.io/host label exceeding
63-char limit) will be addressed in #2002 by moving the label
to an annotation.

Name length validation now only checks the Helm release name
limit (53 - prefix length), which applies uniformly to all
application types.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-12 13:52:37 +03:00
Aleksei Sviridkin
5bf481ae4d chore: update copyright year in start_test.go
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 13:38:26 +03:00
Aleksei Sviridkin
d5e713a4e7 fix(api): fix import order and context-aware error messages
- Fix goimports order: duration before validation/field
- Show rootHost in error messages only for Tenant kind where it
  actually affects the length calculation

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 13:32:59 +03:00
Aleksei Sviridkin
e267cfcf9d fix(api): address review feedback for validation consistency
- Return field.ErrorList from validateNameLength for consistent
  apierrors.NewInvalid error shape (was NewBadRequest)
- Add klog warning when YAML parsing fails in parseRootHostFromSecret
- Fix maxHelmReleaseName comment to accurately describe Helm convention
- Add note that root-host changes require API server restart
- Replace interface{} with any throughout openapi.go and rest.go
- Remove trailing blank line in const block

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 13:29:42 +03:00
Aleksei Sviridkin
c932740dc5 refactor(api): remove global ObjectMeta name patching from OpenAPI
Remove patchObjectMetaNameValidation and patchObjectMetaNameValidationV2
functions that were modifying the global ObjectMeta schema. This patching
affected ALL resources served by the API server, not just Application
resources. Backend validation in Create() is sufficient for enforcing
name constraints.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 13:24:16 +03:00
Aleksei Sviridkin
e978e00c7e refactor(api): use standard IsDNS1035Label and remove static length limit
Replace custom DNS-1035 regex with k8s.io/apimachinery IsDNS1035Label.
Remove hardcoded maxApplicationNameLength=40 from both validation and
OpenAPI — length validation is now handled entirely by validateNameLength
which computes dynamic limits based on Helm release prefix and root-host.
Fix README to reflect that max length depends on cluster configuration.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 13:18:45 +03:00
Aleksei Sviridkin
9e47669f68 fix(api): remove name validation from Update path and use klog
Skip DNS-1035 and length validation on Update since Kubernetes names
are immutable — validating would block updates to pre-existing resources
with non-conforming names. Replace fmt.Printf with klog for structured
logging consistency.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 13:07:50 +03:00
Aleksei Sviridkin
d4556e4c53 fix(api): address review feedback for name validation
- Add DNS-1035 format validation to Update path (was only in Create)
- Simplify Secret reading by reusing existing scheme instead of
  creating a separate client
- Add nil secret test case for parseRootHostFromSecret

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 12:58:44 +03:00
Aleksei Sviridkin
dd34fb581e fix(api): handle edge case when prefix or root host exhaust name capacity
Add protection against negative or zero maxLen when release prefix or
root host are too long, returning a clear configuration error instead of
a confusing "name too long" message. Add corresponding test cases.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 12:50:30 +03:00
Aleksei Sviridkin
3685d49c4e feat(api): add dynamic name length validation based on root-host
Read root-host from cozystack-values secret at API server startup
and use it to compute maximum allowed name length for applications.

For all apps: validates prefix + name fits within the Helm release
name limit (53 chars). For Tenants: additionally checks that the
host label (name + "." + rootHost) fits within the Kubernetes label
value limit (63 chars).

This replaces the static 40-char limit with a dynamic calculation
that accounts for the actual cluster root host length.

Ref: https://github.com/cozystack/cozystack/issues/2001

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 12:50:30 +03:00
Aleksei Sviridkin
7c0e99e1af [platform] Add OpenAPI schema validation for Application names
Add pattern and maxLength constraints to ObjectMeta.name in OpenAPI schema.
This enables UI form validation when openapi-k8s-toolkit supports it.

- Pattern: ^[a-z]([-a-z0-9]*[a-z0-9])?$ (DNS-1035)
- MaxLength: 40

Depends on: cozystack/openapi-k8s-toolkit#1

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 12:49:30 +03:00
Aleksei Sviridkin
9f20771cf8 docs(tenant): update naming requirements in README
Clarify DNS-1035 naming rules:
- Must start with lowercase letter
- Allowed characters: a-z, 0-9, hyphen
- Must end with letter or number
- Maximum 40 characters

Change wording from "not allowed" to "discouraged" for dashes
since the validation technically permits them.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 12:49:30 +03:00
Aleksei Sviridkin
1cbf183164 fix(validation): limit name to 40 chars and add comprehensive tests
- Reduce maxApplicationNameLength from 63 to 40 characters
  to allow room for prefixes like "tenant-" and nested namespaces
- Add 27 test cases covering:
  - Valid names (simple, single letter, with numbers, double hyphen)
  - Invalid start characters (digit, hyphen)
  - Invalid end characters (hyphen)
  - Invalid characters (uppercase, underscore, dot, space, unicode)
  - Empty/whitespace inputs
  - Length boundary tests (40 valid, 41+ invalid)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 12:49:30 +03:00
Aleksei Sviridkin
87e394c0c9 [platform] Add DNS-1035 validation for Application names
Add validation to ensure Application names (including Tenants) conform
to DNS-1035 format. This prevents creation of resources with names
starting with digits, which would cause Kubernetes resource creation
failures (e.g., Services, Namespaces).

DNS-1035 requires names to:
- Start with a lowercase letter [a-z]
- Contain only lowercase alphanumeric or hyphens [-a-z0-9]
- End with an alphanumeric character [a-z0-9]

Also fixes broken validation.go that referenced non-existent internal
types (apps.Application, apps.ApplicationSpec).

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

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 12:49:30 +03:00
455 changed files with 52110 additions and 31119 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @kvaps @lllamnyp @lexfrei @androndo @IvanHunters
* @kvaps @lllamnyp @lexfrei @androndo @IvanHunters @sircthulhu

View File

@@ -71,18 +71,6 @@ jobs:
name: pr-patch
path: _out/assets/pr.patch
- name: Upload CRDs
uses: actions/upload-artifact@v4
with:
name: cozystack-crds
path: _out/assets/cozystack-crds.yaml
- name: Upload operator
uses: actions/upload-artifact@v4
with:
name: cozystack-operator
path: _out/assets/cozystack-operator-talos.yaml
- name: Upload Talos image
uses: actions/upload-artifact@v4
with:
@@ -94,8 +82,6 @@ jobs:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'release')
outputs:
crds_id: ${{ steps.fetch_assets.outputs.crds_id }}
operator_id: ${{ steps.fetch_assets.outputs.operator_id }}
disk_id: ${{ steps.fetch_assets.outputs.disk_id }}
steps:
@@ -139,15 +125,11 @@ jobs:
return;
}
const find = (n) => draft.assets.find(a => a.name === n)?.id;
const crdsId = find('cozystack-crds.yaml');
const operatorId = find('cozystack-operator-talos.yaml');
const diskId = find('nocloud-amd64.raw.xz');
if (!crdsId || !operatorId || !diskId) {
if (!diskId) {
core.setFailed('Required assets missing in draft release');
return;
}
core.setOutput('crds_id', crdsId);
core.setOutput('operator_id', operatorId);
core.setOutput('disk_id', diskId);
@@ -174,20 +156,6 @@ jobs:
name: talos-image
path: _out/assets
- name: "Download CRDs (regular PR)"
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
uses: actions/download-artifact@v4
with:
name: cozystack-crds
path: _out/assets
- name: "Download operator (regular PR)"
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
uses: actions/download-artifact@v4
with:
name: cozystack-operator
path: _out/assets
- name: Download PR patch
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
uses: actions/download-artifact@v4
@@ -208,12 +176,6 @@ jobs:
curl -sSL -H "Authorization: token ${GH_PAT}" -H "Accept: application/octet-stream" \
-o _out/assets/nocloud-amd64.raw.xz \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${{ needs.resolve_assets.outputs.disk_id }}"
curl -sSL -H "Authorization: token ${GH_PAT}" -H "Accept: application/octet-stream" \
-o _out/assets/cozystack-crds.yaml \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${{ needs.resolve_assets.outputs.crds_id }}"
curl -sSL -H "Authorization: token ${GH_PAT}" -H "Accept: application/octet-stream" \
-o _out/assets/cozystack-operator-talos.yaml \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${{ needs.resolve_assets.outputs.operator_id }}"
env:
GH_PAT: ${{ secrets.GH_PAT }}

View File

@@ -38,25 +38,21 @@ build: build-deps
manifests:
mkdir -p _out/assets
helm template installer packages/core/installer -n cozy-system \
-s templates/crds.yaml \
> _out/assets/cozystack-crds.yaml
cat internal/crdinstall/manifests/*.yaml > _out/assets/cozystack-crds.yaml
# Talos variant (default)
helm template installer packages/core/installer -n cozy-system \
-s templates/cozystack-operator.yaml \
-s templates/packagesource.yaml \
--show-only templates/cozystack-operator.yaml \
> _out/assets/cozystack-operator-talos.yaml
# Generic Kubernetes variant (k3s, kubeadm, RKE2)
helm template installer packages/core/installer -n cozy-system \
-s templates/cozystack-operator.yaml \
-s templates/packagesource.yaml \
--set cozystackOperator.variant=generic \
--set cozystack.apiServerHost=REPLACE_ME \
--show-only templates/cozystack-operator.yaml \
> _out/assets/cozystack-operator-generic.yaml
# Hosted variant (managed Kubernetes)
helm template installer packages/core/installer -n cozy-system \
-s templates/cozystack-operator.yaml \
-s templates/packagesource.yaml \
--set cozystackOperator.variant=hosted \
--show-only templates/cozystack-operator.yaml \
> _out/assets/cozystack-operator-hosted.yaml
cozypkg:

View File

@@ -253,3 +253,25 @@ type FactoryList struct {
metav1.ListMeta `json:"metadata,omitempty"`
Items []Factory `json:"items"`
}
// -----------------------------------------------------------------------------
// CustomFormsOverrideMapping
// -----------------------------------------------------------------------------
// +kubebuilder:object:root=true
// +kubebuilder:resource:path=cfomappings,scope=Cluster
// +kubebuilder:subresource:status
type CFOMapping struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ArbitrarySpec `json:"spec"`
Status CommonStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type CFOMappingList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CFOMapping `json:"items"`
}

View File

@@ -69,6 +69,9 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&Factory{},
&FactoryList{},
&CFOMapping{},
&CFOMappingList{},
)
metav1.AddToGroupVersion(scheme, GroupVersion)
return nil

View File

@@ -159,6 +159,65 @@ func (in *BreadcrumbList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CFOMapping) DeepCopyInto(out *CFOMapping) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMapping.
func (in *CFOMapping) DeepCopy() *CFOMapping {
if in == nil {
return nil
}
out := new(CFOMapping)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CFOMapping) 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 *CFOMappingList) DeepCopyInto(out *CFOMappingList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]CFOMapping, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMappingList.
func (in *CFOMappingList) DeepCopy() *CFOMappingList {
if in == nil {
return nil
}
out := new(CFOMappingList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CFOMappingList) 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 *CommonStatus) DeepCopyInto(out *CommonStatus) {
*out = *in

View File

@@ -50,6 +50,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook"
"github.com/cozystack/cozystack/internal/cozyvaluesreplicator"
"github.com/cozystack/cozystack/internal/crdinstall"
"github.com/cozystack/cozystack/internal/fluxinstall"
"github.com/cozystack/cozystack/internal/operator"
"github.com/cozystack/cozystack/internal/telemetry"
@@ -77,6 +78,7 @@ func main() {
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var installCRDs bool
var installFlux bool
var disableTelemetry bool
var telemetryEndpoint string
@@ -97,6 +99,7 @@ func main() {
"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(&installCRDs, "install-crds", false, "Install Cozystack CRDs before starting reconcile loop")
flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop")
flag.BoolVar(&disableTelemetry, "disable-telemetry", false,
"Disable telemetry collection")
@@ -105,7 +108,7 @@ func main() {
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
"Interval between telemetry data collection (e.g. 15m, 1h)")
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-platform", "Name for the generated platform source resource and PackageSource")
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
flag.StringVar(&cozyValuesSecretName, "cozy-values-secret-name", "cozystack-values", "The name of the secret containing cluster-wide configuration values.")
flag.StringVar(&cozyValuesSecretNamespace, "cozy-values-secret-namespace", "cozy-system", "The namespace of the secret containing cluster-wide configuration values.")
@@ -134,8 +137,7 @@ func main() {
os.Exit(1)
}
// Start the controller manager
setupLog.Info("Starting controller manager")
// Initialize the controller manager
mgr, err := ctrl.NewManager(config, ctrl.Options{
Scheme: scheme,
Cache: cache.Options{
@@ -177,10 +179,26 @@ func main() {
os.Exit(1)
}
// Set up signal handler early so install phases respect SIGTERM
mgrCtx := ctrl.SetupSignalHandler()
// Install Cozystack CRDs before starting reconcile loop
if installCRDs {
setupLog.Info("Installing Cozystack CRDs before starting reconcile loop")
installCtx, installCancel := context.WithTimeout(mgrCtx, 2*time.Minute)
defer installCancel()
if err := crdinstall.Install(installCtx, directClient, crdinstall.WriteEmbeddedManifests); err != nil {
setupLog.Error(err, "failed to install CRDs")
os.Exit(1)
}
setupLog.Info("CRD installation completed successfully")
}
// 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)
installCtx, installCancel := context.WithTimeout(mgrCtx, 5*time.Minute)
defer installCancel()
// Use direct client for pre-start operations (cache is not ready yet)
@@ -194,7 +212,7 @@ func main() {
// Generate and install platform source resource if specified
if platformSourceURL != "" {
setupLog.Info("Generating platform source resource", "url", platformSourceURL, "name", platformSourceName, "ref", platformSourceRef)
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
installCtx, installCancel := context.WithTimeout(mgrCtx, 2*time.Minute)
defer installCancel()
// Use direct client for pre-start operations (cache is not ready yet)
@@ -206,6 +224,29 @@ func main() {
}
}
// Create platform PackageSource when CRDs are managed by the operator and
// a platform source URL is configured. Without a URL there is no Flux source
// resource to reference, so creating a PackageSource would leave a dangling SourceRef.
if installCRDs && platformSourceURL != "" {
sourceRefKind := "OCIRepository"
sourceType, _, err := parsePlatformSourceURL(platformSourceURL)
if err != nil {
setupLog.Error(err, "failed to parse platform source URL for PackageSource")
os.Exit(1)
}
if sourceType == "git" {
sourceRefKind = "GitRepository"
}
setupLog.Info("Creating platform PackageSource", "platformSourceName", platformSourceName)
psCtx, psCancel := context.WithTimeout(mgrCtx, 2*time.Minute)
defer psCancel()
if err := installPlatformPackageSource(psCtx, directClient, platformSourceName, sourceRefKind); err != nil {
setupLog.Error(err, "failed to create platform PackageSource")
os.Exit(1)
}
setupLog.Info("Platform PackageSource creation completed successfully")
}
// Setup PackageSource reconciler
if err := (&operator.PackageSourceReconciler{
Client: mgr.GetClient(),
@@ -276,7 +317,6 @@ func main() {
}
setupLog.Info("Starting controller manager")
mgrCtx := ctrl.SetupSignalHandler()
if err := mgr.Start(mgrCtx); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
@@ -535,3 +575,79 @@ func generateGitRepository(name, repoURL string, refMap map[string]string) (*sou
return obj, nil
}
// installPlatformPackageSource creates the platform PackageSource resource
// that references the Flux source resource (OCIRepository or GitRepository).
//
// The variant list is intentionally hardcoded here. These are platform-defined
// deployment profiles (not user-extensible), matching what was previously in
// the Helm template. Changes require a new operator build and release.
func installPlatformPackageSource(ctx context.Context, k8sClient client.Client, platformSourceName, sourceRefKind string) error {
logger := log.FromContext(ctx)
packageSourceName := "cozystack." + platformSourceName
ps := &cozyv1alpha1.PackageSource{
TypeMeta: metav1.TypeMeta{
APIVersion: cozyv1alpha1.GroupVersion.String(),
Kind: "PackageSource",
},
ObjectMeta: metav1.ObjectMeta{
Name: packageSourceName,
Annotations: map[string]string{
"operator.cozystack.io/skip-cozystack-values": "true",
},
},
Spec: cozyv1alpha1.PackageSourceSpec{
SourceRef: &cozyv1alpha1.PackageSourceRef{
Kind: sourceRefKind,
Name: platformSourceName,
Namespace: "cozy-system",
Path: "/",
},
},
}
variantData := []struct {
name string
valuesFiles []string
}{
{"default", []string{"values.yaml"}},
{"isp-full", []string{"values.yaml", "values-isp-full.yaml"}},
{"isp-hosted", []string{"values.yaml", "values-isp-hosted.yaml"}},
{"isp-full-generic", []string{"values.yaml", "values-isp-full-generic.yaml"}},
}
variants := make([]cozyv1alpha1.Variant, len(variantData))
for i, v := range variantData {
variants[i] = cozyv1alpha1.Variant{
Name: v.name,
Components: []cozyv1alpha1.Component{
{
Name: "platform",
Path: "core/platform",
Install: &cozyv1alpha1.ComponentInstall{
Namespace: "cozy-system",
ReleaseName: "cozystack-platform",
},
ValuesFiles: v.valuesFiles,
},
},
}
}
ps.Spec.Variants = variants
logger.Info("Applying platform PackageSource", "name", packageSourceName)
patchOptions := &client.PatchOptions{
FieldManager: "cozystack-operator",
Force: func() *bool { b := true; return &b }(),
}
if err := k8sClient.Patch(ctx, ps, client.Apply, patchOptions); err != nil {
return fmt.Errorf("failed to apply PackageSource %s: %w", packageSourceName, err)
}
logger.Info("Applied platform PackageSource", "name", packageSourceName)
return nil
}

View File

@@ -0,0 +1,574 @@
/*
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"
"testing"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func newTestScheme() *runtime.Scheme {
s := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(s)
return s
}
func TestInstallPlatformPackageSource_Creates(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
// Verify name
if ps.Name != "cozystack.cozystack-platform" {
t.Errorf("expected name %q, got %q", "cozystack.cozystack-platform", ps.Name)
}
// Verify annotation
if ps.Annotations["operator.cozystack.io/skip-cozystack-values"] != "true" {
t.Errorf("expected skip-cozystack-values annotation to be 'true', got %q", ps.Annotations["operator.cozystack.io/skip-cozystack-values"])
}
// Verify sourceRef
if ps.Spec.SourceRef == nil {
t.Fatal("expected SourceRef to be set")
}
if ps.Spec.SourceRef.Kind != "OCIRepository" {
t.Errorf("expected sourceRef.kind %q, got %q", "OCIRepository", ps.Spec.SourceRef.Kind)
}
if ps.Spec.SourceRef.Name != "cozystack-platform" {
t.Errorf("expected sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
}
if ps.Spec.SourceRef.Namespace != "cozy-system" {
t.Errorf("expected sourceRef.namespace %q, got %q", "cozy-system", ps.Spec.SourceRef.Namespace)
}
if ps.Spec.SourceRef.Path != "/" {
t.Errorf("expected sourceRef.path %q, got %q", "/", ps.Spec.SourceRef.Path)
}
// Verify variants
expectedVariants := []string{"default", "isp-full", "isp-hosted", "isp-full-generic"}
if len(ps.Spec.Variants) != len(expectedVariants) {
t.Fatalf("expected %d variants, got %d", len(expectedVariants), len(ps.Spec.Variants))
}
for i, name := range expectedVariants {
if ps.Spec.Variants[i].Name != name {
t.Errorf("expected variant[%d].name %q, got %q", i, name, ps.Spec.Variants[i].Name)
}
if len(ps.Spec.Variants[i].Components) != 1 {
t.Errorf("expected variant[%d] to have 1 component, got %d", i, len(ps.Spec.Variants[i].Components))
}
}
}
func TestInstallPlatformPackageSource_Updates(t *testing.T) {
s := newTestScheme()
existing := &cozyv1alpha1.PackageSource{
ObjectMeta: metav1.ObjectMeta{
Name: "cozystack.cozystack-platform",
ResourceVersion: "1",
Labels: map[string]string{
"custom-label": "should-be-preserved",
},
},
Spec: cozyv1alpha1.PackageSourceSpec{
SourceRef: &cozyv1alpha1.PackageSourceRef{
Kind: "OCIRepository",
Name: "old-name",
Namespace: "cozy-system",
},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(existing).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
// Verify sourceRef was updated
if ps.Spec.SourceRef.Name != "cozystack-platform" {
t.Errorf("expected updated sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
}
// Verify all 4 variants are present after update
if len(ps.Spec.Variants) != 4 {
t.Errorf("expected 4 variants after update, got %d", len(ps.Spec.Variants))
}
// Verify that labels set by other controllers are preserved (SSA does not overwrite unmanaged fields)
if ps.Labels["custom-label"] != "should-be-preserved" {
t.Errorf("expected custom-label to be preserved, got %q", ps.Labels["custom-label"])
}
}
func TestParsePlatformSourceURL(t *testing.T) {
tests := []struct {
name string
url string
wantType string
wantURL string
wantErr bool
}{
{
name: "OCI URL",
url: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
wantType: "oci",
wantURL: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
},
{
name: "HTTPS URL",
url: "https://github.com/cozystack/cozystack",
wantType: "git",
wantURL: "https://github.com/cozystack/cozystack",
},
{
name: "SSH URL",
url: "ssh://git@github.com/cozystack/cozystack",
wantType: "git",
wantURL: "ssh://git@github.com/cozystack/cozystack",
},
{
name: "empty URL",
url: "",
wantErr: true,
},
{
name: "unsupported scheme",
url: "ftp://example.com/repo",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sourceType, repoURL, err := parsePlatformSourceURL(tt.url)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error for URL %q, got nil", tt.url)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if sourceType != tt.wantType {
t.Errorf("expected type %q, got %q", tt.wantType, sourceType)
}
if repoURL != tt.wantURL {
t.Errorf("expected URL %q, got %q", tt.wantURL, repoURL)
}
})
}
}
func TestInstallPlatformPackageSource_VariantValuesFiles(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
expectedValuesFiles := map[string][]string{
"default": {"values.yaml"},
"isp-full": {"values.yaml", "values-isp-full.yaml"},
"isp-hosted": {"values.yaml", "values-isp-hosted.yaml"},
"isp-full-generic": {"values.yaml", "values-isp-full-generic.yaml"},
}
for _, v := range ps.Spec.Variants {
expected, ok := expectedValuesFiles[v.Name]
if !ok {
t.Errorf("unexpected variant %q", v.Name)
continue
}
if len(v.Components) != 1 {
t.Errorf("variant %q: expected 1 component, got %d", v.Name, len(v.Components))
continue
}
comp := v.Components[0]
if comp.Name != "platform" {
t.Errorf("variant %q: expected component name %q, got %q", v.Name, "platform", comp.Name)
}
if comp.Path != "core/platform" {
t.Errorf("variant %q: expected component path %q, got %q", v.Name, "core/platform", comp.Path)
}
if comp.Install == nil {
t.Errorf("variant %q: expected Install to be set", v.Name)
} else {
if comp.Install.Namespace != "cozy-system" {
t.Errorf("variant %q: expected install namespace %q, got %q", v.Name, "cozy-system", comp.Install.Namespace)
}
if comp.Install.ReleaseName != "cozystack-platform" {
t.Errorf("variant %q: expected install releaseName %q, got %q", v.Name, "cozystack-platform", comp.Install.ReleaseName)
}
}
if len(comp.ValuesFiles) != len(expected) {
t.Errorf("variant %q: expected %d valuesFiles, got %d", v.Name, len(expected), len(comp.ValuesFiles))
continue
}
for i, f := range expected {
if comp.ValuesFiles[i] != f {
t.Errorf("variant %q: expected valuesFiles[%d] %q, got %q", v.Name, i, f, comp.ValuesFiles[i])
}
}
}
}
func TestInstallPlatformPackageSource_CustomName(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "custom-source", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.custom-source"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
if ps.Name != "cozystack.custom-source" {
t.Errorf("expected name %q, got %q", "cozystack.custom-source", ps.Name)
}
if ps.Spec.SourceRef.Name != "custom-source" {
t.Errorf("expected sourceRef.name %q, got %q", "custom-source", ps.Spec.SourceRef.Name)
}
}
func TestInstallPlatformPackageSource_GitRepository(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "my-source", "GitRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.my-source"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
if ps.Spec.SourceRef.Kind != "GitRepository" {
t.Errorf("expected sourceRef.kind %q, got %q", "GitRepository", ps.Spec.SourceRef.Kind)
}
if ps.Spec.SourceRef.Name != "my-source" {
t.Errorf("expected sourceRef.name %q, got %q", "my-source", ps.Spec.SourceRef.Name)
}
}
func TestParseRefSpec(t *testing.T) {
tests := []struct {
name string
input string
want map[string]string
wantErr bool
}{
{
name: "empty string",
input: "",
want: map[string]string{},
},
{
name: "single key-value",
input: "tag=v1.0",
want: map[string]string{"tag": "v1.0"},
},
{
name: "multiple key-values",
input: "digest=sha256:abc123,tag=v1.0",
want: map[string]string{"digest": "sha256:abc123", "tag": "v1.0"},
},
{
name: "whitespace around pairs",
input: " tag=v1.0 , branch=main ",
want: map[string]string{"tag": "v1.0", "branch": "main"},
},
{
name: "equals sign in value",
input: "digest=sha256:abc=123",
want: map[string]string{"digest": "sha256:abc=123"},
},
{
name: "missing equals sign",
input: "tag",
wantErr: true,
},
{
name: "empty key",
input: "=value",
wantErr: true,
},
{
name: "empty value",
input: "tag=",
wantErr: true,
},
{
name: "trailing comma",
input: "tag=v1.0,",
want: map[string]string{"tag": "v1.0"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseRefSpec(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error for input %q, got nil", tt.input)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got) != len(tt.want) {
t.Fatalf("expected %d entries, got %d: %v", len(tt.want), len(got), got)
}
for k, v := range tt.want {
if got[k] != v {
t.Errorf("expected %q=%q, got %q=%q", k, v, k, got[k])
}
}
})
}
}
func TestValidateOCIRef(t *testing.T) {
tests := []struct {
name string
refMap map[string]string
wantErr bool
}{
{
name: "valid tag",
refMap: map[string]string{"tag": "v1.0"},
},
{
name: "valid digest",
refMap: map[string]string{"digest": "sha256:abc123def456"},
},
{
name: "valid semver",
refMap: map[string]string{"semver": ">=1.0.0"},
},
{
name: "multiple valid keys",
refMap: map[string]string{"tag": "v1.0", "digest": "sha256:abc"},
},
{
name: "empty map",
refMap: map[string]string{},
},
{
name: "invalid key",
refMap: map[string]string{"branch": "main"},
wantErr: true,
},
{
name: "invalid digest format",
refMap: map[string]string{"digest": "md5:abc"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateOCIRef(tt.refMap)
if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestValidateGitRef(t *testing.T) {
tests := []struct {
name string
refMap map[string]string
wantErr bool
}{
{
name: "valid branch",
refMap: map[string]string{"branch": "main"},
},
{
name: "valid commit",
refMap: map[string]string{"commit": "abc1234"},
},
{
name: "valid tag and branch",
refMap: map[string]string{"tag": "v1.0", "branch": "release"},
},
{
name: "empty map",
refMap: map[string]string{},
},
{
name: "invalid key",
refMap: map[string]string{"digest": "sha256:abc"},
wantErr: true,
},
{
name: "commit too short",
refMap: map[string]string{"commit": "abc"},
wantErr: true,
},
{
name: "commit not hex",
refMap: map[string]string{"commit": "zzzzzzz"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateGitRef(tt.refMap)
if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestGenerateOCIRepository(t *testing.T) {
refMap := map[string]string{"tag": "v1.0", "digest": "sha256:abc123"}
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", refMap)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Name != "my-repo" {
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
}
if obj.Namespace != "cozy-system" {
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
}
if obj.Spec.URL != "oci://registry.example.com/repo" {
t.Errorf("expected URL %q, got %q", "oci://registry.example.com/repo", obj.Spec.URL)
}
if obj.Spec.Reference == nil {
t.Fatal("expected Reference to be set")
}
if obj.Spec.Reference.Tag != "v1.0" {
t.Errorf("expected tag %q, got %q", "v1.0", obj.Spec.Reference.Tag)
}
if obj.Spec.Reference.Digest != "sha256:abc123" {
t.Errorf("expected digest %q, got %q", "sha256:abc123", obj.Spec.Reference.Digest)
}
}
func TestGenerateOCIRepository_NoRef(t *testing.T) {
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.Reference != nil {
t.Error("expected Reference to be nil for empty refMap")
}
}
func TestGenerateOCIRepository_InvalidRef(t *testing.T) {
_, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{"branch": "main"})
if err == nil {
t.Fatal("expected error for invalid OCI ref key, got nil")
}
}
func TestGenerateGitRepository(t *testing.T) {
refMap := map[string]string{"branch": "main", "commit": "abc1234def5678"}
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", refMap)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Name != "my-repo" {
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
}
if obj.Namespace != "cozy-system" {
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
}
if obj.Spec.URL != "https://github.com/user/repo" {
t.Errorf("expected URL %q, got %q", "https://github.com/user/repo", obj.Spec.URL)
}
if obj.Spec.Reference == nil {
t.Fatal("expected Reference to be set")
}
if obj.Spec.Reference.Branch != "main" {
t.Errorf("expected branch %q, got %q", "main", obj.Spec.Reference.Branch)
}
if obj.Spec.Reference.Commit != "abc1234def5678" {
t.Errorf("expected commit %q, got %q", "abc1234def5678", obj.Spec.Reference.Commit)
}
}
func TestGenerateGitRepository_NoRef(t *testing.T) {
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.Reference != nil {
t.Error("expected Reference to be nil for empty refMap")
}
}
func TestGenerateGitRepository_InvalidRef(t *testing.T) {
_, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{"digest": "sha256:abc"})
if err == nil {
t.Fatal("expected error for invalid Git ref key, got nil")
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.5
-->
## Improvements
* **[dashboard] Improve dashboard session params**: Improved session parameter handling in the dashboard for better user experience and more reliable session management ([**@lllamnyp**](https://github.com/lllamnyp) in #1913, #1919).
## Dependencies
* **Update cozyhr to v1.6.1**: Updated cozyhr to v1.6.1, which fixes a critical bug causing helm-controller v0.37.0+ to unexpectedly uninstall HelmReleases after cozyhr apply by correcting history snapshot fields for helm-controller compatibility ([**@kvaps**](https://github.com/kvaps) in cozystack/cozyhr#10).
---
**Full Changelog**: [v0.40.4...v0.40.5](https://github.com/cozystack/cozystack/compare/v0.40.4...v0.40.5)

View File

@@ -0,0 +1,11 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.6
-->
## Fixes
* **[kubernetes] Fix manifests for kubernetes deployment**: Fixed incorrect manifests that prevented proper Kubernetes deployment, restoring correct application behavior ([**@IvanHunters**](https://github.com/IvanHunters) in #1943, #1944).
---
**Full Changelog**: [v0.40.5...v0.40.6](https://github.com/cozystack/cozystack/compare/v0.40.5...v0.40.6)

View File

@@ -0,0 +1,11 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.7
-->
## Security
* **[dashboard] Verify JWT token**: Added JWT token verification to the dashboard, ensuring that authentication tokens are properly validated before granting access. This prevents unauthorized access through forged or expired tokens ([**@lllamnyp**](https://github.com/lllamnyp) in #1980, #1984).
---
**Full Changelog**: [v0.40.6...v0.40.7](https://github.com/cozystack/cozystack/compare/v0.40.6...v0.40.7)

View File

@@ -0,0 +1,11 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.4
-->
## Dependencies
* **Update cozyhr to v1.6.1**: Updated cozyhr to v1.6.1, which fixes a critical bug causing helm-controller v0.37.0+ to unexpectedly uninstall HelmReleases after cozyhr apply by correcting history snapshot fields for helm-controller compatibility ([**@kvaps**](https://github.com/kvaps) in cozystack/cozyhr#10).
---
**Full Changelog**: [v0.41.3...v0.41.4](https://github.com/cozystack/cozystack/compare/v0.41.3...v0.41.4)

View File

@@ -0,0 +1,21 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.5
-->
## Features and Improvements
* **[dashboard] Add "Edit" button to all resources**: Added an "Edit" button across all resource views in the dashboard, allowing users to modify resource configurations directly from the UI ([**@sircthulhu**](https://github.com/sircthulhu) in #1928, #1931).
* **[dashboard] Add resource quota usage to tenant details page**: Added resource quota usage display to the tenant details page, giving administrators visibility into how much of allocated resources each tenant is consuming ([**@sircthulhu**](https://github.com/sircthulhu) in #1929, #1932).
* **[branding] Separate values for keycloak**: Separated Keycloak branding values into dedicated configuration, allowing more granular customization of Keycloak appearance without affecting other branding settings ([**@nbykov0**](https://github.com/nbykov0) in #1946).
* **Add instance profile label to workload monitor**: Added instance profile metadata labels to the workload monitor, enabling better resource tracking and monitoring by instance profile type ([**@matthieu-robin**](https://github.com/matthieu-robin) in #1954, #1957).
## Fixes
* **[kubernetes] Fix manifests for kubernetes deployment**: Fixed incorrect manifests that prevented proper Kubernetes deployment, restoring correct application behavior ([**@IvanHunters**](https://github.com/IvanHunters) in #1943, #1945).
---
**Full Changelog**: [v0.41.4...v0.41.5](https://github.com/cozystack/cozystack/compare/v0.41.4...v0.41.5)

View File

@@ -0,0 +1,17 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.6
-->
## Improvements
* **[vm] Allow changing field external after creation**: Users can now modify the external network field on virtual machines after initial creation, providing more flexibility in VM networking configuration without requiring recreation ([**@sircthulhu**](https://github.com/sircthulhu) in #1956, #1962).
* **[branding] Separate values for keycloak**: Separated Keycloak branding values into dedicated configuration for more granular customization of Keycloak appearance ([**@nbykov0**](https://github.com/nbykov0) in #1947, #1963).
## Fixes
* **[kubernetes] Fix coredns serviceaccount to match kubernetes bootstrap RBAC**: Configured the CoreDNS chart to create a `kube-dns` ServiceAccount matching the Kubernetes bootstrap ClusterRoleBinding, fixing RBAC errors (`Failed to watch`) when CoreDNS pods restart ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #1958, #1978).
---
**Full Changelog**: [v0.41.5...v0.41.6](https://github.com/cozystack/cozystack/compare/v0.41.5...v0.41.6)

View File

@@ -0,0 +1,15 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.7
-->
## Security
* **[dashboard] Verify JWT token**: Added JWT token verification to the dashboard, ensuring that authentication tokens are properly validated before granting access. This prevents unauthorized access through forged or expired tokens ([**@lllamnyp**](https://github.com/lllamnyp) in #1980, #1983).
## Fixes
* **[postgres-operator] Correct PromQL syntax in CNPGClusterOffline alert**: Fixed incorrect PromQL syntax in the `CNPGClusterOffline` alert rule for CloudNativePG, ensuring the alert fires correctly when all instances of a PostgreSQL cluster are offline ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #1981, #1989).
---
**Full Changelog**: [v0.41.6...v0.41.7](https://github.com/cozystack/cozystack/compare/v0.41.6...v0.41.7)

View File

@@ -0,0 +1,17 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.8
-->
## Features and Improvements
* **[kubernetes] Auto-enable Gateway API support in cert-manager**: cert-manager now automatically enables `enableGatewayAPI` when the Gateway API addon is active in the Kubernetes application. Users no longer need to manually configure this setting, and the option can still be overridden via `valuesOverride` ([**@kvaps**](https://github.com/kvaps) in #1997, #2012).
* **[vm] Allow switching between instancetype and custom resources**: Users can now switch virtual machines between instancetype-based and custom resource configurations after creation. The upgrade hook atomically patches VM resources, providing more flexibility in adjusting VM sizing without recreation ([**@sircthulhu**](https://github.com/sircthulhu) in #2008, #2013).
## Fixes
* **[dashboard] Add startupProbe to prevent container restarts on slow hardware**: Added `startupProbe` to both `bff` and `web` containers in the dashboard deployment. On slow hardware, kubelet was killing containers because the `livenessProbe` only allowed ~33 seconds for startup. The `startupProbe` gives containers up to 60 seconds to start before `livenessProbe` kicks in ([**@kvaps**](https://github.com/kvaps) in #1996, #2014).
---
**Full Changelog**: [v0.41.7...v0.41.8](https://github.com/cozystack/cozystack/compare/v0.41.7...v0.41.8)

View File

@@ -0,0 +1,15 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.9
-->
## Fixes
* **[cozystack-basics] Deny resourcequotas deletion for tenant admin**: Prevented tenant administrators from deleting resource quotas, ensuring that resource limits set by platform administrators cannot be bypassed by tenant-level users ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2076).
## Dependencies
* **Update Kube-OVN to v1.15.3**: Updated Kube-OVN CNI to v1.15.3 with latest bug fixes and improvements ([**@kvaps**](https://github.com/kvaps)).
---
**Full Changelog**: [v0.41.8...v0.41.9](https://github.com/cozystack/cozystack/compare/v0.41.8...v0.41.9)

View File

@@ -0,0 +1,36 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.0.0-beta.5
-->
> **⚠️ Beta Release Warning**: This is a pre-release version intended for testing and early adoption. Breaking changes may occur before the stable v1.0.0 release.
## Features and Improvements
* **[installer] Add variant-aware templates for generic Kubernetes support**: Extended the installer chart to support generic and hosted Kubernetes deployments via the existing `cozystackOperator.variant` parameter. When using `variant=generic`, the installer now renders separate templates for the Cozystack operator, skipping Talos-specific components. This enables users to deploy Cozystack on standard Kubernetes distributions and hosted Kubernetes services, expanding platform compatibility beyond Talos Linux ([**@lexfrei**](https://github.com/lexfrei) in #2010).
* **[kilo] Add Cilium compatibility variant**: Added a new `cilium` variant to the kilo PackageSource that deploys kilo with the `--compatibility=cilium` flag. This enables Cilium-aware IPIP encapsulation where the outer packet IP matches the inner packet source, allowing Cilium's network policies to function correctly with kilo's WireGuard mesh networking. Users can now run kilo alongside Cilium CNI while maintaining full network policy enforcement capabilities ([**@kvaps**](https://github.com/kvaps) in #2055).
* **[cluster-autoscaler] Enable enforce-node-group-min-size by default**: Enabled the `enforce-node-group-min-size` option for the system cluster-autoscaler chart. This ensures node groups are always scaled up to their configured minimum size, even when current workload demands are lower, preventing unexpected scale-down below minimum thresholds and improving cluster stability for production workloads ([**@kvaps**](https://github.com/kvaps) in #2050).
* **[dashboard] Upgrade dashboard to version 1.4.0**: Updated the Cozystack dashboard to version 1.4.0 with new features and improvements for better user experience and cluster management capabilities ([**@sircthulhu**](https://github.com/sircthulhu) in #2051).
## Breaking Changes & Upgrade Notes
* **[vpc] Migrate subnets definition from map to array format**: Migrated VPC subnets definition from map format (`map[string]Subnet`) to array format (`[]Subnet`) with an explicit `name` field. This aligns VPC subnet definitions with the vm-instance `networks` field pattern and provides more intuitive configuration. Existing VPC deployments are automatically migrated via migration 30, which converts the subnet map to an array while preserving all existing subnet configurations and network connectivity ([**@kvaps**](https://github.com/kvaps) in #2052).
## Dependencies
* **[kilo] Update to v0.8.0**: Updated Kilo WireGuard mesh networking to v0.8.0 with performance improvements, bug fixes, and new compatibility features ([**@kvaps**](https://github.com/kvaps) in #2053).
* **[talm] Skip config loading for __complete command**: Fixed CLI completion behavior by skipping config loading for the `__complete` command, preventing errors during shell completion when configuration files are not available or misconfigured ([**@kitsunoff**](https://github.com/kitsunoff) in cozystack/talm#109).
## Contributors
We'd like to thank all contributors who made this release possible:
* [**@kitsunoff**](https://github.com/kitsunoff)
* [**@kvaps**](https://github.com/kvaps)
* [**@lexfrei**](https://github.com/lexfrei)
* [**@sircthulhu**](https://github.com/sircthulhu)
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.0-beta.4...v1.0.0-beta.5

View File

@@ -0,0 +1,46 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.0.0-beta.6
-->
> **⚠️ Beta Release Warning**: This is a pre-release version intended for testing and early adoption. Breaking changes may occur before the stable v1.0.0 release.
## Features and Improvements
* **[platform] Add cilium-kilo networking variant**: Added a new `cilium-kilo` networking variant that combines Cilium CNI with Kilo WireGuard mesh overlay. This variant enables `enable-ipip-termination` in Cilium for proper IPIP packet handling and deploys Kilo with `--compatibility=cilium` flag. Users can now select `cilium-kilo` as their networking variant during platform setup, simplifying the multi-location WireGuard setup compared to manually combining Cilium and standalone Kilo ([**@kvaps**](https://github.com/kvaps) in #2064).
* **[nats] Add monitoring**: Added Grafana dashboards for NATS JetStream and server metrics monitoring, along with Prometheus monitoring support with TLS-aware endpoint configuration. Includes updated image customization options (digest and full image name) and component version upgrades for the NATS exporter and utilities. Users now have full observability into NATS message broker performance and health ([**@klinch0**](https://github.com/klinch0) in #1381).
* **[platform] Add DNS-1035 validation for Application names**: Added dynamic DNS-1035 label validation for Application names in the Cozystack API, using `IsDNS1035Label` from `k8s.io/apimachinery`. Validation is performed at creation time and accounts for the root host length to prevent names that would exceed Kubernetes resource naming limits. This prevents creation of resources with invalid names that would fail downstream Kubernetes resource creation ([**@lexfrei**](https://github.com/lexfrei) in #1771).
* **[operator] Add automatic CRD installation at startup**: Added `--install-crds` flag to the Cozystack operator that installs embedded CRD manifests at startup, ensuring CRDs exist before the operator begins reconciliation. CRD manifests are now embedded in the operator binary and verified for consistency with the Helm `crds/` directory via a new CI Makefile check. This eliminates ordering issues during initial cluster setup where CRDs might not yet be present ([**@lexfrei**](https://github.com/lexfrei) in #2060).
## Fixes
* **[platform] Adopt tenant-root into cozystack-basics during migration**: Added migration 31 to adopt existing `tenant-root` Namespace and HelmRelease into the `cozystack-basics` Helm release when upgrading from v0.41.x to v1.0. Previously these resources were applied via `kubectl apply` with no Helm release tracking, causing Helm to treat them as foreign resources and potentially delete them during reconciliation. This migration ensures a safe upgrade path by annotating and labeling these resources for Helm adoption ([**@kvaps**](https://github.com/kvaps) in #2065).
* **[platform] Preserve tenant-root HelmRelease during migration**: Fixed a data-loss risk during migration from v0.41.x to v1.0.0-beta where the `tenant-root` HelmRelease (and the namespace it manages) could be deleted, causing tenant service outages. Added safety annotation to the HelmRelease and lookup logic to preserve current parameters during migration, preventing unwanted deletion of tenant-root resources ([**@sircthulhu**](https://github.com/sircthulhu) in #2063).
* **[codegen] Add gen_client to update-codegen.sh and regenerate applyconfiguration**: Fixed a build error in `pkg/generated/applyconfiguration/utils.go` caused by a reference to `testing.TypeConverter` which was removed in client-go v0.34.1. The root cause was that `hack/update-codegen.sh` never called `gen_client`, leaving the generated applyconfiguration code stale. Running the full code generation now produces a consistent and compilable codebase ([**@lexfrei**](https://github.com/lexfrei) in #2061).
* **[e2e] Make kubernetes test retries effective by cleaning up stale resources**: Fixed E2E test retries for the Kubernetes tenant test by adding pre-creation cleanup of backend deployment/service and NFS pod/PVC in `run-kubernetes.sh`. Previously, retries would fail immediately because stale resources from a failed attempt blocked re-creation. Also increased the tenant deployment wait timeout from 90s to 300s to handle CI resource pressure ([**@lexfrei**](https://github.com/lexfrei) in #2062).
## Development, Testing, and CI/CD
* **[e2e] Use helm install instead of kubectl apply for cozystack installation**: Replaced the pre-rendered static YAML application flow (`kubectl apply`) with direct `helm upgrade --install` of the `packages/core/installer` chart in E2E tests. Removed the CRD/operator artifact upload/download steps from the CI workflow, simplifying the pipeline. The chart with correct values is already present in the sandbox via workspace copy and `pr.patch` ([**@lexfrei**](https://github.com/lexfrei) in #2060).
## Documentation
* **[website] Improve Azure autoscaling troubleshooting guide**: Enhanced the Azure autoscaling troubleshooting documentation with serial console instructions for debugging VMSS worker nodes, a troubleshooting section for nodes stuck in maintenance mode due to invalid or missing machine config, `az vmss update --custom-data` instructions for updating machine config, and a warning that Azure does not support reading back `customData` ([**@kvaps**](https://github.com/kvaps) in cozystack/website#424).
* **[website] Update multi-location documentation for cilium-kilo variant**: Updated multi-location networking documentation to reflect the new integrated `cilium-kilo` variant selection during platform setup, replacing the previous manual Kilo installation and Cilium configuration steps. Added explanation of `enable-ipip-termination` and updated the troubleshooting section ([**@kvaps**](https://github.com/kvaps) in cozystack/website@02d63f0).
## Contributors
We'd like to thank all contributors who made this release possible:
* [**@klinch0**](https://github.com/klinch0)
* [**@kvaps**](https://github.com/kvaps)
* [**@lexfrei**](https://github.com/lexfrei)
* [**@sircthulhu**](https://github.com/sircthulhu)
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.0-beta.5...v1.0.0-beta.6

View File

@@ -0,0 +1,65 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.0.0-rc.1
-->
> **⚠️ Release Candidate Warning**: This is a release candidate intended for final validation before the stable v1.0.0 release. Breaking changes are not expected at this stage, but please test thoroughly before deploying to production.
## Features and Improvements
* **[harbor] Add managed Harbor container registry**: Added Harbor v2.14.2 as a managed tenant-level container registry service. The application uses CloudNativePG for PostgreSQL, the Redis operator for caching, and S3 via COSI BucketClaim (from SeaweedFS) for registry image storage. Auto-generated admin credentials are persisted across upgrades, TLS is handled by cert-manager, and Trivy vulnerability scanner is included. Users can now deploy a fully managed, production-ready OCI container registry within their tenant ([**@lexfrei**](https://github.com/lexfrei) in #2058).
* **[kubernetes] Update supported Kubernetes versions to v1.30v1.35**: Updated the tenant Kubernetes version matrix to v1.30, v1.31, v1.32, v1.33, v1.34, and v1.35 (now the default). EOL versions v1.28 and v1.29 are removed. Kamaji is updated to edge-26.2.4 with full Kubernetes 1.35 support, and the CAPI Kamaji provider is updated to v0.16.0. A compatibility patch ensures kubelets older than v1.35 are not broken by Kamaji injecting 1.35-specific kubelet fields ([**@lexfrei**](https://github.com/lexfrei) in #2073).
* **[platform] Make cluster issuer name and ACME solver configurable**: Added `publishing.certificates.solver` (`http01` or `dns01`) and `publishing.certificates.issuerName` (default: `letsencrypt-prod`) parameters to the platform chart. This allows operators to point all ingress TLS annotations at any ClusterIssuer — custom ACME, self-signed, or internal CA — without modifying individual package templates. See the Breaking Changes section for the rename from the previous `issuerType` field ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2077).
* **[dashboard] VMInstance dropdowns for disks and instanceType**: The VM instance creation form now renders API-backed dropdowns for the `instanceType` field (populated from `VirtualMachineClusterInstancetype` cluster resources) and for disk `name` fields (populated from `VMDisk` resources in the same namespace). Default values are read from the ApplicationDefinition's OpenAPI schema. This eliminates manual lookups and reduces misconfiguration when attaching disks or selecting VM instance types ([**@sircthulhu**](https://github.com/sircthulhu) in #2071).
* **[installer] Remove CRDs from Helm chart, delegate lifecycle to operator**: The `cozy-installer` Helm chart no longer ships CRDs in its `crds/` directory. CRD lifecycle is now fully managed by the Cozystack operator via the `--install-crds` flag, which applies embedded CRD manifests on every startup using server-side apply. The platform PackageSource is also created by the operator instead of a Helm template. This ensures CRDs and the PackageSource are always up to date after each operator restart, eliminating stale CRDs from Helm's install-only behavior ([**@lexfrei**](https://github.com/lexfrei) in #2074).
## Fixes
* **[kubevirt] Update KubeVirt to v1.6.4 and CDI to v1.64.0, fix VM pod initialization**: Updated KubeVirt operator to v1.6.4 and CDI operator to v1.64.0, including live migration of existing VMs during the upgrade. Additionally, disabled serial console logging globally via the KubeVirt CR to prevent a known v1.6.x issue ([upstream #15989](https://github.com/kubevirt/kubevirt/issues/15989)) where the `guest-console-log` init container blocked virt-launcher pods from starting, causing all VMs to get stuck in `PodInitializing` state ([**@nbykov0**](https://github.com/nbykov0) in #1833; [**@kvaps**](https://github.com/kvaps) in 7dfb819).
* **[linstor] Fix DRBD+LUKS+STORAGE resource creation failure**: All newly created encrypted volumes were failing because the DRBD `.res` file was never written due to a missing `setExists(true)` call in the `LuksLayer`. Applied the upstream `skip-adjust-when-device-inaccessible` patch ([LINBIT/linstor-server#477](https://github.com/LINBIT/linstor-server/pull/477)) which fixes the root cause and also prevents unnecessary lsblk calls when devices are not yet physically present ([**@kvaps**](https://github.com/kvaps) in #2072).
* **[system] Fix monitoring-agents FQDN resolution for tenant workload clusters**: Monitoring agents (`vmagent`, `fluent-bit`) in tenant workload clusters were failing to deliver metrics and logs because service addresses used short DNS names without the cluster domain suffix. Fixed by appending the configured cluster domain from `_cluster.cluster-domain` (with fallback to `cluster.local`) to all vmagent remoteWrite URLs and fluent-bit output hosts ([**@IvanHunters**](https://github.com/IvanHunters) in #2075).
* **[cozystack-basics] Preserve existing HelmRelease values during reconciliations**: Fixed a data-loss bug where changes made to the `tenant-root` HelmRelease were silently dropped on the next forced or upgrade reconciliation of the `cozystack-basics` HelmRelease. The reconciler now merges new configuration with existing values instead of overwriting them ([**@sircthulhu**](https://github.com/sircthulhu) in #2068).
* **[cozystack-basics] Deny resourcequotas deletion for tenant admin**: Fixed the `cozy:tenant:admin:base` ClusterRole to explicitly deny deletion of `ResourceQuota` objects for tenant admins and superadmins, preventing accidental removal of tenant resource limits ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2076).
## Breaking Changes & Upgrade Notes
* **[platform] Certificate issuer configuration parameters renamed**: The `publishing.certificates.issuerType` field is renamed to `publishing.certificates.solver`, and the value `cloudflare` is renamed to `dns01` to align with standard ACME terminology. A new `publishing.certificates.issuerName` field (default: `letsencrypt-prod`) is introduced to allow pointing all ingresses at a custom ClusterIssuer. Migration 32 is included and automatically converts existing configurations during upgrade — no manual action is required ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2077).
## Documentation
* **[website] Migrate ConfigMap references to Platform Package in v1 documentation**: Updated the entire v1 documentation tree to replace legacy ConfigMap-based configuration references with the new Platform Package API, ensuring guides are consistent with the v1 configuration model ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#426).
* **[website] Add generic Kubernetes deployment guide for v1**: Added a new installation guide covering Cozystack deployment on any generic Kubernetes cluster, expanding the set of supported deployment targets beyond provider-specific guides ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#408).
* **[website] Refactor resource planning documentation**: Improved the resource planning guide with a clearer structure and more comprehensive coverage of planning considerations for Cozystack deployments ([**@IvanStukov**](https://github.com/IvanStukov) in cozystack/website#423).
* **[website] Add ServiceAccount API access documentation and update FAQ**: Added a new article documenting ServiceAccount API access token configuration and updated the FAQ to include related troubleshooting guidance ([**@IvanStukov**](https://github.com/IvanStukov) in cozystack/website#421).
* **[website] Update networking-mesh allowed-location-ips example**: Replaced provider-specific CLI usage with standard `kubectl` commands in the multi-location networking guide's `allowed-location-ips` example, making the documentation more universally applicable ([**@kvaps**](https://github.com/kvaps) in cozystack/website#425).
## Contributors
We'd like to thank all contributors who made this release possible:
* [**@IvanHunters**](https://github.com/IvanHunters)
* [**@IvanStukov**](https://github.com/IvanStukov)
* [**@kvaps**](https://github.com/kvaps)
* [**@lexfrei**](https://github.com/lexfrei)
* [**@myasnikovdaniil**](https://github.com/myasnikovdaniil)
* [**@nbykov0**](https://github.com/nbykov0)
* [**@sircthulhu**](https://github.com/sircthulhu)
### New Contributors
We're excited to welcome our first-time contributors:
* [**@myasnikovdaniil**](https://github.com/myasnikovdaniil) - First contribution!
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.0-beta.6...v1.0.0-rc.1

View File

@@ -0,0 +1,57 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.0.0-rc.2
-->
> **⚠️ Release Candidate Warning**: This is a release candidate intended for final validation before the stable v1.0.0 release. Breaking changes are not expected at this stage, but please test thoroughly before deploying to production.
## Features and Improvements
* **[keycloak] Allow custom Ingress hostname via values**: Added an `ingress.host` field to the cozy-keycloak chart values, allowing operators to override the default `keycloak.<root-host>` Ingress hostname. The custom hostname is applied to both the Ingress resource and the `KC_HOSTNAME` environment variable in the StatefulSet. When left empty, the original behavior is preserved (fully backward compatible) ([**@sircthulhu**](https://github.com/sircthulhu) in #2101).
## Fixes
* **[platform] Fix upgrade issues in migrations, etcd timeout, and migration script**: Fixed multiple upgrade failures discovered during v0.41.1 → v1.0 upgrade testing. Migration 26 now uses the `cozystack.io/ui=true` label (always present on v0.41.1) instead of the new label that depends on migration 22 having run, and adds robust Helm secret deletion with fallback and verification. Migrations 28 and 29 wrap `grep` calls to prevent `pipefail` exits and fix the reconcile annotation to use RFC3339 format. Migration 27 now skips missing CRDs and adds a name-pattern fallback for Helm secret deletion. The etcd HelmRelease timeout is increased from 10m to 30m to accommodate TLS cert rotation hooks. The `migrate-to-version-1.0.sh` script gains the missing `bundle-disable`, `bundle-enable`, `expose-ingress`, and `expose-services` field mappings ([**@kvaps**](https://github.com/kvaps) in #2096).
* **[platform] Fix orphaned -rd HelmReleases after application renames**: After the `ferretdb→mongodb`, `mysql→mariadb`, and `virtual-machine→vm-disk+vm-instance` renames, the system-level `-rd` HelmReleases in `cozy-system` (`ferretdb-rd`, `mysql-rd`, `virtual-machine-rd`) were left orphaned, referencing ExternalArtifacts that no longer exist and causing persistent reconciliation failures. Migrations 28 and 29 are updated to remove these resources, and migration 33 is added as a safety net for clusters that already passed those migrations ([**@kvaps**](https://github.com/kvaps) in #2102).
* **[monitoring-agents] Fix FQDN resolution regression in tenant workload clusters**: The fix introduced in #2075 used `_cluster.cluster-domain` references in `values.yaml`, but `_cluster` values are not accessible from Helm subchart contexts — meaning fluent-bit received empty hostnames and failed to forward logs. This PR replaces the `_cluster` references with a new `global.clusterDomain` variable (empty by default for management clusters, set to the cluster domain for tenant clusters), which is correctly shared with all subcharts ([**@kvaps**](https://github.com/kvaps) in #2086).
* **[dashboard] Fix legacy templating and cluster identifier in sidebar links**: Standardized the cluster identifier used across dashboard menu links, administration links, and API request paths, resolving incorrect or broken link targets for the Backups and External IPs sidebar sections ([**@androndo**](https://github.com/androndo) in #2093).
* **[dashboard] Fix backupjobs creation form and sidebar backup category identifier**: Fixed the backup job creation form configuration, adding the required Name, Namespace, Plan Name, Application, and Backup Class fields. Fixed the sidebar backup category identifier that was causing incorrect navigation ([**@androndo**](https://github.com/androndo) in #2103).
## Documentation
* **[website] Add Helm chart development principles guide**: Added a new developer guide section documenting Cozystack's four core Helm chart principles: easy upstream updates, local-first artifacts, local dev/test workflow, and no external dependencies ([**@kvaps**](https://github.com/kvaps) in cozystack/website#418).
* **[website] Add network architecture overview**: Added comprehensive network architecture documentation covering the multi-layered networking stack — MetalLB (L2/BGP), Cilium eBPF (kube-proxy replacement), Kube-OVN (centralized IPAM), and tenant isolation with identity-based eBPF policies — with Mermaid diagrams for all major traffic flows ([**@IvanHunters**](https://github.com/IvanHunters) in cozystack/website#422).
* **[website] Update documentation to use jsonpatch for service exposure**: Improved `kubectl patch` commands throughout installation and configuration guides to use JSON Patch `add` operations for extending arrays instead of replacing them wholesale, making the documented commands safer and more precise ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#427).
* **[website] Update certificates section in Platform Package documentation**: Updated the certificate configuration documentation to reflect the new `solver` and `issuerName` fields introduced in v1.0.0-rc.1, replacing the legacy `issuerType` references ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#429).
* **[website] Add tenant Kubernetes cluster log querying guide**: Added documentation for querying logs from tenant Kubernetes clusters in Grafana using VictoriaLogs labels (`tenant`, `kubernetes_namespace_name`, `kubernetes_pod_name`), including the `monitoringAgents` addon prerequisite and step-by-step filtering examples ([**@IvanHunters**](https://github.com/IvanHunters) in cozystack/website#430).
* **[website] Replace non-idempotent commands with idempotent alternatives**: Updated `helm install` to `helm upgrade --install`, `kubectl create -f` to `kubectl apply -f`, and `kubectl create ns` to the dry-run+apply pattern across all installation and deployment guides so commands can be safely re-run ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#431).
* **[website] Fix broken documentation links with `.md` suffix**: Fixed incorrect internal links with `.md` suffix across virtualization guides for both v0 and v1 documentation, standardizing link text to "Developer Guide" ([**@cheese**](https://github.com/cheese) in cozystack/website#432).
## Contributors
We'd like to thank all contributors who made this release possible:
* [**@androndo**](https://github.com/androndo)
* [**@cheese**](https://github.com/cheese)
* [**@IvanHunters**](https://github.com/IvanHunters)
* [**@kvaps**](https://github.com/kvaps)
* [**@lexfrei**](https://github.com/lexfrei)
* [**@myasnikovdaniil**](https://github.com/myasnikovdaniil)
* [**@sircthulhu**](https://github.com/sircthulhu)
### New Contributors
We're excited to welcome our first-time contributors:
* [**@cheese**](https://github.com/cheese) - First contribution!
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.0-rc.1...v1.0.0-rc.2

289
docs/changelogs/v1.0.0.md Normal file
View File

@@ -0,0 +1,289 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.0.0
-->
# Cozystack v1.0.0 — "Stable"
We are thrilled to announce **Cozystack v1.0.0**, the first stable major release of the Cozystack platform. This milestone represents a fundamental architectural evolution from the v0.x series, introducing a fully operator-driven package management system, a comprehensive backup and restore framework, a redesigned virtual machine architecture, and a rich set of new managed applications — all hardened through an extensive alpha, beta, and release-candidate cycle.
## Feature Highlights
### Package-Based Architecture with Cozystack Operator
The most significant architectural change in v1.0.0 is the replacement of HelmRelease bundle deployments with a declarative **Package** and **PackageSource** model managed by the new `cozystack-operator`. Operators now define their platform configuration in a structured `values.yaml` and the operator reconciles the desired state by managing Package and PackageSource resources across the cluster.
The operator also takes ownership of CRD lifecycle — installing and updating CRDs from embedded manifests at every startup — eliminating the stale-CRD problem that affected Helm-only installations. Flux sharding has been added to distribute tenant HelmRelease reconciliation across multiple Flux controllers, providing horizontal scalability in large multi-tenant environments.
A migration script (`hack/migrate-to-version-1.0.sh`) is provided for upgrading existing v0.x clusters, along with 33 incremental migration steps that automate resource renaming, secret cleanup, and configuration conversion.
### Comprehensive Backup and Restore System
v1.0.0 ships a fully featured, production-ready backup and restore framework built on Velero integration. Users can define **BackupClass** resources to describe backup storage targets, create **BackupPlan** schedules, and trigger **RestoreJob** resources for end-to-end application recovery.
Virtual machine backups are supported natively via the Velero KubeVirt plugin, which captures consistent VM disk snapshots alongside metadata. The backup controller and the backup strategy sub-controllers (including the VM-specific strategy) are installed by default, and a full dashboard UI allows users to monitor backup status, view backup job history, and initiate restore workflows.
### Redesigned Virtual Machine Architecture
The legacy `virtual-machine` application has been replaced with a two-resource architecture: **`vm-disk`** for managing persistent disks and **`vm-instance`** for managing VM lifecycle. This separation provides cleaner disk/instance management, allows disks to be reused across VM instances, and aligns with modern KubeVirt patterns.
New capabilities include: a `cpuModel` field for direct CPU model specification without using an instanceType; the ability to switch between `instanceType`-based and custom resource-based configurations; migration from the deprecated `running` field to `runStrategy`; and native **RWX (NFS) filesystem support** in the KubeVirt CSI driver, enabling multiple pods to mount the same persistent volume simultaneously.
### New Managed Applications
v1.0.0 expands the application catalog significantly:
- **MongoDB**: A fully managed MongoDB replica set with persistent storage, monitoring integration, and unified user/database configuration API.
- **Qdrant**: A high-performance vector database for AI and machine learning workloads, supporting single-replica and clustered modes with API key authentication and optional external LoadBalancer access.
- **Harbor**: A fully managed OCI container registry backed by CloudNativePG, Redis operator, and COSI BucketClaim (SeaweedFS). Includes Trivy vulnerability scanner, auto-generated admin credentials, and TLS via cert-manager.
- **NATS**: Enhanced with full Grafana monitoring dashboards for JetStream and server metrics, Prometheus support with TLS-aware configuration, and updated image customization options.
- **MariaDB**: The `mysql` application is renamed to `mariadb`, accurately reflecting the underlying engine. An automatic migration (migration 27) converts all existing MySQL resources to use the `mariadb` naming.
FerretDB has been removed from the catalog as it is superseded by native MongoDB support.
### Multi-Location Networking with Kilo and cilium-kilo
Cozystack v1.0.0 introduces first-class support for multi-location clusters via the **Kilo** WireGuard mesh networking package. Kilo automatically establishes encrypted WireGuard tunnels between nodes in different network segments, enabling seamless cross-region communication.
A new integrated **`cilium-kilo`** networking variant combines Cilium eBPF CNI with Kilo's WireGuard overlay in a single platform configuration selection. This variant enables `enable-ipip-termination` in Cilium and deploys Kilo with `--compatibility=cilium`, allowing Cilium network policies to function correctly over the WireGuard mesh — without any manual configuration of the two components.
### Flux Sharding for Scalable Multi-Tenancy
Tenant HelmRelease reconciliation is now distributed across multiple Flux controllers via sharding labels. Each tenant workload is assigned to a shard based on a deterministic hash, preventing a single Flux controller from becoming a bottleneck in large multi-tenant environments. The platform operator manages the shard assignment automatically, and new shards can be added by scaling the Flux deployment.
## Major Features and Improvements
### Cozystack Operator
* **[cozystack-operator] Introduce Package and PackageSource APIs**: Added new CRDs for declarative package management, defining the full API for Package and PackageSource resources ([**@kvaps**](https://github.com/kvaps) in #1740, #1741, #1755, #1756, #1760, #1761).
* **[platform] Migrate from HelmRelease bundles to Package-based deployment**: Replaced HelmRelease bundle system with Package resources managed by cozystack-operator, including restructured values.yaml with full configuration support for networking, publishing, authentication, scheduling, branding, and resources ([**@kvaps**](https://github.com/kvaps) in #1816).
* **[cozystack-operator] Add automatic CRD installation at startup**: Added `--install-crds` flag to install embedded CRD manifests on every startup via server-side apply, ensuring CRDs and the PackageSource are always up to date ([**@lexfrei**](https://github.com/lexfrei) in #2060).
* **[installer] Remove CRDs from Helm chart, delegate lifecycle to operator**: The `cozy-installer` Helm chart no longer ships CRDs; CRD lifecycle is fully managed by the Cozystack operator ([**@lexfrei**](https://github.com/lexfrei) in #2074).
* **[cozystack-operator] Preserve existing suspend field in package reconciler**: Fixed package reconciler to properly preserve the suspend field state during reconciliation ([**@sircthulhu**](https://github.com/sircthulhu) in #2043).
* **[cozystack-operator] Fix namespace privileged flag resolution and field ownership**: Fixed operator to correctly check all Packages in a namespace when determining privileged status, and resolved SSA field ownership conflicts ([**@kvaps**](https://github.com/kvaps) in #2046).
* **[platform] Add flux-plunger controller**: Added flux-plunger controller to automatically fix stuck HelmRelease errors by cleaning up failed resources and retrying reconciliation ([**@kvaps**](https://github.com/kvaps) in #1843).
* **[installer] Add variant-aware templates for generic Kubernetes support**: Extended the installer to support generic and hosted Kubernetes deployments via the `cozystackOperator.variant=generic` parameter ([**@lexfrei**](https://github.com/lexfrei) in #2010).
* **[installer] Unify operator templates**: Merged separate operator templates into a single variant-based template supporting Talos and non-Talos deployments ([**@kvaps**](https://github.com/kvaps) in #2034).
### API and Platform
* **[api] Rename CozystackResourceDefinition to ApplicationDefinition**: Renamed CRD and all related types for clarity and consistency, with migration 24 handling the transition automatically ([**@kvaps**](https://github.com/kvaps) in #1864).
* **[platform] Add DNS-1035 validation for Application names**: Added dynamic DNS-1035 label validation for Application names at creation time, preventing resources with invalid names that would fail downstream ([**@lexfrei**](https://github.com/lexfrei) in #1771).
* **[platform] Make cluster issuer name and ACME solver configurable**: Added `publishing.certificates.solver` and `publishing.certificates.issuerName` parameters to allow pointing all ingress TLS annotations at any ClusterIssuer ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2077).
* **[platform] Add cilium-kilo networking variant**: Added integrated `cilium-kilo` networking variant combining Cilium CNI with Kilo WireGuard mesh overlay ([**@kvaps**](https://github.com/kvaps) in #2064).
* **[cozystack-api] Switch from DaemonSet to Deployment**: Migrated cozystack-api to a Deployment with PreferClose topology spread constraints, reducing resource consumption while maintaining high availability ([**@kvaps**](https://github.com/kvaps) in #2041, #2048).
### Virtual Machines
* **[vm-instance] Complete migration from virtual-machine to vm-disk and vm-instance**: Fully migrated from `virtual-machine` to the new `vm-disk` and `vm-instance` architecture, with automatic migration script (migration 28) for existing VMs ([**@kvaps**](https://github.com/kvaps) in #2040).
* **[kubevirt-csi-driver] Add RWX Filesystem (NFS) support**: Added Read-Write-Many filesystem support to kubevirt-csi-driver via automatic NFS server deployment per PVC ([**@kvaps**](https://github.com/kvaps) in #2042).
* **[vm] Add cpuModel field to specify CPU model without instanceType**: Added cpuModel field to VirtualMachine API for granular CPU control ([**@sircthulhu**](https://github.com/sircthulhu) in #2007).
* **[vm] Allow switching between instancetype and custom resources**: Implemented atomic upgrade hook for switching between instanceType-based and custom resource VM configurations ([**@sircthulhu**](https://github.com/sircthulhu) in #2008).
* **[vm] Migrate to runStrategy instead of running**: Migrated VirtualMachine API from deprecated `running` field to `runStrategy` ([**@sircthulhu**](https://github.com/sircthulhu) in #2004).
* **[vm] Always expose VMs with a service**: Virtual machines are now always exposed with at least a ClusterIP service, ensuring in-cluster DNS names ([**@lllamnyp**](https://github.com/lllamnyp) in #1738, #1751).
* **[dashboard] VMInstance dropdowns for disks and instanceType**: VM instance creation form now renders API-backed dropdowns for `instanceType` and disk `name` fields ([**@sircthulhu**](https://github.com/sircthulhu) in #2071).
### Backup System
* **[backups] Implement comprehensive backup and restore functionality**: Core backup Plan controller, Velero strategy controller, RestoreJob resource with end-to-end restore workflows, and enhanced backup plans UI ([**@lllamnyp**](https://github.com/lllamnyp) in #1640, #1685, #1687, #1719, #1720, #1737, #1967; [**@androndo**](https://github.com/androndo) in #1762, #1967, #1968, #1811).
* **[backups] Add kubevirt plugin to velero**: Added KubeVirt plugin to Velero for consistent VM state and data snapshots ([**@lllamnyp**](https://github.com/lllamnyp) in #2017).
* **[backups] Install backupstrategy controller by default**: Enabled backupstrategy controller by default for automatic backup scheduling ([**@lllamnyp**](https://github.com/lllamnyp) in #2020).
* **[backups] Better selectors for VM strategy**: Improved VM backup strategy selectors for accurate and reliable backup targeting ([**@lllamnyp**](https://github.com/lllamnyp) in #2023).
* **[backups] Create RBAC for backup resources**: Added comprehensive RBAC configuration for backup operations and restore jobs ([**@lllamnyp**](https://github.com/lllamnyp) in #2018).
### Networking
* **[kilo] Introduce Kilo WireGuard mesh networking**: Added Kilo as a system package providing secure WireGuard-based VPN mesh for connecting Kubernetes nodes across different networks and regions ([**@kvaps**](https://github.com/kvaps) in #1691).
* **[kilo] Add Cilium compatibility variant**: Added `cilium` variant enabling Cilium-aware IPIP encapsulation for full network policy enforcement with Kilo mesh ([**@kvaps**](https://github.com/kvaps) in #2055).
* **[kilo] Update to v0.8.0 with configurable MTU**: Updated Kilo to v0.8.0 with configurable MTU parameter and performance improvements ([**@kvaps**](https://github.com/kvaps) in #2003, #2049, #2053).
* **[local-ccm] Add local-ccm package**: Added local cloud controller manager for managing load balancer services in bare-metal environments ([**@kvaps**](https://github.com/kvaps) in #1831).
* **[local-ccm] Add node-lifecycle-controller component**: Added optional node-lifecycle-controller that automatically deletes unreachable NotReady nodes, solving the "zombie" node problem in autoscaled clusters ([**@IvanHunters**](https://github.com/IvanHunters) in #1992).
* **[tenant] Allow egress to parent ingress pods**: Updated tenant network policies to allow egress traffic to parent cluster ingress pods ([**@lexfrei**](https://github.com/lexfrei) in #1765, #1776).
### New Applications
* **[mongodb] Add MongoDB managed application**: Added MongoDB as a fully managed database with replica sets, persistent storage, and unified user/database configuration ([**@lexfrei**](https://github.com/lexfrei) in #1822; [**@kvaps**](https://github.com/kvaps) in #1923).
* **[qdrant] Add Qdrant vector database**: Added Qdrant as a high-performance vector database for AI/ML workloads with API key authentication and optional LoadBalancer access ([**@lexfrei**](https://github.com/lexfrei) in #1987).
* **[harbor] Add managed Harbor container registry**: Added Harbor v2.14.2 as a managed tenant-level container registry with CloudNativePG, Redis operator, COSI BucketClaim storage, and Trivy scanner ([**@lexfrei**](https://github.com/lexfrei) in #2058).
* **[nats] Add monitoring**: Added Grafana dashboards for NATS JetStream and server metrics, Prometheus monitoring with TLS support ([**@klinch0**](https://github.com/klinch0) in #1381).
* **[mariadb] Rename mysql application to mariadb**: Renamed MySQL application to MariaDB with automatic migration (migration 27) for all existing resources ([**@kvaps**](https://github.com/kvaps) in #2026).
* **[ferretdb] Remove FerretDB application**: Removed FerretDB, superseded by native MongoDB support ([**@kvaps**](https://github.com/kvaps) in #2028).
### Kubernetes and System Components
* **[kubernetes] Update supported Kubernetes versions to v1.30v1.35**: Updated the tenant Kubernetes version matrix, with v1.35 as the new default. Kamaji updated to edge-26.2.4 and CAPI Kamaji provider to v0.16.0 ([**@lexfrei**](https://github.com/lexfrei) in #2073).
* **[kubernetes] Auto-enable Gateway API support in cert-manager**: Added automatic Gateway API support in cert-manager for tenant clusters ([**@kvaps**](https://github.com/kvaps) in #1997).
* **[kubernetes] Use ingress-nginx nodeport service**: Changed tenant Kubernetes clusters to use ingress-nginx NodePort service for improved compatibility ([**@sircthulhu**](https://github.com/sircthulhu) in #1948).
* **[system] Add cluster-autoscaler for Hetzner and Azure**: Added cluster-autoscaler system package for automatically scaling management cluster nodes on Hetzner and Azure ([**@kvaps**](https://github.com/kvaps) in #1964).
* **[cluster-autoscaler] Enable enforce-node-group-min-size by default**: Ensures node groups are always scaled up to their configured minimum size ([**@kvaps**](https://github.com/kvaps) in #2050).
* **[system] Add clustersecret-operator package**: Added clustersecret-operator for managing secrets across multiple namespaces ([**@sircthulhu**](https://github.com/sircthulhu) in #2025).
### Monitoring
* **[monitoring] Enable monitoring for core components**: Enhanced monitoring capabilities with dashboards and metrics for core Cozystack components ([**@IvanHunters**](https://github.com/IvanHunters) in #1937).
* **[monitoring] Add SLACK_SEVERITY_FILTER and VMAgent for tenant monitoring**: Added SLACK_SEVERITY_FILTER for Slack alert filtering and VMAgent for tenant namespace metrics scraping ([**@IvanHunters**](https://github.com/IvanHunters) in #1712).
* **[monitoring-agents] Fix FQDN resolution for tenant workload clusters**: Fixed monitoring agents in tenant clusters to use full DNS names with cluster domain suffix ([**@IvanHunters**](https://github.com/IvanHunters) in #2075; [**@kvaps**](https://github.com/kvaps) in #2086).
### Storage
* **[linstor] Move CRDs to dedicated piraeus-operator-crds chart**: Moved LINSTOR CRDs to a dedicated chart, ensuring reliable installation of all CRDs including `linstorsatellites.io` ([**@kvaps**](https://github.com/kvaps) in #2036; [**@IvanHunters**](https://github.com/IvanHunters) in #1991).
* **[seaweedfs] Increase certificate duration to 10 years**: Increased SeaweedFS certificate validity to 10 years to reduce rotation overhead ([**@IvanHunters**](https://github.com/IvanHunters) in #1986).
## Improvements
* **[dashboard] Upgrade dashboard to version 1.4.0**: Updated Cozystack dashboard to v1.4.0 with new features and improvements ([**@sircthulhu**](https://github.com/sircthulhu) in #2051).
* **[dashboard] Hide Ingresses/Services/Secrets tabs when no selectors defined**: Tabs are now conditionally shown based on whether the ApplicationDefinition has resource selectors configured, reducing UI clutter ([**@kvaps**](https://github.com/kvaps) in #2087).
* **[dashboard] Add startupProbe to prevent container restarts on slow hardware**: Added startup probe to dashboard pods to prevent unnecessary restarts ([**@kvaps**](https://github.com/kvaps) in #1996).
* **[keycloak] Allow custom Ingress hostname via values**: Added `ingress.host` field to cozy-keycloak chart values for overriding the default `keycloak.<root-host>` hostname ([**@sircthulhu**](https://github.com/sircthulhu) in #2101).
* **[branding] Separate values for Keycloak**: Separated Keycloak branding values for better customization capabilities ([**@nbykov0**](https://github.com/nbykov0) in #1947).
* **[rbac] Use hierarchical naming scheme**: Refactored RBAC to use hierarchical naming for cluster roles and role bindings ([**@lllamnyp**](https://github.com/lllamnyp) in #2019).
* **[tenant,rbac] Use shared clusterroles**: Refactored tenant RBAC to use shared ClusterRoles for improved consistency ([**@lllamnyp**](https://github.com/lllamnyp) in #1999).
* **[kubernetes] Increase default apiServer resourcesPreset to large**: Increased kube-apiserver resource preset to `large` for more reliable operation under higher workloads ([**@kvaps**](https://github.com/kvaps) in #1875).
* **[kubernetes] Increase kube-apiserver startup probe threshold**: Increased startup probe threshold to allow more time for API server readiness ([**@kvaps**](https://github.com/kvaps) in #1876).
* **[etcd] Increase probe thresholds for better recovery**: Increased etcd probe thresholds to improve cluster resilience during temporary slowdowns ([**@kvaps**](https://github.com/kvaps) in #1874).
* **[etcd-operator] Add vertical-pod-autoscaler dependency**: Added VPA as a dependency to etcd-operator for proper resource scaling ([**@sircthulhu**](https://github.com/sircthulhu) in #2047).
* **[cilium] Change cilium-operator replicas to 1**: Reduced Cilium operator replicas to decrease resource consumption in smaller deployments ([**@IvanHunters**](https://github.com/IvanHunters) in #1784).
* **[keycloak-configure,dashboard] Enable insecure TLS verification by default**: Made SSL certificate verification configurable with insecure mode enabled by default for local development ([**@IvanHunters**](https://github.com/IvanHunters) in #2005).
* **[platform] Split telemetry between operator and controller**: Separated telemetry collection for better metrics isolation ([**@kvaps**](https://github.com/kvaps) in #1869).
* **[system] Add resource requests and limits to etcd-defrag**: Added resource requests and limits to etcd-defrag job to prevent resource contention ([**@matthieu-robin**](https://github.com/matthieu-robin) in #1785, #1786).
## Fixes
* **[dashboard] Fix sidebar visibility on cluster-level pages**: Fixed broken URLs with double `//` on cluster-level pages by hiding namespace-scoped sidebar items when no tenant is selected ([**@sircthulhu**](https://github.com/sircthulhu) in #2106).
* **[platform] Fix upgrade issues in migrations, etcd timeout, and migration script**: Fixed multiple upgrade failures discovered during v0.41.1 → v1.0 upgrade testing, including migration 26-29 fixes, RFC3339 format for annotations, and extended etcd HelmRelease timeout to 30m ([**@kvaps**](https://github.com/kvaps) in #2096).
* **[platform] Fix orphaned -rd HelmReleases after application renames**: Migrations 28-29 updated to remove orphaned `-rd` HelmReleases in `cozy-system` after `ferretdb→mongodb`, `mysql→mariadb`, and `virtual-machine→vm-disk+vm-instance` renames, with migration 33 as a safety net ([**@kvaps**](https://github.com/kvaps) in #2102).
* **[platform] Adopt tenant-root into cozystack-basics during migration**: Added migration 31 to adopt existing `tenant-root` Namespace and HelmRelease into `cozystack-basics` for a safe v0.41.x → v1.0 upgrade path ([**@kvaps**](https://github.com/kvaps) in #2065).
* **[platform] Preserve tenant-root HelmRelease during migration**: Fixed data-loss risk during migration where `tenant-root` HelmRelease could be deleted ([**@sircthulhu**](https://github.com/sircthulhu) in #2063).
* **[platform] Fix cozystack-values secret race condition**: Fixed race condition in cozystack-values secret creation that could cause initialization failures ([**@lllamnyp**](https://github.com/lllamnyp) in #2024).
* **[cozystack-basics] Preserve existing HelmRelease values during reconciliations**: Fixed data-loss bug where changes to `tenant-root` HelmRelease were dropped on the next reconciliation ([**@sircthulhu**](https://github.com/sircthulhu) in #2068).
* **[cozystack-basics] Deny resourcequotas deletion for tenant admin**: Fixed `cozy:tenant:admin:base` ClusterRole to explicitly deny deletion of ResourceQuota objects ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2076).
* **[dashboard] Fix legacy templating and cluster identifier in sidebar links**: Standardized cluster identifier across dashboard menu links resolving broken link targets for Backups and External IPs ([**@androndo**](https://github.com/androndo) in #2093).
* **[dashboard] Fix backupjobs creation form and sidebar backup category identifier**: Fixed backup job creation form fields and fixed sidebar backup category identifier ([**@androndo**](https://github.com/androndo) in #2103).
* **[kubevirt] Update KubeVirt to v1.6.4 and CDI to v1.64.0, fix VM pod initialization**: Updated KubeVirt and CDI and disabled serial console logging globally to fix the `guest-console-log` init container blocking virt-launcher pods ([**@nbykov0**](https://github.com/nbykov0) in #1833; [**@kvaps**](https://github.com/kvaps)).
* **[linstor] Fix DRBD+LUKS+STORAGE resource creation failure**: Applied upstream fix for all newly created encrypted volumes failing due to missing `setExists(true)` call in `LuksLayer` ([**@kvaps**](https://github.com/kvaps) in #2072).
* **[platform] Clean up Helm secrets for removed releases**: Added cleanup logic to migration 23 to remove orphaned Helm secrets from removed `-rd` releases ([**@kvaps**](https://github.com/kvaps) in #2035).
* **[monitoring] Fix YAML parse error in vmagent template**: Fixed YAML parsing error in monitoring-agents vmagent template ([**@kvaps**](https://github.com/kvaps) in #2037).
* **[monitoring] Remove cozystack-controller dependency**: Fixed monitoring package to remove unnecessary cozystack-controller dependency ([**@IvanHunters**](https://github.com/IvanHunters) in #1990).
* **[monitoring] Remove duplicate dashboards.list**: Fixed duplicate dashboards.list configuration in extra/monitoring package ([**@IvanHunters**](https://github.com/IvanHunters) in #2016).
* **[linstor] Update piraeus-server patches with critical fixes**: Backported critical patches fixing edge cases in device management and DRBD resource handling ([**@kvaps**](https://github.com/kvaps) in #1850).
* **[apiserver] Fix Watch resourceVersion and bookmark handling**: Fixed Watch API handling of resourceVersion and bookmarks for proper event streaming ([**@kvaps**](https://github.com/kvaps) in #1860).
* **[bootbox] Auto-create bootbox-application as dependency**: Fixed bootbox package to automatically create required bootbox-application dependency ([**@kvaps**](https://github.com/kvaps) in #1974).
* **[postgres-operator] Correct PromQL syntax in CNPGClusterOffline alert**: Fixed incorrect PromQL syntax in the CNPGClusterOffline Prometheus alert ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #1981).
* **[coredns] Fix serviceaccount to match kubernetes bootstrap RBAC**: Fixed CoreDNS service account to correctly match Kubernetes bootstrap RBAC requirements ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #1958).
* **[dashboard] Verify JWT token**: Added JWT token verification to dashboard for improved security ([**@lllamnyp**](https://github.com/lllamnyp) in #1980).
* **[codegen] Fix missing gen_client in update-codegen.sh**: Fixed build error in `pkg/generated/applyconfiguration/utils.go` by including `gen_client` in the codegen script ([**@lexfrei**](https://github.com/lexfrei) in #2061).
* **[kubevirt-operator] Fix typo in VMNotRunningFor10Minutes alert**: Fixed typo in VM alert name ensuring proper alert triggering ([**@lexfrei**](https://github.com/lexfrei) in #1770, #1775).
## Security
* **[dashboard] Verify JWT token**: Added JWT token verification to the dashboard for improved authentication security ([**@lllamnyp**](https://github.com/lllamnyp) in #1980).
## Dependencies
* **[cilium] Update to v1.18.6**: Updated Cilium CNI to v1.18.6 with security fixes and performance improvements ([**@sircthulhu**](https://github.com/sircthulhu) in #1868).
* **[kube-ovn] Update to v1.15.3**: Updated Kube-OVN CNI to v1.15.3 with performance improvements and bug fixes ([**@kvaps**](https://github.com/kvaps) in #2022).
* **[kilo] Update to v0.8.0**: Updated Kilo WireGuard mesh to v0.8.0 with performance improvements and new compatibility features ([**@kvaps**](https://github.com/kvaps) in #2053).
* **Update Talos Linux to v1.12.1**: Updated Talos Linux to v1.12.1 with latest features and security patches ([**@kvaps**](https://github.com/kvaps) in #1877).
## System Configuration
* **[vpc] Migrate subnets definition from map to array format**: Migrated VPC subnets from `map[string]Subnet` to `[]Subnet` with explicit `name` field, with automatic migration via migration 30 ([**@kvaps**](https://github.com/kvaps) in #2052).
* **[migrations] Add migrations 23-33 for v1.0 upgrade path**: Added 11 incremental migrations handling CRD ownership, resource renaming, secret cleanup, Helm adoption, and configuration conversion for the v0.41.x → v1.0.0 upgrade path ([**@kvaps**](https://github.com/kvaps) in #1975, #2035, #2036, #2040, #2026, #2065, #2052, #2102).
* **[tenant] Run cleanup job from system namespace**: Moved tenant cleanup job to system namespace for improved security and resource isolation ([**@lllamnyp**](https://github.com/lllamnyp) in #1774, #1777).
## Development, Testing, and CI/CD
* **[ci] Use GitHub Copilot CLI for changelog generation**: Automated changelog generation using GitHub Copilot CLI ([**@androndo**](https://github.com/androndo) in #1753).
* **[ci] Choose runner conditional on label**: Added conditional runner selection in CI based on PR labels ([**@lllamnyp**](https://github.com/lllamnyp) in #1998).
* **[e2e] Use helm install instead of kubectl apply for cozystack installation**: Replaced static YAML apply flow with direct `helm upgrade --install` of the installer chart in E2E tests ([**@lexfrei**](https://github.com/lexfrei) in #2060).
* **[e2e] Make kubernetes test retries effective by cleaning up stale resources**: Fixed E2E test retries by adding pre-creation cleanup and increasing deployment wait timeout to 300s ([**@lexfrei**](https://github.com/lexfrei) in #2062).
* **[e2e] Increase HelmRelease readiness timeout for kubernetes test**: Increased HelmRelease readiness timeout to prevent false failures on slower hardware ([**@lexfrei**](https://github.com/lexfrei) in #2033).
* **[ci] Improve cozyreport functionality**: Enhanced cozyreport tool with improved reporting for CI/CD pipelines ([**@lllamnyp**](https://github.com/lllamnyp) in #2032).
* **feat(cozypkg): add cross-platform build targets with version injection**: Added cross-platform build targets for cozypkg/cozyhr tool for linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 ([**@kvaps**](https://github.com/kvaps) in #1862).
* **refactor: move scripts to hack directory**: Reorganized scripts to the standard `hack/` location ([**@kvaps**](https://github.com/kvaps) in #1863).
* **Update CODEOWNERS**: Updated CODEOWNERS to include new maintainers ([**@lllamnyp**](https://github.com/lllamnyp) in #1972; [**@IvanHunters**](https://github.com/IvanHunters) in #2015).
* **[talm] Skip config loading for completion subcommands**: Fixed talm CLI to skip config loading for shell completion commands ([**@kitsunoff**](https://github.com/kitsunoff) in cozystack/talm#109).
* **[talm] Fix metadata.id type casting in physical_links_info**: Fixed Prometheus query to properly cast metadata.id to string for regexMatch operations ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#110).
## Documentation
* **[website] Add documentation versioning**: Implemented comprehensive documentation versioning with separate v0 and v1 documentation trees and a version selector in the UI ([**@IvanStukov**](https://github.com/IvanStukov) in cozystack/website#415).
* **[website] Describe upgrade to v1.0**: Added detailed upgrade instructions for migrating from v0.x to v1.0 ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website@21bbe84).
* **[website] Migrate ConfigMap references to Platform Package in v1 docs**: Updated entire v1 documentation to replace legacy ConfigMap-based configuration with the new Platform Package API ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#426).
* **[website] Add generic Kubernetes deployment guide for v1**: Added installation guide for deploying Cozystack on any generic Kubernetes cluster ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#408).
* **[website] Describe operator-based and HelmRelease-based package patterns**: Added development documentation explaining operator-based and HelmRelease-based package patterns ([**@kvaps**](https://github.com/kvaps) in cozystack/website#413).
* **[website] Add Helm chart development principles guide**: Added developer guide documenting Cozystack's four core Helm chart principles ([**@kvaps**](https://github.com/kvaps) in cozystack/website#418).
* **[website] Add network architecture overview**: Added comprehensive network architecture documentation covering the multi-layered networking stack with Mermaid diagrams ([**@IvanHunters**](https://github.com/IvanHunters) in cozystack/website#422).
* **[website] Add LINSTOR disk preparation guide**: Added comprehensive documentation for preparing disks for LINSTOR storage ([**@IvanHunters**](https://github.com/IvanHunters) in cozystack/website#411).
* **[website] Add Proxmox VM migration guide**: Added detailed guide for migrating virtual machines from Proxmox to Cozystack ([**@IvanHunters**](https://github.com/IvanHunters) in cozystack/website#410).
* **[website] Add cluster autoscaler documentation**: Added documentation for Hetzner setup with Talos, vSwitch, and Kilo mesh integration ([**@kvaps**](https://github.com/kvaps) in #1964).
* **[website] Improve Azure autoscaling troubleshooting guide**: Enhanced Azure autoscaling documentation with serial console instructions and `az vmss update --custom-data` guidance ([**@kvaps**](https://github.com/kvaps) in cozystack/website#424).
* **[website] Update multi-location documentation for cilium-kilo variant**: Updated multi-location networking docs to reflect the integrated `cilium-kilo` variant selection ([**@kvaps**](https://github.com/kvaps) in cozystack/website@02d63f0).
* **[website] Update documentation to use jsonpatch for service exposure**: Improved `kubectl patch` commands to use JSON Patch `add` operations ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#427).
* **[website] Update certificates section in Platform Package documentation**: Updated certificate configuration docs to reflect new `solver` and `issuerName` fields ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#429).
* **[website] Add tenant Kubernetes cluster log querying guide**: Added documentation for querying logs from tenant clusters in Grafana using VictoriaLogs labels ([**@IvanHunters**](https://github.com/IvanHunters) in cozystack/website#430).
* **[website] Replace non-idempotent commands with idempotent alternatives**: Updated `helm install` to `helm upgrade --install` and `kubectl create` to `kubectl apply` across all installation guides ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#431).
* **[website] Fix broken documentation links with .md suffix**: Fixed incorrect internal links across virtualization guides for v0 and v1 documentation ([**@cheese**](https://github.com/cheese) in cozystack/website#432).
* **[website] Refactor resource planning documentation**: Improved resource planning guide with clearer structure and more comprehensive coverage ([**@IvanStukov**](https://github.com/IvanStukov) in cozystack/website#423).
* **[website] Add ServiceAccount API access documentation and update FAQ**: Added documentation for ServiceAccount API access token configuration and updated FAQ ([**@IvanStukov**](https://github.com/IvanStukov) in cozystack/website#421).
* **[website] Update networking-mesh allowed-location-ips example**: Replaced provider-specific CLI with standard `kubectl` commands in multi-location networking guide ([**@kvaps**](https://github.com/kvaps) in cozystack/website#425).
* **[website] docs(storage): simplify NFS driver setup instructions**: Simplified NFS driver setup documentation ([**@kvaps**](https://github.com/kvaps) in cozystack/website#399).
* **[website] Add Hetzner RobotLB documentation**: Added documentation for configuring public IP with Hetzner RobotLB ([**@kvaps**](https://github.com/kvaps) in cozystack/website#394).
* **[website] Add documentation for creating and managing cloned VMs**: Added comprehensive guide for VM cloning operations ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#401).
* **[website] Update Talos installation docs for Hetzner and Servers.com**: Updated installation documentation for Hetzner and Servers.com environments ([**@kvaps**](https://github.com/kvaps) in cozystack/website#395).
* **[website] Add Hidora organization support details**: Added Hidora to the support page ([**@matthieu-robin**](https://github.com/matthieu-robin) in cozystack/website#397, cozystack/website#398).
* **[website] Check quotas before an upgrade**: Added troubleshooting documentation for checking resource quotas before upgrades ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website#405).
* **[website] Update support documentation**: Updated support documentation with current contact information ([**@xrmtech-isk**](https://github.com/xrmtech-isk) in cozystack/website#420).
* **[website] Correct typo in kubeconfig reference in Kubernetes installation guide**: Fixed documentation typo in kubeconfig reference ([**@shkarface**](https://github.com/shkarface) in cozystack/website#414).
## Breaking Changes & Upgrade Notes
* **[api] CozystackResourceDefinition renamed to ApplicationDefinition**: The `CozystackResourceDefinition` CRD has been renamed to `ApplicationDefinition`. Migration 24 handles the transition automatically during upgrade ([**@kvaps**](https://github.com/kvaps) in #1864).
* **[platform] Certificate issuer configuration parameters renamed**: The `publishing.certificates.issuerType` field is renamed to `publishing.certificates.solver`, and the value `cloudflare` is renamed to `dns01`. A new `publishing.certificates.issuerName` field (default: `letsencrypt-prod`) is added. Migration 32 automatically converts existing configurations — no manual action required ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2077).
* **[vpc] VPC subnets definition migrated from map to array format**: VPC subnets are now defined as `[]Subnet` with an explicit `name` field instead of `map[string]Subnet`. Migration 30 handles the conversion automatically ([**@kvaps**](https://github.com/kvaps) in #2052).
* **[vm] virtual-machine application replaced by vm-disk and vm-instance**: The legacy `virtual-machine` application has been fully replaced. Migration 28 automatically converts existing VMs to the new architecture ([**@kvaps**](https://github.com/kvaps) in #2040).
* **[mysql] mysql application renamed to mariadb**: Existing MySQL deployments are automatically renamed to MariaDB via migration 27 ([**@kvaps**](https://github.com/kvaps) in #2026).
### Upgrade Guide
To upgrade from v0.41.x to v1.0.0:
1. **Backup your cluster** before upgrading.
2. Run the provided migration script: `hack/migrate-to-version-1.0.sh`.
3. The 33 incremental migration steps will automatically handle all resource renaming, configuration conversion, CRD adoption, and secret cleanup.
4. Refer to the [upgrade documentation](https://cozystack.io/docs/v1/upgrade) for detailed instructions and troubleshooting.
## Contributors
We'd like to thank all contributors who made this release possible:
* [**@androndo**](https://github.com/androndo)
* [**@cheese**](https://github.com/cheese)
* [**@IvanHunters**](https://github.com/IvanHunters)
* [**@IvanStukov**](https://github.com/IvanStukov)
* [**@kitsunoff**](https://github.com/kitsunoff)
* [**@klinch0**](https://github.com/klinch0)
* [**@kvaps**](https://github.com/kvaps)
* [**@lexfrei**](https://github.com/lexfrei)
* [**@lllamnyp**](https://github.com/lllamnyp)
* [**@matthieu-robin**](https://github.com/matthieu-robin)
* [**@mattia-eleuteri**](https://github.com/mattia-eleuteri)
* [**@myasnikovdaniil**](https://github.com/myasnikovdaniil)
* [**@nbykov0**](https://github.com/nbykov0)
* [**@shkarface**](https://github.com/shkarface)
* [**@sircthulhu**](https://github.com/sircthulhu)
* [**@xrmtech-isk**](https://github.com/xrmtech-isk)
### New Contributors
We're excited to welcome our first-time contributors:
* [**@cheese**](https://github.com/cheese) - First contribution!
* [**@IvanStukov**](https://github.com/IvanStukov) - First contribution!
* [**@kitsunoff**](https://github.com/kitsunoff) - First contribution!
* [**@shkarface**](https://github.com/shkarface) - First contribution!
* [**@xrmtech-isk**](https://github.com/xrmtech-isk) - First contribution!
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.41.0...v1.0.0

21
docs/changelogs/v1.0.1.md Normal file
View File

@@ -0,0 +1,21 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.0.1
-->
## Fixes
* **[platform] Prevent cozystack-version ConfigMap from deletion**: Added resource protection to prevent the `cozystack-version` ConfigMap from being accidentally deleted, improving platform stability and reliability ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2112, #2114).
* **[installer] Add keep annotation to Namespace and update migration script**: Added `helm.sh/resource-policy: keep` annotation to the `cozy-system` Namespace in the installer Helm chart to prevent Helm from deleting the namespace (and all HelmReleases within it) when the installer release is removed. The v1.0 migration script is also updated to annotate the `cozy-system` namespace and `cozystack-version` ConfigMap with this policy before migration ([**@kvaps**](https://github.com/kvaps) in #2122, #2123).
* **[dashboard] Add FlowSchema to exempt BFF from API throttling**: Added a `cozy-dashboard-exempt` FlowSchema to exempt the dashboard Back-End-for-Frontend (BFF) service account from Kubernetes API Priority and Fairness throttling. Previously, the BFF fell under the `workload-low` priority level, causing 429 (Too Many Requests) errors under load, resulting in dashboard unresponsiveness ([**@kvaps**](https://github.com/kvaps) in #2121, #2124).
## Documentation
* **[website] Replace bundles documentation with variants**: Renamed the "Bundles" documentation section to "Variants" to match current Cozystack terminology. Removed deprecated variants (`iaas-full`, `distro-full`, `distro-hosted`) and added new variants: `default` (PackageSources only, for manual package management via cozypkg) and `isp-full-generic` (full PaaS/IaaS on k3s, kubeadm, or RKE2). Updated all cross-references throughout the documentation ([**@kvaps**](https://github.com/kvaps) in cozystack/website#433).
* **[website] Add step to protect namespace before upgrading**: Updated the cluster upgrade guide and v0.41→v1.0 migration guide with a required step to annotate the `cozy-system` namespace and `cozystack-version` ConfigMap with `helm.sh/resource-policy=keep` before running `helm upgrade`, preventing accidental namespace deletion ([**@kvaps**](https://github.com/kvaps) in cozystack/website#435).
---
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.0...v1.0.1

19
docs/changelogs/v1.0.2.md Normal file
View File

@@ -0,0 +1,19 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.0.2
-->
## Fixes
* **[platform] Suspend cozy-proxy if it conflicts with installer release during migration**: Added a check in the v0.41→v1.0 migration script to detect and automatically suspend the `cozy-proxy` HelmRelease when its `releaseName` is set to `cozystack`, which conflicts with the installer release and would cause `cozystack-operator` deletion during the upgrade ([**@kvaps**](https://github.com/kvaps) in #2128, #2130).
* **[platform] Fix off-by-one error in run-migrations script**: Fixed a bug in the migration runner where the first required migration was always skipped due to an off-by-one error in the migration range calculation, ensuring all upgrade steps execute correctly ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2126, #2132).
* **[system] Fix Keycloak proxy configuration for v26.x**: Replaced the deprecated `KC_PROXY=edge` environment variable with `KC_PROXY_HEADERS=xforwarded` and `KC_HTTP_ENABLED=true` in the Keycloak StatefulSet template. `KC_PROXY` was removed in Keycloak 26.x, previously causing "Non-secure context detected" warnings and broken cookie handling when running behind a reverse proxy with TLS termination ([**@sircthulhu**](https://github.com/sircthulhu) in #2125, #2134).
* **[dashboard] Allow clearing instanceType field and preserve newlines in secret copy**: Added `allowEmpty: true` to the `instanceType` field in the VMInstance form so users can explicitly clear it to use custom KubeVirt resources without a named instance type. Also fixed newline preservation when copying secrets with CMD+C ([**@sircthulhu**](https://github.com/sircthulhu) in #2135, #2137).
* **[dashboard] Restore stock-instance sidebars for namespace-level pages**: Restored `stock-instance-api-form`, `stock-instance-api-table`, `stock-instance-builtin-form`, and `stock-instance-builtin-table` sidebar resources that were inadvertently removed in #2106. Without these sidebars, namespace-level pages such as Backup Plans rendered as empty pages with no interactive content ([**@sircthulhu**](https://github.com/sircthulhu) in #2136, #2138).
---
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.1...v1.0.2

3
go.mod
View File

@@ -29,7 +29,7 @@ require (
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
)
require (
@@ -125,7 +125,6 @@ require (
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

5
go.sum
View File

@@ -81,7 +81,6 @@ github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -324,13 +323,9 @@ sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327U
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -10,7 +10,11 @@ PATTERN=${2:-*}
LINE='----------------------------------------------------------------'
cols() { stty size 2>/dev/null | awk '{print $2}' || echo 80; }
MAXW=$(( $(cols) - 12 )); [ "$MAXW" -lt 40 ] && MAXW=70
if [ -t 1 ]; then
MAXW=$(( $(cols) - 12 )); [ "$MAXW" -lt 40 ] && MAXW=70
else
MAXW=0 # no truncation when not a tty (e.g. CI)
fi
BEGIN=$(date +%s)
timestamp() { s=$(( $(date +%s) - BEGIN )); printf '[%02d:%02d]' $((s/60)) $((s%60)); }
@@ -45,7 +49,7 @@ run_one() {
*) out=$line ;;
esac
now=$(( $(date +%s) - START ))
[ ${#out} -gt "$MAXW" ] && out="$(printf '%.*s…' "$MAXW" "$out")"
[ "$MAXW" -gt 0 ] && [ ${#out} -gt "$MAXW" ] && out="$(printf '%.*s…' "$MAXW" "$out")"
printf '┊[%02d:%02d] %s\n' $((now/60)) $((now%60)) "$out"
done

View File

@@ -83,6 +83,8 @@ modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//flux/flux-stats
modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//kafka/strimzi-kafka.json
modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//seaweedfs/seaweedfs.json
modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//goldpinger/goldpinger.json
modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//nats/nats-jetstream.json
modules/340-monitoring-kubernetes/monitoring/grafana-dashboards//nats/nats-server.json
EOT

74
hack/e2e-apps/harbor.bats Normal file
View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bats
@test "Create Harbor" {
name='test'
release="harbor-$name"
# Clean up stale resources from previous failed runs
kubectl -n tenant-test delete harbor.apps.cozystack.io $name 2>/dev/null || true
kubectl -n tenant-test wait hr $release --timeout=60s --for=delete 2>/dev/null || true
kubectl apply -f- <<EOF
apiVersion: apps.cozystack.io/v1alpha1
kind: Harbor
metadata:
name: $name
namespace: tenant-test
spec:
host: ""
storageClass: ""
core:
resources: {}
resourcesPreset: "nano"
registry:
resources: {}
resourcesPreset: "nano"
jobservice:
resources: {}
resourcesPreset: "nano"
trivy:
enabled: false
size: 2Gi
resources: {}
resourcesPreset: "nano"
database:
size: 2Gi
replicas: 1
redis:
size: 1Gi
replicas: 1
EOF
sleep 5
kubectl -n tenant-test wait hr $release --timeout=60s --for=condition=ready
# Wait for COSI to provision bucket
kubectl -n tenant-test wait bucketclaims.objectstorage.k8s.io $release-registry \
--timeout=300s --for=jsonpath='{.status.bucketReady}'=true
kubectl -n tenant-test wait bucketaccesses.objectstorage.k8s.io $release-registry \
--timeout=60s --for=jsonpath='{.status.accessGranted}'=true
kubectl -n tenant-test wait hr $release-system --timeout=600s --for=condition=ready || {
echo "=== HelmRelease status ==="
kubectl -n tenant-test get hr $release-system -o yaml 2>&1 || true
echo "=== Pods ==="
kubectl -n tenant-test get pods 2>&1 || true
echo "=== Events ==="
kubectl -n tenant-test get events --sort-by='.lastTimestamp' 2>&1 | tail -30 || true
echo "=== ExternalArtifact ==="
kubectl -n cozy-system get externalartifact cozystack-harbor-application-default-harbor-system -o yaml 2>&1 || true
echo "=== BucketClaim status ==="
kubectl -n tenant-test get bucketclaims.objectstorage.k8s.io $release-registry -o yaml 2>&1 || true
echo "=== BucketAccess status ==="
kubectl -n tenant-test get bucketaccesses.objectstorage.k8s.io $release-registry -o yaml 2>&1 || true
echo "=== BucketAccess Secret ==="
kubectl -n tenant-test get secret $release-registry-bucket -o jsonpath='{.data.BucketInfo}' 2>&1 | base64 -d 2>&1 || true
false
}
kubectl -n tenant-test wait deploy $release-core --timeout=120s --for=condition=available
kubectl -n tenant-test wait deploy $release-registry --timeout=120s --for=condition=available
kubectl -n tenant-test wait deploy $release-portal --timeout=120s --for=condition=available
kubectl -n tenant-test get secret $release-credentials -o jsonpath='{.data.admin-password}' | base64 --decode | grep -q '.'
kubectl -n tenant-test get secret $release-credentials -o jsonpath='{.data.url}' | base64 --decode | grep -q 'https://'
kubectl -n tenant-test get svc $release -o jsonpath='{.spec.ports[0].port}' | grep -q '80'
kubectl -n tenant-test delete harbor.apps.cozystack.io $name
}

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bats
@test "Create OpenBAO (standalone)" {
name='test'
kubectl apply -f- <<EOF
apiVersion: apps.cozystack.io/v1alpha1
kind: OpenBAO
metadata:
name: $name
namespace: tenant-test
spec:
replicas: 1
size: 10Gi
storageClass: ""
resourcesPreset: "small"
resources: {}
external: false
ui: true
EOF
sleep 5
kubectl -n tenant-test wait hr openbao-$name --timeout=60s --for=condition=ready
kubectl -n tenant-test wait hr openbao-$name-system --timeout=120s --for=condition=ready
# Wait for container to be started (pod Running does not guarantee container is ready for exec on slow CI)
if ! timeout 120 sh -ec "until kubectl -n tenant-test get pod openbao-$name-0 --output jsonpath='{.status.containerStatuses[0].started}' 2>/dev/null | grep -q true; do sleep 5; done"; then
echo "=== DEBUG: Container did not start in time ===" >&2
kubectl -n tenant-test describe pod openbao-$name-0 >&2 || true
kubectl -n tenant-test logs openbao-$name-0 --previous >&2 || true
kubectl -n tenant-test logs openbao-$name-0 >&2 || true
return 1
fi
# Wait for OpenBAO API to accept connections
# bao status exit codes: 0 = unsealed, 1 = error/not ready, 2 = sealed but responsive
if ! timeout 60 sh -ec "until kubectl -n tenant-test exec openbao-$name-0 -- bao status >/dev/null 2>&1; rc=\$?; test \$rc -eq 0 -o \$rc -eq 2; do sleep 3; done"; then
echo "=== DEBUG: OpenBAO API did not become responsive ===" >&2
kubectl -n tenant-test describe pod openbao-$name-0 >&2 || true
kubectl -n tenant-test logs openbao-$name-0 --previous >&2 || true
kubectl -n tenant-test logs openbao-$name-0 >&2 || true
return 1
fi
# Initialize OpenBAO (single key share for testing simplicity)
init_output=$(kubectl -n tenant-test exec openbao-$name-0 -- bao operator init -key-shares=1 -key-threshold=1 -format=json)
unseal_key=$(echo "$init_output" | jq -r '.unseal_keys_b64[0]')
if [ -z "$unseal_key" ] || [ "$unseal_key" = "null" ]; then
echo "Failed to extract unseal key. Init output: $init_output" >&2
return 1
fi
# Unseal OpenBAO
kubectl -n tenant-test exec openbao-$name-0 -- bao operator unseal "$unseal_key"
# Now wait for pod to become ready (readiness probe checks seal status)
kubectl -n tenant-test wait sts openbao-$name --timeout=90s --for=jsonpath='{.status.readyReplicas}'=1
kubectl -n tenant-test wait pvc data-openbao-$name-0 --timeout=50s --for=jsonpath='{.status.phase}'=Bound
kubectl -n tenant-test delete openbao.apps.cozystack.io $name
kubectl -n tenant-test delete pvc data-openbao-$name-0 --ignore-not-found
}

View File

@@ -80,33 +80,41 @@ EOF
# Wait for the machine deployment to scale to 2 replicas (timeout after 1 minute)
kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=1m --for=jsonpath='{.status.replicas}'=2
# Get the admin kubeconfig and save it to a file
kubectl get secret kubernetes-${test_name}-admin-kubeconfig -ojsonpath='{.data.super-admin\.conf}' -n tenant-test | base64 -d > tenantkubeconfig-${test_name}
kubectl get secret kubernetes-${test_name}-admin-kubeconfig -ojsonpath='{.data.super-admin\.conf}' -n tenant-test | base64 -d > "tenantkubeconfig-${test_name}"
# Update the kubeconfig to use localhost for the API server
yq -i ".clusters[0].cluster.server = \"https://localhost:${port}\"" tenantkubeconfig-${test_name}
yq -i ".clusters[0].cluster.server = \"https://localhost:${port}\"" "tenantkubeconfig-${test_name}"
# Set up port forwarding to the Kubernetes API server for a 200 second timeout
# Kill any stale port-forward on this port from a previous retry
pkill -f "port-forward.*${port}:" 2>/dev/null || true
sleep 1
# Set up port forwarding to the Kubernetes API server
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'
# Wait for the nodes to be ready (timeout after 2 minutes)
timeout 3m bash -c '
until [ "$(kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -eq 2 ]; do
# Wait for at least 2 nodes to join (timeout after 8 minutes)
timeout 8m bash -c '
until [ "$(kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -ge 2 ]; do
sleep 2
done
'
# Verify the nodes are ready
kubectl --kubeconfig tenantkubeconfig-${test_name} wait node --all --timeout=2m --for=condition=Ready
kubectl --kubeconfig tenantkubeconfig-${test_name} get nodes -o wide
if ! kubectl --kubeconfig "tenantkubeconfig-${test_name}" wait node --all --timeout=2m --for=condition=Ready; then
# Additional debug messages
kubectl --kubeconfig "tenantkubeconfig-${test_name}" describe nodes
kubectl -n tenant-test get hr
fi
kubectl --kubeconfig "tenantkubeconfig-${test_name}" get nodes -o wide
# Verify the kubelet version matches what we expect
versions=$(kubectl --kubeconfig "tenantkubeconfig-${test_name}" \
get nodes -o jsonpath='{.items[*].status.nodeInfo.kubeletVersion}')
node_ok=true
for v in $versions; do
case "$v" in
"${k8s_version}" | "${k8s_version}".* | "${k8s_version}"-*)
@@ -125,15 +133,21 @@ EOF
fi
kubectl --kubeconfig tenantkubeconfig-${test_name} apply -f - <<EOF
kubectl --kubeconfig "tenantkubeconfig-${test_name}" apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: tenant-test
EOF
# Clean up backend resources from any previous failed attempt
kubectl delete deployment --kubeconfig "tenantkubeconfig-${test_name}" "${test_name}-backend" \
-n tenant-test --ignore-not-found --timeout=60s || true
kubectl delete service --kubeconfig "tenantkubeconfig-${test_name}" "${test_name}-backend" \
-n tenant-test --ignore-not-found --timeout=60s || true
# Backend 1
kubectl apply --kubeconfig tenantkubeconfig-${test_name} -f- <<EOF
kubectl apply --kubeconfig "tenantkubeconfig-${test_name}" -f- <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -165,7 +179,7 @@ spec:
EOF
# LoadBalancer Service
kubectl apply --kubeconfig tenantkubeconfig-${test_name} -f- <<EOF
kubectl apply --kubeconfig "tenantkubeconfig-${test_name}" -f- <<EOF
apiVersion: v1
kind: Service
metadata:
@@ -182,8 +196,8 @@ spec:
EOF
# Wait for pods readiness
kubectl wait deployment --kubeconfig tenantkubeconfig-${test_name} ${test_name}-backend -n tenant-test --for=condition=Available --timeout=90s
kubectl wait deployment --kubeconfig "tenantkubeconfig-${test_name}" "${test_name}-backend" -n tenant-test --for=condition=Available --timeout=300s
# 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 \
@@ -193,7 +207,7 @@ EOF
"
LB_ADDR=$(
kubectl get svc --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" \
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}'
)
@@ -215,11 +229,17 @@ fi
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
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
# Clean up NFS test resources from any previous failed attempt
kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pod nfs-test-pod \
-n tenant-test --ignore-not-found --timeout=60s || true
kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pvc nfs-test-pvc \
-n tenant-test --ignore-not-found --timeout=60s || true
# Test RWX NFS mount in tenant cluster (uses kubevirt CSI driver with RWX support)
kubectl --kubeconfig tenantkubeconfig-${test_name} apply -f - <<EOF
kubectl --kubeconfig "tenantkubeconfig-${test_name}" apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -235,10 +255,10 @@ spec:
EOF
# Wait for PVC to be bound
kubectl --kubeconfig tenantkubeconfig-${test_name} wait pvc nfs-test-pvc -n tenant-test --timeout=2m --for=jsonpath='{.status.phase}'=Bound
kubectl --kubeconfig "tenantkubeconfig-${test_name}" wait pvc nfs-test-pvc -n tenant-test --timeout=2m --for=jsonpath='{.status.phase}'=Bound
# Create Pod that writes and reads data from NFS volume
kubectl --kubeconfig tenantkubeconfig-${test_name} apply -f - <<EOF
kubectl --kubeconfig "tenantkubeconfig-${test_name}" apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
@@ -260,20 +280,20 @@ spec:
EOF
# Wait for Pod to complete successfully
kubectl --kubeconfig tenantkubeconfig-${test_name} wait pod nfs-test-pod -n tenant-test --timeout=5m --for=jsonpath='{.status.phase}'=Succeeded
kubectl --kubeconfig "tenantkubeconfig-${test_name}" wait pod nfs-test-pod -n tenant-test --timeout=5m --for=jsonpath='{.status.phase}'=Succeeded
# Verify NFS data integrity
nfs_result=$(kubectl --kubeconfig tenantkubeconfig-${test_name} logs nfs-test-pod -n tenant-test)
nfs_result=$(kubectl --kubeconfig "tenantkubeconfig-${test_name}" logs nfs-test-pod -n tenant-test)
if [ "$nfs_result" != "nfs-mount-ok" ]; then
echo "NFS mount test failed: expected 'nfs-mount-ok', got '$nfs_result'" >&2
kubectl --kubeconfig tenantkubeconfig-${test_name} delete pod nfs-test-pod -n tenant-test --wait=false 2>/dev/null || true
kubectl --kubeconfig tenantkubeconfig-${test_name} delete pvc nfs-test-pvc -n tenant-test --wait=false 2>/dev/null || true
kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pod nfs-test-pod -n tenant-test --wait=false 2>/dev/null || true
kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pvc nfs-test-pvc -n tenant-test --wait=false 2>/dev/null || true
exit 1
fi
# Cleanup NFS test resources in tenant cluster
kubectl --kubeconfig tenantkubeconfig-${test_name} delete pod nfs-test-pod -n tenant-test --wait
kubectl --kubeconfig tenantkubeconfig-${test_name} delete pvc nfs-test-pvc -n tenant-test
kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pod nfs-test-pod -n tenant-test --wait
kubectl --kubeconfig "tenantkubeconfig-${test_name}" delete pvc nfs-test-pvc -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

@@ -1,27 +1,32 @@
#!/usr/bin/env bats
@test "Required installer assets exist" {
if [ ! -f _out/assets/cozystack-crds.yaml ]; then
echo "Missing: _out/assets/cozystack-crds.yaml" >&2
exit 1
fi
if [ ! -f _out/assets/cozystack-operator-talos.yaml ]; then
echo "Missing: _out/assets/cozystack-operator-talos.yaml" >&2
@test "Required installer chart exists" {
if [ ! -f packages/core/installer/Chart.yaml ]; then
echo "Missing: packages/core/installer/Chart.yaml" >&2
exit 1
fi
}
@test "Install Cozystack" {
# Create namespace
kubectl create namespace cozy-system --dry-run=client -o yaml | kubectl apply -f -
# Install cozy-installer chart (operator installs CRDs on startup via --install-crds)
helm upgrade installer packages/core/installer \
--install \
--namespace cozy-system \
--create-namespace \
--wait \
--timeout 2m
# Apply installer manifests (CRDs + operator)
kubectl apply -f _out/assets/cozystack-crds.yaml
kubectl apply -f _out/assets/cozystack-operator-talos.yaml
# Wait for the operator deployment to become available
# Verify the operator deployment is available
kubectl wait deployment/cozystack-operator -n cozy-system --timeout=1m --for=condition=Available
# Wait for operator to install CRDs (happens at startup before reconcile loop).
# kubectl wait fails immediately if the CRD does not exist yet, so poll until it appears first.
timeout 120 sh -ec 'until kubectl wait crd/packages.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
timeout 120 sh -ec 'until kubectl wait crd/packagesources.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
# Wait for operator to create the platform PackageSource
timeout 120 sh -ec 'until kubectl get packagesource cozystack.cozystack-platform >/dev/null 2>&1; do sleep 2; done'
# Create platform Package with isp-full variant
kubectl apply -f - <<EOF
apiVersion: cozystack.io/v1alpha1

View File

@@ -32,6 +32,54 @@ if ! kubectl get namespace "$NAMESPACE" &> /dev/null; then
exit 1
fi
# Step 0: Annotate critical resources to prevent Helm from deleting them
echo "Step 0: Protect critical resources from Helm deletion"
echo ""
echo "The following resources will be annotated with helm.sh/resource-policy=keep"
echo "to prevent Helm from deleting them when the installer release is removed:"
echo " - Namespace: $NAMESPACE"
echo " - ConfigMap: $NAMESPACE/cozystack-version"
echo ""
read -p "Do you want to annotate these resources? (y/N) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Annotating namespace $NAMESPACE..."
kubectl annotate namespace "$NAMESPACE" helm.sh/resource-policy=keep --overwrite
echo "Annotating ConfigMap cozystack-version..."
kubectl annotate configmap -n "$NAMESPACE" cozystack-version helm.sh/resource-policy=keep --overwrite 2>/dev/null || echo " ConfigMap cozystack-version not found, skipping."
echo ""
echo "Resources annotated successfully."
else
echo "WARNING: Skipping annotation. If you remove the Helm installer release,"
echo "the namespace and its contents may be deleted!"
fi
echo ""
# Step 1: Check for cozy-proxy HelmRelease with conflicting releaseName
# In v0.41.x, cozy-proxy was incorrectly configured with releaseName "cozystack",
# which conflicts with the installer helm release name. If not suspended, cozy-proxy
# HelmRelease will overwrite the installer release and delete cozystack-operator.
COZY_PROXY_RELEASE_NAME=$(kubectl get hr -n "$NAMESPACE" cozy-proxy -o jsonpath='{.spec.releaseName}' 2>/dev/null || true)
if [ "$COZY_PROXY_RELEASE_NAME" = "cozystack" ]; then
echo "WARNING: HelmRelease cozy-proxy has releaseName 'cozystack', which conflicts"
echo "with the installer release. It must be suspended before proceeding, otherwise"
echo "it will overwrite the installer and delete cozystack-operator."
echo ""
read -p "Suspend HelmRelease cozy-proxy? (y/N) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
kubectl -n "$NAMESPACE" patch hr cozy-proxy --type=merge --field-manager=flux-client-side-apply -p '{"spec":{"suspend":true}}'
echo "HelmRelease cozy-proxy suspended."
else
echo "ERROR: Cannot proceed with conflicting cozy-proxy HelmRelease active."
echo "Please suspend it manually:"
echo " kubectl -n $NAMESPACE patch hr cozy-proxy --type=merge -p '{\"spec\":{\"suspend\":true}}'"
exit 1
fi
echo ""
fi
# Read ConfigMap cozystack
echo "Reading ConfigMap cozystack..."
COZYSTACK_CM=$(kubectl get configmap -n "$NAMESPACE" cozystack -o json 2>/dev/null || echo "{}")
@@ -52,6 +100,43 @@ OIDC_ENABLED=$(echo "$COZYSTACK_CM" | jq -r '.data["oidc-enabled"] // "false"')
KEYCLOAK_REDIRECTS=$(echo "$COZYSTACK_CM" | jq -r '.data["extra-keycloak-redirect-uri-for-dashboard"] // ""' )
TELEMETRY_ENABLED=$(echo "$COZYSTACK_CM" | jq -r '.data["telemetry-enabled"] // "true"')
BUNDLE_NAME=$(echo "$COZYSTACK_CM" | jq -r '.data["bundle-name"] // "paas-full"')
BUNDLE_DISABLE=$(echo "$COZYSTACK_CM" | jq -r '.data["bundle-disable"] // ""')
BUNDLE_ENABLE=$(echo "$COZYSTACK_CM" | jq -r '.data["bundle-enable"] // ""')
EXPOSE_INGRESS=$(echo "$COZYSTACK_CM" | jq -r '.data["expose-ingress"] // "tenant-root"')
EXPOSE_SERVICES=$(echo "$COZYSTACK_CM" | jq -r '.data["expose-services"] // ""')
# Certificate issuer configuration (old undocumented field: clusterissuer)
OLD_CLUSTER_ISSUER=$(echo "$COZYSTACK_CM" | jq -r '.data["clusterissuer"] // ""')
# Convert old clusterissuer value to new solver/issuerName fields
SOLVER=""
ISSUER_NAME=""
case "$OLD_CLUSTER_ISSUER" in
cloudflare)
SOLVER="dns01"
ISSUER_NAME="letsencrypt-prod"
;;
http01)
SOLVER="http01"
ISSUER_NAME="letsencrypt-prod"
;;
"")
# Field not set; omit from Package so chart defaults apply
;;
*)
# Unrecognised value — treat as custom ClusterIssuer name with no solver override
ISSUER_NAME="$OLD_CLUSTER_ISSUER"
;;
esac
# Build certificates YAML block (empty string when no override needed)
if [ -n "$SOLVER" ] || [ -n "$ISSUER_NAME" ]; then
CERTIFICATES_SECTION=" certificates:
solver: \"${SOLVER}\"
issuerName: \"${ISSUER_NAME}\""
else
CERTIFICATES_SECTION=""
fi
# Network configuration
POD_CIDR=$(echo "$COZYSTACK_CM" | jq -r '.data["ipv4-pod-cidr"] // "10.244.0.0/16"')
@@ -66,21 +151,24 @@ else
EXTERNAL_IPS=$(echo "$EXTERNAL_IPS" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}')
fi
# Determine bundle type
case "$BUNDLE_NAME" in
paas-full|distro-full)
SYSTEM_ENABLED="true"
SYSTEM_TYPE="full"
;;
paas-hosted|distro-hosted)
SYSTEM_ENABLED="false"
SYSTEM_TYPE="hosted"
;;
*)
SYSTEM_ENABLED="false"
SYSTEM_TYPE="hosted"
;;
esac
# Convert comma-separated lists to YAML arrays
if [ -z "$BUNDLE_DISABLE" ]; then
DISABLED_PACKAGES="[]"
else
DISABLED_PACKAGES=$(echo "$BUNDLE_DISABLE" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}')
fi
if [ -z "$BUNDLE_ENABLE" ]; then
ENABLED_PACKAGES="[]"
else
ENABLED_PACKAGES=$(echo "$BUNDLE_ENABLE" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}')
fi
if [ -z "$EXPOSE_SERVICES" ]; then
EXPOSED_SERVICES_YAML="[]"
else
EXPOSED_SERVICES_YAML=$(echo "$EXPOSE_SERVICES" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}')
fi
# Update bundle naming
BUNDLE_NAME=$(echo "$BUNDLE_NAME" | sed 's/paas/isp/')
@@ -108,8 +196,8 @@ echo " Root Host: $ROOT_HOST"
echo " API Server Endpoint: $API_SERVER_ENDPOINT"
echo " OIDC Enabled: $OIDC_ENABLED"
echo " Bundle Name: $BUNDLE_NAME"
echo " System Enabled: $SYSTEM_ENABLED"
echo " System Type: $SYSTEM_TYPE"
echo " Certificate Solver: ${SOLVER:-http01 (default)}"
echo " Issuer Name: ${ISSUER_NAME:-letsencrypt-prod (default)}"
echo ""
# Generate Package YAML
@@ -125,15 +213,8 @@ spec:
platform:
values:
bundles:
system:
enabled: $SYSTEM_ENABLED
type: "$SYSTEM_TYPE"
iaas:
enabled: true
paas:
enabled: true
naas:
enabled: true
disabledPackages: $DISABLED_PACKAGES
enabledPackages: $ENABLED_PACKAGES
networking:
clusterDomain: "$CLUSTER_DOMAIN"
podCIDR: "$POD_CIDR"
@@ -142,8 +223,11 @@ spec:
joinCIDR: "$JOIN_CIDR"
publishing:
host: "$ROOT_HOST"
ingressName: "$EXPOSE_INGRESS"
exposedServices: $EXPOSED_SERVICES_YAML
apiServerEndpoint: "$API_SERVER_ENDPOINT"
externalIPs: $EXTERNAL_IPS
${CERTIFICATES_SECTION}
authentication:
oidc:
enabled: $OIDC_ENABLED

View File

@@ -24,7 +24,7 @@ API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-ru
UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}"
CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4"
TMPDIR=$(mktemp -d)
OPERATOR_CRDDIR=packages/core/installer/definitions
OPERATOR_CRDDIR=internal/crdinstall/manifests
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions
COZY_RD_CRDDIR=packages/system/application-definition-crd/definition
BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions
@@ -34,7 +34,7 @@ trap 'rm -rf ${TMPDIR}' EXIT
source "${CODEGEN_PKG}/kube_codegen.sh"
THIS_PKG="k8s.io/sample-apiserver"
THIS_PKG="github.com/cozystack/cozystack"
kube::codegen::gen_helpers \
--boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \
@@ -60,6 +60,13 @@ kube::codegen::gen_openapi \
--boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \
"${SCRIPT_ROOT}/pkg/apis"
kube::codegen::gen_client \
--with-applyconfig \
--output-dir "${SCRIPT_ROOT}/pkg/generated" \
--output-pkg "${THIS_PKG}/pkg/generated" \
--boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \
"${SCRIPT_ROOT}/pkg/apis"
$CONTROLLER_GEN object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=${TMPDIR}

View File

@@ -156,7 +156,7 @@ menuItems = append(menuItems, map[string]any{
map[string]any{
"key": "{plural}",
"label": "{ResourceLabel}",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/{group}/{version}/{plural}",
"link": "/openapi-ui/{cluster}/{namespace}/api-table/{group}/{version}/{plural}",
},
},
}),
@@ -174,7 +174,7 @@ menuItems = append(menuItems, map[string]any{
**Important Notes**:
- The sidebar tag (`{lowercase-kind}-sidebar`) must match what the Factory uses
- The link format: `/openapi-ui/{clusterName}/{namespace}/api-table/{group}/{version}/{plural}`
- The link format: `/openapi-ui/{cluster}/{namespace}/api-table/{group}/{version}/{plural}`
- All sidebars share the same `keysAndTags` and `menuItems`, so changes affect all sidebar instances
### Step 4: Verify Integration

View File

@@ -33,12 +33,12 @@ func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.Applic
key := plural // e.g., "virtualmachines"
label := labelPlural
link := fmt.Sprintf("/openapi-ui/{clusterName}/{namespace}/api-table/%s/%s/%s", strings.ToLower(group), strings.ToLower(version), plural)
link := fmt.Sprintf("/openapi-ui/{cluster}/{namespace}/api-table/%s/%s/%s", strings.ToLower(group), strings.ToLower(version), plural)
// If this is a module, change the first breadcrumb item to "Tenant Modules"
if crd.Spec.Dashboard != nil && crd.Spec.Dashboard.Module {
key = "tenantmodules"
label = "Tenant Modules"
link = "/openapi-ui/{clusterName}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules"
link = "/openapi-ui/{cluster}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules"
}
items := []any{

View File

@@ -46,8 +46,11 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph
}
}
// Build schema with multilineString for string fields without enum
// Parse OpenAPI schema once for reuse
l := log.FromContext(ctx)
openAPIProps := parseOpenAPIProperties(crd.Spec.Application.OpenAPISchema)
// Build schema with multilineString for string fields without enum
schema, err := buildMultilineStringSchema(crd.Spec.Application.OpenAPISchema)
if err != nil {
// If schema parsing fails, log the error and use an empty schema
@@ -55,6 +58,9 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph
schema = map[string]any{}
}
// Override specific fields with API-backed dropdowns (listInput type)
applyListInputOverrides(schema, kind, openAPIProps)
spec := map[string]any{
"customizationId": customizationID,
"hidden": hidden,
@@ -84,6 +90,53 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph
return err
}
// ensureCFOMapping updates the CFOMapping resource to include a mapping for the given CRD
func (m *Manager) ensureCFOMapping(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
g, v, kind := pickGVK(crd)
plural := pickPlural(kind, crd)
resourcePath := fmt.Sprintf("/%s/%s/%s", g, v, plural)
customizationID := fmt.Sprintf("default-%s", resourcePath)
obj := &dashv1alpha1.CFOMapping{}
obj.SetName("cfomapping")
_, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error {
// Parse existing mappings
mappings := make(map[string]string)
if obj.Spec.JSON.Raw != nil {
var spec map[string]any
if err := json.Unmarshal(obj.Spec.JSON.Raw, &spec); err == nil {
if m, ok := spec["mappings"].(map[string]any); ok {
for k, val := range m {
if s, ok := val.(string); ok {
mappings[k] = s
}
}
}
}
}
// Add/update the mapping for this CRD
mappings[resourcePath] = customizationID
specData := map[string]any{
"mappings": mappings,
}
b, err := json.Marshal(specData)
if err != nil {
return err
}
newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
if !compareArbitrarySpecs(obj.Spec, newSpec) {
obj.Spec = newSpec
}
return nil
})
return err
}
// buildMultilineStringSchema parses OpenAPI schema and creates schema with multilineString
// for all string fields inside spec that don't have enum
func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
@@ -129,6 +182,130 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
return schema, nil
}
// applyListInputOverrides injects listInput type overrides into the schema
// for fields that should be rendered as API-backed dropdowns in the dashboard.
// openAPIProps are the parsed top-level properties from the OpenAPI schema.
func applyListInputOverrides(schema map[string]any, kind string, openAPIProps map[string]any) {
switch kind {
case "VMInstance":
specProps := ensureSchemaPath(schema, "spec")
field := map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/instancetype.kubevirt.io/v1beta1/virtualmachineclusterinstancetypes",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
"allowEmpty": true,
},
}
if prop, _ := openAPIProps["instanceType"].(map[string]any); prop != nil {
if def := prop["default"]; def != nil {
field["default"] = def
}
}
specProps["instanceType"] = field
// Override disks[].name to be an API-backed dropdown listing VMDisk resources
disksItemProps := ensureArrayItemProps(specProps, "disks")
disksItemProps["name"] = map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
},
}
case "ClickHouse", "Harbor", "HTTPCache", "Kubernetes", "MariaDB", "MongoDB",
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis", "VMDisk":
specProps := ensureSchemaPath(schema, "spec")
specProps["storageClass"] = storageClassListInput()
case "FoundationDB":
storageProps := ensureSchemaPath(schema, "spec", "storage")
storageProps["storageClass"] = storageClassListInput()
case "Kafka":
kafkaProps := ensureSchemaPath(schema, "spec", "kafka")
kafkaProps["storageClass"] = storageClassListInput()
zkProps := ensureSchemaPath(schema, "spec", "zookeeper")
zkProps["storageClass"] = storageClassListInput()
}
}
// storageClassListInput returns a listInput field config for a storageClass dropdown
// backed by the cluster's available StorageClasses.
func storageClassListInput() map[string]any {
return map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/storage.k8s.io/v1/storageclasses",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
},
}
}
// ensureArrayItemProps ensures that parentProps[fieldName].items.properties exists
// and returns the items properties map. Used for overriding fields inside array items.
func ensureArrayItemProps(parentProps map[string]any, fieldName string) map[string]any {
field, ok := parentProps[fieldName].(map[string]any)
if !ok {
field = map[string]any{}
parentProps[fieldName] = field
}
items, ok := field["items"].(map[string]any)
if !ok {
items = map[string]any{}
field["items"] = items
}
props, ok := items["properties"].(map[string]any)
if !ok {
props = map[string]any{}
items["properties"] = props
}
return props
}
// parseOpenAPIProperties parses the top-level properties from an OpenAPI schema JSON string.
func parseOpenAPIProperties(openAPISchema string) map[string]any {
if openAPISchema == "" {
return nil
}
var root map[string]any
if err := json.Unmarshal([]byte(openAPISchema), &root); err != nil {
return nil
}
props, _ := root["properties"].(map[string]any)
return props
}
// ensureSchemaPath ensures the nested properties structure exists in a schema
// and returns the innermost properties map.
// e.g. ensureSchemaPath(schema, "spec") returns schema["properties"]["spec"]["properties"]
func ensureSchemaPath(schema map[string]any, segments ...string) map[string]any {
current := schema
for _, seg := range segments {
props, ok := current["properties"].(map[string]any)
if !ok {
props = map[string]any{}
current["properties"] = props
}
child, ok := props[seg].(map[string]any)
if !ok {
child = map[string]any{}
props[seg] = child
}
current = child
}
props, ok := current["properties"].(map[string]any)
if !ok {
props = map[string]any{}
current["properties"] = props
}
return props
}
// processSpecProperties recursively processes spec properties and adds multilineString type
// for string fields without enum
func processSpecProperties(props map[string]any, schemaProps map[string]any) {

View File

@@ -169,3 +169,301 @@ func TestBuildMultilineStringSchemaInvalidJSON(t *testing.T) {
t.Errorf("Expected nil schema for invalid JSON, got %v", schema)
}
}
func TestApplyListInputOverrides_VMInstance(t *testing.T) {
openAPIProps := map[string]any{
"instanceType": map[string]any{"type": "string", "default": "u1.medium"},
}
schema := map[string]any{}
applyListInputOverrides(schema, "VMInstance", openAPIProps)
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
instanceType, ok := specProps["instanceType"].(map[string]any)
if !ok {
t.Fatal("instanceType not found in schema.properties.spec.properties")
}
if instanceType["type"] != "listInput" {
t.Errorf("expected type listInput, got %v", instanceType["type"])
}
if instanceType["default"] != "u1.medium" {
t.Errorf("expected default u1.medium, got %v", instanceType["default"])
}
customProps, ok := instanceType["customProps"].(map[string]any)
if !ok {
t.Fatal("customProps not found")
}
expectedURI := "/api/clusters/{cluster}/k8s/apis/instancetype.kubevirt.io/v1beta1/virtualmachineclusterinstancetypes"
if customProps["valueUri"] != expectedURI {
t.Errorf("expected valueUri %s, got %v", expectedURI, customProps["valueUri"])
}
if customProps["allowEmpty"] != true {
t.Errorf("expected allowEmpty true, got %v", customProps["allowEmpty"])
}
// Check disks[].name is a listInput
disks, ok := specProps["disks"].(map[string]any)
if !ok {
t.Fatal("disks not found in schema.properties.spec.properties")
}
items, ok := disks["items"].(map[string]any)
if !ok {
t.Fatal("disks.items not found")
}
itemProps, ok := items["properties"].(map[string]any)
if !ok {
t.Fatal("disks.items.properties not found")
}
diskName, ok := itemProps["name"].(map[string]any)
if !ok {
t.Fatal("disks.items.properties.name not found")
}
if diskName["type"] != "listInput" {
t.Errorf("expected disks name type listInput, got %v", diskName["type"])
}
diskCustomProps, ok := diskName["customProps"].(map[string]any)
if !ok {
t.Fatal("disks name customProps not found")
}
expectedDiskURI := "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks"
if diskCustomProps["valueUri"] != expectedDiskURI {
t.Errorf("expected disks valueUri %s, got %v", expectedDiskURI, diskCustomProps["valueUri"])
}
}
func TestApplyListInputOverrides_StorageClassSimple(t *testing.T) {
for _, kind := range []string{
"ClickHouse", "Harbor", "HTTPCache", "Kubernetes", "MariaDB", "MongoDB",
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis", "VMDisk",
} {
t.Run(kind, func(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, kind, map[string]any{})
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
sc, ok := specProps["storageClass"].(map[string]any)
if !ok {
t.Fatalf("storageClass not found in spec.properties for kind %s", kind)
}
assertStorageClassListInput(t, sc)
})
}
}
func TestApplyListInputOverrides_StorageClassFoundationDB(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, "FoundationDB", map[string]any{})
storageProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)["storage"].(map[string]any)["properties"].(map[string]any)
sc, ok := storageProps["storageClass"].(map[string]any)
if !ok {
t.Fatal("storageClass not found in spec.storage.properties")
}
assertStorageClassListInput(t, sc)
}
func TestApplyListInputOverrides_StorageClassKafka(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, "Kafka", map[string]any{})
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
kafkaSC, ok := specProps["kafka"].(map[string]any)["properties"].(map[string]any)["storageClass"].(map[string]any)
if !ok {
t.Fatal("storageClass not found in spec.kafka.properties")
}
assertStorageClassListInput(t, kafkaSC)
zkSC, ok := specProps["zookeeper"].(map[string]any)["properties"].(map[string]any)["storageClass"].(map[string]any)
if !ok {
t.Fatal("storageClass not found in spec.zookeeper.properties")
}
assertStorageClassListInput(t, zkSC)
}
// assertStorageClassListInput verifies that a field is a correctly configured storageClass listInput.
func assertStorageClassListInput(t *testing.T, field map[string]any) {
t.Helper()
if field["type"] != "listInput" {
t.Errorf("expected type listInput, got %v", field["type"])
}
customProps, ok := field["customProps"].(map[string]any)
if !ok {
t.Fatal("customProps not found")
}
expectedURI := "/api/clusters/{cluster}/k8s/apis/storage.k8s.io/v1/storageclasses"
if customProps["valueUri"] != expectedURI {
t.Errorf("expected valueUri %s, got %v", expectedURI, customProps["valueUri"])
}
}
func TestApplyListInputOverrides_UnknownKind(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, "SomeOtherKind", map[string]any{})
if len(schema) != 0 {
t.Errorf("expected empty schema for unknown kind, got %v", schema)
}
}
func TestApplyListInputOverrides_NoDefault(t *testing.T) {
openAPIProps := map[string]any{
"instanceType": map[string]any{"type": "string"},
}
schema := map[string]any{}
applyListInputOverrides(schema, "VMInstance", openAPIProps)
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
instanceType := specProps["instanceType"].(map[string]any)
if _, exists := instanceType["default"]; exists {
t.Errorf("expected no default key, got %v", instanceType["default"])
}
}
func TestApplyListInputOverrides_MergesWithExistingSchema(t *testing.T) {
openAPIProps := map[string]any{
"instanceType": map[string]any{"type": "string", "default": "u1.medium"},
}
// Simulate schema that already has spec.properties from buildMultilineStringSchema
schema := map[string]any{
"properties": map[string]any{
"spec": map[string]any{
"properties": map[string]any{
"otherField": map[string]any{"type": "multilineString"},
},
},
},
}
applyListInputOverrides(schema, "VMInstance", openAPIProps)
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
// instanceType should be added
if _, ok := specProps["instanceType"].(map[string]any); !ok {
t.Fatal("instanceType not found after override")
}
// otherField should be preserved
otherField, ok := specProps["otherField"].(map[string]any)
if !ok {
t.Fatal("otherField was lost after override")
}
if otherField["type"] != "multilineString" {
t.Errorf("otherField type changed, got %v", otherField["type"])
}
}
func TestParseOpenAPIProperties(t *testing.T) {
t.Run("extracts properties", func(t *testing.T) {
props := parseOpenAPIProperties(`{"type":"object","properties":{"instanceType":{"type":"string","default":"u1.medium"}}}`)
field, _ := props["instanceType"].(map[string]any)
if field["default"] != "u1.medium" {
t.Errorf("expected default u1.medium, got %v", field["default"])
}
})
t.Run("empty string", func(t *testing.T) {
if props := parseOpenAPIProperties(""); props != nil {
t.Errorf("expected nil, got %v", props)
}
})
t.Run("invalid JSON", func(t *testing.T) {
if props := parseOpenAPIProperties("{bad"); props != nil {
t.Errorf("expected nil, got %v", props)
}
})
t.Run("no properties key", func(t *testing.T) {
if props := parseOpenAPIProperties(`{"type":"object"}`); props != nil {
t.Errorf("expected nil, got %v", props)
}
})
}
func TestEnsureSchemaPath(t *testing.T) {
t.Run("creates path from empty schema", func(t *testing.T) {
schema := map[string]any{}
props := ensureSchemaPath(schema, "spec")
props["field"] = "value"
// Verify structure: schema.properties.spec.properties.field
got := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)["field"]
if got != "value" {
t.Errorf("expected value, got %v", got)
}
})
t.Run("preserves existing nested properties", func(t *testing.T) {
schema := map[string]any{
"properties": map[string]any{
"spec": map[string]any{
"properties": map[string]any{
"existing": "keep",
},
},
},
}
props := ensureSchemaPath(schema, "spec")
if props["existing"] != "keep" {
t.Errorf("existing property lost, got %v", props["existing"])
}
})
t.Run("multi-level path", func(t *testing.T) {
schema := map[string]any{}
props := ensureSchemaPath(schema, "spec", "nested")
props["deep"] = true
got := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)["nested"].(map[string]any)["properties"].(map[string]any)["deep"]
if got != true {
t.Errorf("expected true, got %v", got)
}
})
}
func TestEnsureArrayItemProps(t *testing.T) {
t.Run("creates from empty parent", func(t *testing.T) {
parent := map[string]any{}
props := ensureArrayItemProps(parent, "disks")
props["name"] = map[string]any{"type": "listInput"}
got := parent["disks"].(map[string]any)["items"].(map[string]any)["properties"].(map[string]any)["name"].(map[string]any)["type"]
if got != "listInput" {
t.Errorf("expected listInput, got %v", got)
}
})
t.Run("preserves existing item properties", func(t *testing.T) {
parent := map[string]any{
"disks": map[string]any{
"items": map[string]any{
"properties": map[string]any{
"bus": map[string]any{"type": "string"},
},
},
},
}
props := ensureArrayItemProps(parent, "disks")
props["name"] = map[string]any{"type": "listInput"}
if props["bus"].(map[string]any)["type"] != "string" {
t.Error("existing bus property was lost")
}
if props["name"].(map[string]any)["type"] != "listInput" {
t.Error("name property was not added")
}
})
}

View File

@@ -47,7 +47,7 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Applicati
if prefix, ok := vncTabPrefix(kind); ok {
tabs = append(tabs, vncTab(prefix))
}
tabs = append(tabs, yamlTab(plural))
tabs = append(tabs, yamlTab(g, v, plural))
// Use unified factory creation
config := UnifiedResourceConfig{
@@ -160,11 +160,11 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "vpc-subnets-table",
"baseprefix": "/openapi-ui",
"clusterNamePartOfUrl": "{2}",
"customizationId": "virtualprivatecloud-subnets",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/configmaps",
"id": "vpc-subnets-table",
"baseprefix": "/openapi-ui",
"cluster": "{2}",
"customizationId": "virtualprivatecloud-subnets",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/configmaps",
"fieldSelector": map[string]any{
"metadata.name": "virtualprivatecloud-{6}-subnets",
},
@@ -188,12 +188,12 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "resource-quotas-table",
"baseprefix": "/openapi-ui",
"clusterNamePartOfUrl": "{2}",
"customizationId": "factory-resource-quotas",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{reqsJsonPath[0]['.status.namespace']}/resourcequotas",
"pathToItems": []any{`items`},
"id": "resource-quotas-table",
"baseprefix": "/openapi-ui",
"cluster": "{2}",
"customizationId": "factory-resource-quotas",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{reqsJsonPath[0]['.status.namespace']}/resourcequotas",
"pathToItems": []any{`items`},
},
},
}),
@@ -242,13 +242,13 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "conditions-table",
"fetchUrl": endpoint,
"clusterNamePartOfUrl": "{2}",
"customizationId": "factory-status-conditions",
"baseprefix": "/openapi-ui",
"withoutControls": true,
"pathToItems": []any{"status", "conditions"},
"id": "conditions-table",
"fetchUrl": endpoint,
"cluster": "{2}",
"customizationId": "factory-status-conditions",
"baseprefix": "/openapi-ui",
"withoutControls": true,
"pathToItems": []any{"status", "conditions"},
},
},
}),
@@ -264,12 +264,12 @@ func workloadsTab(kind string) map[string]any {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "workloads-table",
"fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloadmonitors",
"clusterNamePartOfUrl": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1alpha1.cozystack.io.workloadmonitors",
"pathToItems": []any{"items"},
"id": "workloads-table",
"fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloadmonitors",
"cluster": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1alpha1.cozystack.io.workloadmonitors",
"pathToItems": []any{"items"},
"labelSelector": map[string]any{
"apps.cozystack.io/application.group": "apps.cozystack.io",
"apps.cozystack.io/application.kind": kind,
@@ -289,12 +289,12 @@ func servicesTab(kind string) map[string]any {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "services-table",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services",
"clusterNamePartOfUrl": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1.services",
"pathToItems": []any{"items"},
"id": "services-table",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services",
"cluster": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1.services",
"pathToItems": []any{"items"},
"labelSelector": map[string]any{
"apps.cozystack.io/application.group": "apps.cozystack.io",
"apps.cozystack.io/application.kind": kind,
@@ -315,12 +315,12 @@ func ingressesTab(kind string) map[string]any {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "ingresses-table",
"fetchUrl": "/api/clusters/{2}/k8s/apis/networking.k8s.io/v1/namespaces/{3}/ingresses",
"clusterNamePartOfUrl": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-networking.k8s.io.v1.ingresses",
"pathToItems": []any{"items"},
"id": "ingresses-table",
"fetchUrl": "/api/clusters/{2}/k8s/apis/networking.k8s.io/v1/namespaces/{3}/ingresses",
"cluster": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-networking.k8s.io.v1.ingresses",
"pathToItems": []any{"items"},
"labelSelector": map[string]any{
"apps.cozystack.io/application.group": "apps.cozystack.io",
"apps.cozystack.io/application.kind": kind,
@@ -341,12 +341,12 @@ func secretsTab(kind string) map[string]any {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "secrets-table",
"fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecrets",
"clusterNamePartOfUrl": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecrets",
"pathToItems": []any{"items"},
"id": "secrets-table",
"fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecrets",
"cluster": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecrets",
"pathToItems": []any{"items"},
"labelSelector": map[string]any{
"apps.cozystack.io/application.group": "apps.cozystack.io",
"apps.cozystack.io/application.kind": kind,
@@ -358,7 +358,7 @@ func secretsTab(kind string) map[string]any {
}
}
func yamlTab(plural string) map[string]any {
func yamlTab(group, version, plural string) map[string]any {
return map[string]any{
"key": "yaml",
"label": "YAML",
@@ -369,8 +369,10 @@ func yamlTab(plural string) map[string]any {
"id": "yaml-editor",
"cluster": "{2}",
"isNameSpaced": true,
"type": "builtin",
"typeName": plural,
"type": "apis",
"apiGroup": group,
"apiVersion": version,
"plural": plural,
"prefillValuesRequestIndex": float64(0),
"readOnly": true,
"substractHeight": float64(400),
@@ -580,15 +582,14 @@ type factoryFlags struct {
Secrets bool
}
// factoryFeatureFlags tries several conventional locations so you can evolve the API
// without breaking the controller. Defaults are false (hidden).
// factoryFeatureFlags determines which tabs to show based on whether the
// ApplicationDefinition has non-empty Include resource selectors.
// Workloads tab is always shown.
func factoryFeatureFlags(crd *cozyv1alpha1.ApplicationDefinition) factoryFlags {
var f factoryFlags
f.Workloads = true
f.Ingresses = true
f.Services = true
f.Secrets = true
return f
return factoryFlags{
Workloads: true,
Ingresses: len(crd.Spec.Ingresses.Include) > 0,
Services: len(crd.Spec.Services.Include) > 0,
Secrets: len(crd.Spec.Secrets.Include) > 0,
}
}

View File

@@ -132,6 +132,10 @@ func (m *Manager) EnsureForAppDef(ctx context.Context, crd *cozyv1alpha1.Applica
return reconcile.Result{}, err
}
if err := m.ensureCFOMapping(ctx, crd); err != nil {
return reconcile.Result{}, err
}
if err := m.ensureSidebar(ctx, crd); err != nil {
return reconcile.Result{}, err
}
@@ -139,6 +143,10 @@ func (m *Manager) EnsureForAppDef(ctx context.Context, crd *cozyv1alpha1.Applica
if err := m.ensureFactory(ctx, crd); err != nil {
return reconcile.Result{}, err
}
if err := m.ensureNavigation(ctx, crd); err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
@@ -291,10 +299,6 @@ func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.ApplicationDefini
// Add other stock sidebars that are created for each CRD
stockSidebars := []string{
"stock-instance-api-form",
"stock-instance-api-table",
"stock-instance-builtin-form",
"stock-instance-builtin-table",
"stock-project-factory-marketplace",
"stock-project-factory-workloadmonitor-details",
"stock-project-api-form",
@@ -303,6 +307,10 @@ func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.ApplicationDefini
"stock-project-builtin-table",
"stock-project-crd-form",
"stock-project-crd-table",
"stock-instance-api-form",
"stock-instance-api-table",
"stock-instance-builtin-form",
"stock-instance-builtin-table",
}
for _, sidebarID := range stockSidebars {
expected["Sidebar"][sidebarID] = true

View File

@@ -74,7 +74,7 @@ func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1.
"type": "nonCrd",
"apiGroup": "apps.cozystack.io",
"apiVersion": "v1alpha1",
"typeName": app.Plural, // e.g., "buckets"
"plural": app.Plural, // e.g., "buckets"
"disabled": false,
"hidden": false,
"tags": tags,

View File

@@ -0,0 +1,69 @@
package dashboard
import (
"context"
"encoding/json"
"fmt"
"strings"
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
// ensureNavigation updates the Navigation resource to include a baseFactoriesMapping entry for the given CRD
func (m *Manager) ensureNavigation(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
g, v, kind := pickGVK(crd)
plural := pickPlural(kind, crd)
lowerKind := strings.ToLower(kind)
factoryKey := fmt.Sprintf("%s-details", lowerKind)
// All CRD resources are namespaced API resources
mappingKey := fmt.Sprintf("base-factory-namespaced-api-%s-%s-%s", g, v, plural)
obj := &dashv1alpha1.Navigation{}
obj.SetName("navigation")
_, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error {
// Parse existing spec
spec := make(map[string]any)
if obj.Spec.JSON.Raw != nil {
if err := json.Unmarshal(obj.Spec.JSON.Raw, &spec); err != nil {
spec = make(map[string]any)
}
}
// Get or create baseFactoriesMapping
var mappings map[string]string
if existing, ok := spec["baseFactoriesMapping"].(map[string]any); ok {
mappings = make(map[string]string, len(existing))
for k, val := range existing {
if s, ok := val.(string); ok {
mappings[k] = s
}
}
} else {
mappings = make(map[string]string)
}
// Add/update the mapping for this CRD
mappings[mappingKey] = factoryKey
spec["baseFactoriesMapping"] = mappings
b, err := json.Marshal(spec)
if err != nil {
return err
}
newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
if !compareArbitrarySpecs(obj.Spec, newSpec) {
obj.Spec = newSpec
}
return nil
})
return err
}

View File

@@ -17,13 +17,12 @@ import (
// ensureSidebar creates/updates multiple Sidebar resources that share the same menu:
// - The "details" sidebar tied to the current kind (stock-project-factory-<kind>-details)
// - The stock-instance sidebars: api-form, api-table, builtin-form, builtin-table
// - The stock-project sidebars: api-form, api-table, builtin-form, builtin-table, crd-form, crd-table
// - The stock-project sidebars: api-form, api-table, builtin-form, builtin-table, crd-form, crd-table
//
// Menu rules:
// - The first section is "Marketplace" with two hardcoded entries:
// - Marketplace (/openapi-ui/{clusterName}/{namespace}/factory/marketplace)
// - Tenant Info (/openapi-ui/{clusterName}/{namespace}/factory/info-details/info)
// - Marketplace (/openapi-ui/{cluster}/{namespace}/factory/marketplace)
// - Tenant Info (/openapi-ui/{cluster}/{namespace}/factory/info-details/info)
// - All other sections are built from CRDs where spec.dashboard != nil.
// - Categories are ordered strictly as:
// Marketplace, IaaS, PaaS, NaaS, <others A→Z>, Resources, Backups, Administration
@@ -91,7 +90,7 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
// Weight (default 0)
weight := def.Spec.Dashboard.Weight
link := fmt.Sprintf("/openapi-ui/{clusterName}/{namespace}/api-table/%s/%s/%s", g, v, plural)
link := fmt.Sprintf("/openapi-ui/{cluster}/{namespace}/api-table/%s/%s/%s", g, v, plural)
categories[cat] = append(categories[cat], item{
Key: plural,
@@ -146,7 +145,7 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
map[string]any{
"key": "marketplace",
"label": "Marketplace",
"link": "/openapi-ui/{clusterName}/{namespace}/factory/marketplace",
"link": "/openapi-ui/{cluster}/{namespace}/factory/marketplace",
},
},
},
@@ -176,23 +175,23 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
// Add hardcoded Backups section
menuItems = append(menuItems, map[string]any{
"key": "backups",
"key": "backups-category",
"label": "Backups",
"children": []any{
map[string]any{
"key": "plans",
"label": "Plans",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/backups.cozystack.io/v1alpha1/plans",
"link": "/openapi-ui/{cluster}/{namespace}/api-table/backups.cozystack.io/v1alpha1/plans",
},
map[string]any{
"key": "backupjobs",
"label": "BackupJobs",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/backups.cozystack.io/v1alpha1/backupjobs",
"link": "/openapi-ui/{cluster}/{namespace}/api-table/backups.cozystack.io/v1alpha1/backupjobs",
},
map[string]any{
"key": "backups",
"label": "Backups",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/backups.cozystack.io/v1alpha1/backups",
"link": "/openapi-ui/{cluster}/{namespace}/api-table/backups.cozystack.io/v1alpha1/backups",
},
},
})
@@ -205,22 +204,22 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
map[string]any{
"key": "info",
"label": "Info",
"link": "/openapi-ui/{clusterName}/{namespace}/factory/info-details/info",
"link": "/openapi-ui/{cluster}/{namespace}/factory/info-details/info",
},
map[string]any{
"key": "modules",
"label": "Modules",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules",
"link": "/openapi-ui/{cluster}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules",
},
map[string]any{
"key": "loadbalancer-services",
"label": "External IPs",
"link": "/openapi-ui/{clusterName}/{namespace}/factory/external-ips",
"link": "/openapi-ui/{cluster}/{namespace}/factory/external-ips",
},
map[string]any{
"key": "tenants",
"label": "Tenants",
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/apps.cozystack.io/v1alpha1/tenants",
"link": "/openapi-ui/{cluster}/{namespace}/api-table/apps.cozystack.io/v1alpha1/tenants",
},
},
})
@@ -228,13 +227,7 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
// 6) Prepare the list of Sidebar IDs to upsert with the SAME content
// Create sidebars for ALL CRDs with dashboard config
targetIDs := []string{
// stock-instance sidebars
"stock-instance-api-form",
"stock-instance-api-table",
"stock-instance-builtin-form",
"stock-instance-builtin-table",
// stock-project sidebars
// stock-project sidebars (namespace-level, full menu)
"stock-project-factory-marketplace",
"stock-project-factory-workloadmonitor-details",
"stock-project-factory-kube-service-details",
@@ -250,6 +243,11 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
"stock-project-builtin-table",
"stock-project-crd-form",
"stock-project-crd-table",
// stock-instance sidebars (namespace-level pages after namespace is selected)
"stock-instance-api-form",
"stock-instance-api-table",
"stock-instance-builtin-form",
"stock-instance-builtin-table",
}
// Add details sidebars for all CRDs with dashboard config

View File

@@ -1134,7 +1134,7 @@ func yamlEditor(id, cluster string, isNameSpaced bool, typeName string, prefillV
"cluster": cluster,
"isNameSpaced": isNameSpaced,
"type": "builtin",
"typeName": typeName,
"plural": typeName,
"prefillValuesRequestIndex": prefillValuesRequestIndex,
"substractHeight": float64(400),
},

View File

@@ -49,6 +49,8 @@ func (m *Manager) ensureStaticResource(ctx context.Context, obj client.Object) e
resource.(*dashv1alpha1.Navigation).Spec = o.Spec
case *dashv1alpha1.TableUriMapping:
resource.(*dashv1alpha1.TableUriMapping).Spec = o.Spec
case *dashv1alpha1.CFOMapping:
resource.(*dashv1alpha1.CFOMapping).Spec = o.Spec
}
// Ensure labels are always set
m.addDashboardLabels(resource, nil, ResourceTypeStatic)

View File

@@ -17,111 +17,111 @@ func CreateAllBreadcrumbs() []*dashboardv1alpha1.Breadcrumb {
return []*dashboardv1alpha1.Breadcrumb{
// Stock project factory configmap details
createBreadcrumb("stock-project-factory-configmap-details", []map[string]any{
createBreadcrumbItem("configmaps", "v1/configmaps", "/openapi-ui/{clusterName}/{namespace}/builtin-table/configmaps"),
createBreadcrumbItem("configmaps", "v1/configmaps", "/openapi-ui/{cluster}/{namespace}/builtin-table/configmaps"),
createBreadcrumbItem("configmap", "{6}"),
}),
// Stock cluster factory namespace details
createBreadcrumb("stock-cluster-factory-namespace-details", []map[string]any{
createBreadcrumbItem("namespaces", "v1/namespaces", "/openapi-ui/{clusterName}/builtin-table/namespaces"),
createBreadcrumbItem("namespaces", "v1/namespaces", "/openapi-ui/{cluster}/builtin-table/namespaces"),
createBreadcrumbItem("namespace", "{5}"),
}),
// Stock cluster factory node details
createBreadcrumb("stock-cluster-factory-node-details", []map[string]any{
createBreadcrumbItem("node", "v1/nodes", "/openapi-ui/{clusterName}/builtin-table/nodes"),
createBreadcrumbItem("node", "v1/nodes", "/openapi-ui/{cluster}/builtin-table/nodes"),
createBreadcrumbItem("node", "{5}"),
}),
// Stock project factory pod details
createBreadcrumb("stock-project-factory-pod-details", []map[string]any{
createBreadcrumbItem("pods", "v1/pods", "/openapi-ui/{clusterName}/{namespace}/builtin-table/pods"),
createBreadcrumbItem("pods", "v1/pods", "/openapi-ui/{cluster}/{namespace}/builtin-table/pods"),
createBreadcrumbItem("pod", "{6}"),
}),
// Stock project factory secret details
createBreadcrumb("stock-project-factory-kube-secret-details", []map[string]any{
createBreadcrumbItem("secrets", "v1/secrets", "/openapi-ui/{clusterName}/{namespace}/builtin-table/secrets"),
createBreadcrumbItem("secrets", "v1/secrets", "/openapi-ui/{cluster}/{namespace}/builtin-table/secrets"),
createBreadcrumbItem("secret", "{6}"),
}),
// Stock project factory service details
createBreadcrumb("stock-project-factory-kube-service-details", []map[string]any{
createBreadcrumbItem("services", "v1/services", "/openapi-ui/{clusterName}/{namespace}/builtin-table/services"),
createBreadcrumbItem("services", "v1/services", "/openapi-ui/{cluster}/{namespace}/builtin-table/services"),
createBreadcrumbItem("service", "{6}"),
}),
// Stock project factory ingress details
createBreadcrumb("stock-project-factory-kube-ingress-details", []map[string]any{
createBreadcrumbItem("ingresses", "networking.k8s.io/v1/ingresses", "/openapi-ui/{clusterName}/{namespace}/builtin-table/ingresses"),
createBreadcrumbItem("ingresses", "networking.k8s.io/v1/ingresses", "/openapi-ui/{cluster}/{namespace}/builtin-table/ingresses"),
createBreadcrumbItem("ingress", "{6}"),
}),
// Stock cluster api table
createBreadcrumb("stock-cluster-api-table", []map[string]any{
createBreadcrumbItem("api", "{apiGroup}/{apiVersion}/{typeName}"),
createBreadcrumbItem("api", "{apiGroup}/{apiVersion}/{plural}"),
}),
// Stock cluster api form
createBreadcrumb("stock-cluster-api-form", []map[string]any{
createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{typeName}", "/openapi-ui/{clusterName}/api-table/{apiGroup}/{apiVersion}/{typeName}"),
createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{plural}", "/openapi-ui/{cluster}/api-table/{apiGroup}/{apiVersion}/{plural}"),
createBreadcrumbItem("create-api-res-namespaced-typename", "Create"),
}),
// Stock cluster api form edit
createBreadcrumb("stock-cluster-api-form-edit", []map[string]any{
createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{typeName}", "/openapi-ui/{clusterName}/api-table/{apiGroup}/{apiVersion}/{typeName}"),
createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{plural}", "/openapi-ui/{cluster}/api-table/{apiGroup}/{apiVersion}/{plural}"),
createBreadcrumbItem("create-api-res-namespaced-typename", "Update"),
}),
// Stock cluster builtin table
createBreadcrumb("stock-cluster-builtin-table", []map[string]any{
createBreadcrumbItem("api", "v1/{typeName}"),
createBreadcrumbItem("api", "v1/{plural}"),
}),
// Stock cluster builtin form
createBreadcrumb("stock-cluster-builtin-form", []map[string]any{
createBreadcrumbItem("create-api-res-namespaced-table", "v1/{typeName}", "/openapi-ui/{clusterName}/builtin-table/{typeName}"),
createBreadcrumbItem("create-api-res-namespaced-table", "v1/{plural}", "/openapi-ui/{cluster}/builtin-table/{plural}"),
createBreadcrumbItem("create-api-res-namespaced-typename", "Create"),
}),
// Stock cluster builtin form edit
createBreadcrumb("stock-cluster-builtin-form-edit", []map[string]any{
createBreadcrumbItem("create-api-res-namespaced-table", "v1/{typeName}", "/openapi-ui/{clusterName}/builtin-table/{typeName}"),
createBreadcrumbItem("create-api-res-namespaced-table", "v1/{plural}", "/openapi-ui/{cluster}/builtin-table/{plural}"),
createBreadcrumbItem("create-api-res-namespaced-typename", "Update"),
}),
// Stock project api table
createBreadcrumb("stock-project-api-table", []map[string]any{
createBreadcrumbItem("api", "{apiGroup}/{apiVersion}/{typeName}"),
createBreadcrumbItem("api", "{apiGroup}/{apiVersion}/{plural}"),
}),
// Stock project api form
createBreadcrumb("stock-project-api-form", []map[string]any{
createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{typeName}", "/openapi-ui/{clusterName}/{namespace}/api-table/{apiGroup}/{apiVersion}/{typeName}"),
createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{plural}", "/openapi-ui/{cluster}/{namespace}/api-table/{apiGroup}/{apiVersion}/{plural}"),
createBreadcrumbItem("create-api-res-namespaced-typename", "Create"),
}),
// Stock project api form edit
createBreadcrumb("stock-project-api-form-edit", []map[string]any{
createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{typeName}", "/openapi-ui/{clusterName}/{namespace}/api-table/{apiGroup}/{apiVersion}/{typeName}"),
createBreadcrumbItem("create-api-res-namespaced-table", "{apiGroup}/{apiVersion}/{plural}", "/openapi-ui/{cluster}/{namespace}/api-table/{apiGroup}/{apiVersion}/{plural}"),
createBreadcrumbItem("create-api-res-namespaced-typename", "Update"),
}),
// Stock project builtin table
createBreadcrumb("stock-project-builtin-table", []map[string]any{
createBreadcrumbItem("api", "v1/{typeName}"),
createBreadcrumbItem("api", "v1/{plural}"),
}),
// Stock project builtin form
createBreadcrumb("stock-project-builtin-form", []map[string]any{
createBreadcrumbItem("create-api-res-namespaced-table", "v1/{typeName}", "/openapi-ui/{clusterName}/{namespace}/builtin-table/{typeName}"),
createBreadcrumbItem("create-api-res-namespaced-table", "v1/{plural}", "/openapi-ui/{cluster}/{namespace}/builtin-table/{plural}"),
createBreadcrumbItem("create-api-res-namespaced-typename", "Create"),
}),
// Stock project builtin form edit
createBreadcrumb("stock-project-builtin-form-edit", []map[string]any{
createBreadcrumbItem("create-api-res-namespaced-table", "v1/{typeName}", "/openapi-ui/{clusterName}/{namespace}/builtin-table/{typeName}"),
createBreadcrumbItem("create-api-res-namespaced-table", "v1/{plural}", "/openapi-ui/{cluster}/{namespace}/builtin-table/{plural}"),
createBreadcrumbItem("create-api-res-namespaced-typename", "Update"),
}),
}
@@ -503,18 +503,27 @@ func CreateAllCustomFormsOverrides() []*dashboardv1alpha1.CustomFormsOverride {
createFormItem("metadata.namespace", "Namespace", "text"),
createFormItem("spec.applicationRef.kind", "Application Kind", "text"),
createFormItem("spec.applicationRef.name", "Application Name", "text"),
createFormItemWithAPI("spec.backupClassName", "Backup Class", "select", map[string]any{
"api": map[string]any{
"fetchUrl": "/api/clusters/{clusterName}/k8s/apis/backups.cozystack.io/v1alpha1/backupclasses",
"pathToItems": []any{"items"},
"pathToValue": []any{"metadata", "name"},
"pathToLabel": []any{"metadata", "name"},
"clusterNameVar": "clusterName",
},
}),
createFormItem("spec.schedule.type", "Schedule Type", "text"),
createFormItem("spec.schedule.cron", "Schedule Cron", "text"),
},
"schema": createSchema(map[string]any{
"backupClassName": listInputScemaItemBackupClass(),
}),
}),
// BackupJobs form override - backups.cozystack.io/v1alpha1
createCustomFormsOverride("default-/backups.cozystack.io/v1alpha1/backupjobs", map[string]any{
"formItems": []any{
createFormItem("metadata.name", "Name", "text"),
createFormItem("metadata.namespace", "Namespace", "text"),
createFormItem("spec.planRef.name", "Plan Name (optional)", "text"),
createFormItem("spec.applicationRef.apiGroup", "Application API Group", "text"),
createFormItem("spec.applicationRef.kind", "Application Kind", "text"),
createFormItem("spec.applicationRef.name", "Application Name", "text"),
},
"schema": createSchema(map[string]any{
"backupClassName": listInputScemaItemBackupClass(),
}),
}),
}
}
@@ -535,14 +544,14 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
}, []any{
map[string]any{
"data": map[string]any{
"baseApiVersion": "v1alpha1",
"baseprefix": "openapi-ui",
"clusterNamePartOfUrl": "{2}",
"id": 311,
"mpResourceKind": "MarketplacePanel",
"mpResourceName": "marketplacepanels",
"namespacePartOfUrl": "{3}",
"baseApiGroup": "dashboard.cozystack.io",
"baseApiVersion": "v1alpha1",
"baseprefix": "openapi-ui",
"cluster": "{2}",
"id": 311,
"marketplaceKind": "MarketplacePanel",
"marketplacePlural": "marketplacepanels",
"namespace": "{3}",
"baseApiGroup": "dashboard.cozystack.io",
},
"type": "MarketplaceCard",
},
@@ -855,7 +864,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
"prefillValuesRequestIndex": 0,
"substractHeight": float64(400),
"type": "builtin",
"typeName": "secrets",
"plural": "secrets",
"readOnly": true,
},
},
@@ -1085,13 +1094,13 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "service-port-mapping-table",
"baseprefix": "/openapi-ui",
"clusterNamePartOfUrl": "{2}",
"customizationId": "factory-kube-service-details-port-mapping",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services/{6}",
"pathToItems": ".spec.ports",
"withoutControls": true,
"id": "service-port-mapping-table",
"baseprefix": "/openapi-ui",
"cluster": "{2}",
"customizationId": "factory-kube-service-details-port-mapping",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services/{6}",
"pathToItems": ".spec.ports",
"withoutControls": true,
},
},
}),
@@ -1111,11 +1120,11 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "service-pod-serving-table",
"baseprefix": "/openapi-ui",
"clusterNamePartOfUrl": "{2}",
"customizationId": "factory-kube-service-details-endpointslice",
"fetchUrl": "/api/clusters/{2}/k8s/apis/discovery.k8s.io/v1/namespaces/{3}/endpointslices",
"id": "service-pod-serving-table",
"baseprefix": "/openapi-ui",
"cluster": "{2}",
"customizationId": "factory-kube-service-details-endpointslice",
"fetchUrl": "/api/clusters/{2}/k8s/apis/discovery.k8s.io/v1/namespaces/{3}/endpointslices",
"labelSelector": map[string]any{
"kubernetes.io/service-name": "{reqsJsonPath[0]['.metadata.name']['-']}",
},
@@ -1145,7 +1154,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
"prefillValuesRequestIndex": 0,
"substractHeight": float64(400),
"type": "builtin",
"typeName": "services",
"plural": "services",
},
},
},
@@ -1168,11 +1177,11 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "pods-table",
"baseprefix": "/openapi-ui",
"clusterNamePartOfUrl": "{2}",
"customizationId": "factory-node-details-/v1/pods",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/pods",
"id": "pods-table",
"baseprefix": "/openapi-ui",
"cluster": "{2}",
"customizationId": "factory-node-details-/v1/pods",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/pods",
"labelSelectorFull": map[string]any{
"pathToLabels": ".spec.selector",
"reqIndex": 0,
@@ -1300,13 +1309,13 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "rules-table",
"fetchUrl": "/api/clusters/{2}/k8s/apis/networking.k8s.io/v1/namespaces/{3}/ingresses/{6}",
"clusterNamePartOfUrl": "{2}",
"customizationId": "factory-kube-ingress-details-rules",
"baseprefix": "/openapi-ui",
"withoutControls": true,
"pathToItems": []any{"spec", "rules"},
"id": "rules-table",
"fetchUrl": "/api/clusters/{2}/k8s/apis/networking.k8s.io/v1/namespaces/{3}/ingresses/{6}",
"cluster": "{2}",
"customizationId": "factory-kube-ingress-details-rules",
"baseprefix": "/openapi-ui",
"withoutControls": true,
"pathToItems": []any{"spec", "rules"},
},
},
}),
@@ -1322,8 +1331,10 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
"id": "yaml-editor",
"cluster": "{2}",
"isNameSpaced": true,
"type": "builtin",
"typeName": "ingresses",
"type": "apis",
"apiGroup": "networking.k8s.io",
"apiVersion": "v1",
"plural": "ingresses",
"prefillValuesRequestIndex": float64(0),
"substractHeight": float64(400),
},
@@ -1452,11 +1463,11 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "workloads-table",
"baseprefix": "/openapi-ui",
"clusterNamePartOfUrl": "{2}",
"customizationId": "factory-details-v1alpha1.cozystack.io.workloads",
"fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloads",
"id": "workloads-table",
"baseprefix": "/openapi-ui",
"cluster": "{2}",
"customizationId": "factory-details-v1alpha1.cozystack.io.workloads",
"fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloads",
"labelSelector": map[string]any{
"workloads.cozystack.io/monitor": "{reqs[0]['metadata','name']}",
},
@@ -1477,8 +1488,10 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
"isNameSpaced": true,
"prefillValuesRequestIndex": 0,
"substractHeight": float64(400),
"type": "builtin",
"typeName": "workloadmonitors",
"type": "apis",
"apiGroup": "cozystack.io",
"apiVersion": "v1alpha1",
"plural": "workloadmonitors",
},
},
},
@@ -1960,12 +1973,27 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
// CreateAllNavigations creates all navigation resources using helper functions
func CreateAllNavigations() []*dashboardv1alpha1.Navigation {
// Build baseFactoriesMapping for static (built-in) factories
baseFactoriesMapping := map[string]string{
// Cluster-scoped builtin resources
"base-factory-clusterscoped-builtin-v1-namespaces": "namespace-details",
"base-factory-clusterscoped-builtin-v1-nodes": "node-details",
// Namespaced builtin resources
"base-factory-namespaced-builtin-v1-pods": "pod-details",
"base-factory-namespaced-builtin-v1-secrets": "kube-secret-details",
"base-factory-namespaced-builtin-v1-services": "kube-service-details",
// Namespaced API resources
"base-factory-namespaced-api-networking.k8s.io-v1-ingresses": "kube-ingress-details",
"base-factory-namespaced-api-cozystack.io-v1alpha1-workloadmonitors": "workloadmonitor-details",
}
return []*dashboardv1alpha1.Navigation{
createNavigation("navigation", map[string]any{
"namespaces": map[string]any{
"change": "/openapi-ui/{selectedCluster}/{value}/factory/marketplace",
"clear": "/openapi-ui/{selectedCluster}/api-table/core.cozystack.io/v1alpha1/tenantnamespaces",
},
"baseFactoriesMapping": baseFactoriesMapping,
}),
}
}
@@ -2023,9 +2051,9 @@ func createCustomFormsOverride(customizationId string, spec map[string]any) *das
"strategy": "merge",
}
// Merge caller-provided fields (like formItems) into newSpec
// Merge into newSpec caller-provided fields without: customizationId, hidden, strategy
for key, value := range spec {
if key != "customizationId" && key != "hidden" && key != "schema" && key != "strategy" {
if key != "customizationId" && key != "hidden" && key != "strategy" {
newSpec[key] = value
}
}
@@ -2070,6 +2098,28 @@ func createNavigation(name string, spec map[string]any) *dashboardv1alpha1.Navig
}
}
func listInputScemaItemBackupClass() map[string]any {
return map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/backups.cozystack.io/v1alpha1/backupclasses",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
},
}
}
// backupClassSchema returns the schema for spec.backupClassName as listInput (BackupJob/Plan).
func createSchema(customProps map[string]any) map[string]any {
return map[string]any{
"properties": map[string]any{
"spec": map[string]any{
"properties": customProps,
},
},
}
}
// createFormItem creates a form item for CustomFormsOverride
func createFormItem(path, label, fieldType string) map[string]any {
return map[string]any{
@@ -2342,6 +2392,51 @@ func createWorkloadmonitorHeader() map[string]any {
}
}
// CreateStaticCFOMapping creates the CFOMapping resource with mappings from static CustomFormsOverrides
func CreateStaticCFOMapping() *dashboardv1alpha1.CFOMapping {
// Build mappings from static CustomFormsOverrides
customFormsOverrides := CreateAllCustomFormsOverrides()
mappings := make(map[string]string, len(customFormsOverrides))
for _, cfo := range customFormsOverrides {
var spec map[string]any
if err := json.Unmarshal(cfo.Spec.JSON.Raw, &spec); err != nil {
continue
}
customizationID, ok := spec["customizationId"].(string)
if !ok {
continue
}
// Extract the resource path from customizationId (remove "default-" prefix)
resourcePath := strings.TrimPrefix(customizationID, "default-")
mappings[resourcePath] = customizationID
}
return createCFOMapping("cfomapping", mappings)
}
// createCFOMapping creates a CFOMapping resource
func createCFOMapping(name string, mappings map[string]string) *dashboardv1alpha1.CFOMapping {
spec := map[string]any{
"mappings": mappings,
}
jsonData, _ := json.Marshal(spec)
return &dashboardv1alpha1.CFOMapping{
TypeMeta: metav1.TypeMeta{
APIVersion: "dashboard.cozystack.io/v1alpha1",
Kind: "CFOMapping",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: dashboardv1alpha1.ArbitrarySpec{
JSON: v1.JSON{
Raw: jsonData,
},
},
}
}
// ---------------- Complete resource creation function ----------------
// CreateAllStaticResources creates all static dashboard resources using helper functions
@@ -2378,5 +2473,8 @@ func CreateAllStaticResources() []client.Object {
resources = append(resources, tableUriMapping)
}
// Add CFOMapping
resources = append(resources, CreateStaticCFOMapping())
return resources
}

View File

@@ -43,7 +43,7 @@ func TestWorkloadReconciler_DeletesOnMissingMonitor(t *testing.T) {
Name: "pod-foo",
Namespace: "default",
Labels: map[string]string{
"workloadmonitor.cozystack.io/name": "missing-monitor",
"workloads.cozystack.io/monitor": "missing-monitor",
},
},
}
@@ -89,7 +89,7 @@ func TestWorkloadReconciler_KeepsWhenAllExist(t *testing.T) {
Name: "pod-foo",
Namespace: "default",
Labels: map[string]string{
"workloadmonitor.cozystack.io/name": "mon",
"workloads.cozystack.io/monitor": "mon",
},
},
}

View File

@@ -0,0 +1,112 @@
/*
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 crdinstall
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/cozystack/cozystack/internal/manifestutil"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// Install applies Cozystack CRDs using embedded manifests.
// It extracts the manifests and applies them to the cluster using server-side apply,
// then waits for all CRDs to have the Established condition.
func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifests func(string) error) error {
logger := log.FromContext(ctx)
tmpDir, err := os.MkdirTemp("", "crd-install-*")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
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)
}
entries, err := os.ReadDir(manifestsDir)
if err != nil {
return fmt.Errorf("failed to read manifests directory: %w", err)
}
var manifestFiles []string
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".yaml") {
manifestFiles = append(manifestFiles, filepath.Join(manifestsDir, entry.Name()))
}
}
if len(manifestFiles) == 0 {
return fmt.Errorf("no YAML manifest files found in directory")
}
var objects []*unstructured.Unstructured
for _, manifestPath := range manifestFiles {
objs, err := manifestutil.ParseManifestFile(manifestPath)
if err != nil {
return fmt.Errorf("failed to parse manifests from %s: %w", manifestPath, err)
}
objects = append(objects, objs...)
}
if len(objects) == 0 {
return fmt.Errorf("no objects found in manifests")
}
// Validate all objects are CRDs — reject anything else to prevent
// accidental force-apply of arbitrary resources.
for _, obj := range objects {
if obj.GetAPIVersion() != "apiextensions.k8s.io/v1" || obj.GetKind() != "CustomResourceDefinition" {
return fmt.Errorf("unexpected object %s %s/%s in CRD manifests, only apiextensions.k8s.io/v1 CustomResourceDefinition is allowed",
obj.GetAPIVersion(), obj.GetKind(), obj.GetName())
}
}
logger.Info("Applying Cozystack CRDs", "count", len(objects))
for _, obj := range objects {
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 CRD %s: %w", obj.GetName(), err)
}
logger.Info("Applied CRD", "name", obj.GetName())
}
crdNames := manifestutil.CollectCRDNames(objects)
if err := manifestutil.WaitForCRDsEstablished(ctx, k8sClient, crdNames); err != nil {
return fmt.Errorf("CRDs not established after apply: %w", err)
}
logger.Info("CRD installation completed successfully")
return nil
}

View File

@@ -0,0 +1,302 @@
/*
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 crdinstall
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)
func TestWriteEmbeddedManifests(t *testing.T) {
tmpDir := t.TempDir()
if err := WriteEmbeddedManifests(tmpDir); err != nil {
t.Fatalf("WriteEmbeddedManifests() error = %v", err)
}
entries, err := os.ReadDir(tmpDir)
if err != nil {
t.Fatalf("failed to read output dir: %v", err)
}
var yamlFiles []string
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".yaml") {
yamlFiles = append(yamlFiles, e.Name())
}
}
if len(yamlFiles) == 0 {
t.Error("WriteEmbeddedManifests() produced no YAML files")
}
expectedFiles := []string{
"cozystack.io_packages.yaml",
"cozystack.io_packagesources.yaml",
}
for _, expected := range expectedFiles {
found := false
for _, actual := range yamlFiles {
if actual == expected {
found = true
break
}
}
if !found {
t.Errorf("expected file %q not found in output, got %v", expected, yamlFiles)
}
}
// Verify files are non-empty
for _, f := range yamlFiles {
data, err := os.ReadFile(filepath.Join(tmpDir, f))
if err != nil {
t.Errorf("failed to read %s: %v", f, err)
continue
}
if len(data) == 0 {
t.Errorf("file %s is empty", f)
}
}
}
func TestWriteEmbeddedManifests_filePermissions(t *testing.T) {
tmpDir := t.TempDir()
if err := WriteEmbeddedManifests(tmpDir); err != nil {
t.Fatalf("WriteEmbeddedManifests() error = %v", err)
}
entries, err := os.ReadDir(tmpDir)
if err != nil {
t.Fatalf("failed to read output dir: %v", err)
}
for _, e := range entries {
if !strings.HasSuffix(e.Name(), ".yaml") {
continue
}
info, err := e.Info()
if err != nil {
t.Errorf("failed to get info for %s: %v", e.Name(), err)
continue
}
perm := info.Mode().Perm()
if perm&0o077 != 0 {
t.Errorf("file %s has overly permissive mode %o, expected no group/other access", e.Name(), perm)
}
}
}
// newCRDManifestWriter returns a function that writes test CRD YAML files.
func newCRDManifestWriter(crds ...string) func(string) error {
return func(dir string) error {
for i, crd := range crds {
filename := filepath.Join(dir, fmt.Sprintf("crd%d.yaml", i+1))
if err := os.WriteFile(filename, []byte(crd), 0600); err != nil {
return err
}
}
return nil
}
}
var testCRD1 = `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: packages.cozystack.io
spec:
group: cozystack.io
names:
kind: Package
plural: packages
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
`
var testCRD2 = `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: packagesources.cozystack.io
spec:
group: cozystack.io
names:
kind: PackageSource
plural: packagesources
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
`
// establishedInterceptor simulates CRDs becoming Established in the API server.
func establishedInterceptor() interceptor.Funcs {
return interceptor.Funcs{
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
if err := c.Get(ctx, key, obj, opts...); err != nil {
return err
}
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil
}
if u.GetKind() == "CustomResourceDefinition" {
_ = unstructured.SetNestedSlice(u.Object, []interface{}{
map[string]interface{}{
"type": "Established",
"status": "True",
},
}, "status", "conditions")
}
return nil
},
}
}
func TestInstall_appliesAllCRDs(t *testing.T) {
log.SetLogger(zap.New(zap.UseDevMode(true)))
scheme := runtime.NewScheme()
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add apiextensions to scheme: %v", err)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithInterceptorFuncs(establishedInterceptor()).
Build()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ctx = log.IntoContext(ctx, log.FromContext(context.Background()))
err := Install(ctx, fakeClient, newCRDManifestWriter(testCRD1, testCRD2))
if err != nil {
t.Fatalf("Install() error = %v", err)
}
}
func TestInstall_noManifests(t *testing.T) {
log.SetLogger(zap.New(zap.UseDevMode(true)))
scheme := runtime.NewScheme()
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = log.IntoContext(ctx, log.FromContext(context.Background()))
err := Install(ctx, fakeClient, func(string) error { return nil })
if err == nil {
t.Error("Install() expected error for empty manifests, got nil")
}
if !strings.Contains(err.Error(), "no YAML manifest files found") {
t.Errorf("Install() error = %v, want error containing 'no YAML manifest files found'", err)
}
}
func TestInstall_writeManifestsFails(t *testing.T) {
log.SetLogger(zap.New(zap.UseDevMode(true)))
scheme := runtime.NewScheme()
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = log.IntoContext(ctx, log.FromContext(context.Background()))
err := Install(ctx, fakeClient, func(string) error { return os.ErrPermission })
if err == nil {
t.Error("Install() expected error when writeManifests fails, got nil")
}
}
func TestInstall_rejectsNonCRDObjects(t *testing.T) {
log.SetLogger(zap.New(zap.UseDevMode(true)))
scheme := runtime.NewScheme()
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add apiextensions to scheme: %v", err)
}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
nonCRD := `apiVersion: v1
kind: Namespace
metadata:
name: should-not-be-applied
`
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = log.IntoContext(ctx, log.FromContext(context.Background()))
err := Install(ctx, fakeClient, newCRDManifestWriter(nonCRD))
if err == nil {
t.Fatal("Install() expected error for non-CRD object, got nil")
}
if !strings.Contains(err.Error(), "unexpected object") {
t.Errorf("Install() error = %v, want error containing 'unexpected object'", err)
}
}
func TestInstall_crdNotEstablished(t *testing.T) {
log.SetLogger(zap.New(zap.UseDevMode(true)))
scheme := runtime.NewScheme()
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add apiextensions to scheme: %v", err)
}
// No interceptor: CRDs will never get Established condition
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ctx = log.IntoContext(ctx, log.FromContext(context.Background()))
err := Install(ctx, fakeClient, newCRDManifestWriter(testCRD1))
if err == nil {
t.Fatal("Install() expected error when CRDs never become established, got nil")
}
if !strings.Contains(err.Error(), "CRDs not established") {
t.Errorf("Install() error = %v, want error containing 'CRDs not established'", err)
}
}

View File

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

View File

@@ -17,18 +17,15 @@ limitations under the License.
package fluxinstall
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/cozystack/cozystack/internal/manifestutil"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
@@ -76,7 +73,7 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest
// Parse all manifest files
var objects []*unstructured.Unstructured
for _, manifestPath := range manifestFiles {
objs, err := parseManifests(manifestPath)
objs, err := manifestutil.ParseManifestFile(manifestPath)
if err != nil {
return fmt.Errorf("failed to parse manifests from %s: %w", manifestPath, err)
}
@@ -110,56 +107,6 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest
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)
@@ -183,8 +130,11 @@ func applyManifests(ctx context.Context, k8sClient client.Client, objects []*uns
return fmt.Errorf("failed to apply cluster definitions: %w", err)
}
// Wait a bit for CRDs to be registered
time.Sleep(2 * time.Second)
// Wait for CRDs to be established before applying dependent resources
crdNames := manifestutil.CollectCRDNames(stageOne)
if err := manifestutil.WaitForCRDsEstablished(ctx, k8sClient, crdNames); err != nil {
return fmt.Errorf("CRDs not established after apply: %w", err)
}
}
// Apply stage two (everything else)
@@ -215,7 +165,6 @@ func applyObjects(ctx context.Context, k8sClient client.Client, objects []*unstr
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 {
@@ -386,4 +335,3 @@ func setEnvVar(env []interface{}, name, value string) []interface{} {
return env
}

View File

@@ -22,6 +22,7 @@ import (
"io/fs"
"os"
"path"
"path/filepath"
)
//go:embed manifests/*.yaml
@@ -40,8 +41,8 @@ func WriteEmbeddedManifests(dir string) error {
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 {
outputPath := filepath.Join(dir, manifest.Name())
if err := os.WriteFile(outputPath, data, 0600); err != nil {
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
}
}

View File

@@ -0,0 +1,118 @@
/*
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 manifestutil
import (
"context"
"fmt"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
var crdGVK = schema.GroupVersionKind{
Group: "apiextensions.k8s.io",
Version: "v1",
Kind: "CustomResourceDefinition",
}
// WaitForCRDsEstablished polls the API server until all named CRDs have the
// Established condition set to True, or the context is cancelled.
func WaitForCRDsEstablished(ctx context.Context, k8sClient client.Client, crdNames []string) error {
if len(crdNames) == 0 {
return nil
}
logger := log.FromContext(ctx)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled while waiting for CRDs to be established: %w", ctx.Err())
default:
}
allEstablished := true
var pendingCRD string
for _, name := range crdNames {
crd := &unstructured.Unstructured{}
crd.SetGroupVersionKind(crdGVK)
if err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, crd); err != nil {
allEstablished = false
pendingCRD = name
break
}
conditions, found, err := unstructured.NestedSlice(crd.Object, "status", "conditions")
if err != nil || !found {
allEstablished = false
pendingCRD = name
break
}
established := false
for _, c := range conditions {
cond, ok := c.(map[string]interface{})
if !ok {
continue
}
if cond["type"] == "Established" && cond["status"] == "True" {
established = true
break
}
}
if !established {
allEstablished = false
pendingCRD = name
break
}
}
if allEstablished {
logger.Info("All CRDs established", "count", len(crdNames))
return nil
}
logger.V(1).Info("Waiting for CRD to be established", "crd", pendingCRD)
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled while waiting for CRD %q to be established: %w", pendingCRD, ctx.Err())
case <-ticker.C:
}
}
}
// CollectCRDNames returns the names of all CustomResourceDefinition objects
// from the given list of unstructured objects. Only objects with
// apiVersion "apiextensions.k8s.io/v1" and kind "CustomResourceDefinition"
// are matched.
func CollectCRDNames(objects []*unstructured.Unstructured) []string {
var names []string
for _, obj := range objects {
if obj.GetAPIVersion() == "apiextensions.k8s.io/v1" && obj.GetKind() == "CustomResourceDefinition" {
names = append(names, obj.GetName())
}
}
return names
}

View File

@@ -0,0 +1,202 @@
/*
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 manifestutil
import (
"context"
"strings"
"testing"
"time"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)
func TestCollectCRDNames(t *testing.T) {
objects := []*unstructured.Unstructured{
{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]interface{}{"name": "test-ns"},
}},
{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "packages.cozystack.io"},
}},
{Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{"name": "test-deploy"},
}},
{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "packagesources.cozystack.io"},
}},
}
names := CollectCRDNames(objects)
if len(names) != 2 {
t.Fatalf("CollectCRDNames() returned %d names, want 2", len(names))
}
if names[0] != "packages.cozystack.io" {
t.Errorf("names[0] = %q, want %q", names[0], "packages.cozystack.io")
}
if names[1] != "packagesources.cozystack.io" {
t.Errorf("names[1] = %q, want %q", names[1], "packagesources.cozystack.io")
}
}
func TestCollectCRDNames_ignoresWrongAPIVersion(t *testing.T) {
objects := []*unstructured.Unstructured{
{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "real.crd.io"},
}},
{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1beta1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "legacy.crd.io"},
}},
}
names := CollectCRDNames(objects)
if len(names) != 1 {
t.Fatalf("CollectCRDNames() returned %d names, want 1", len(names))
}
if names[0] != "real.crd.io" {
t.Errorf("names[0] = %q, want %q", names[0], "real.crd.io")
}
}
func TestCollectCRDNames_noCRDs(t *testing.T) {
objects := []*unstructured.Unstructured{
{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]interface{}{"name": "test"},
}},
}
names := CollectCRDNames(objects)
if len(names) != 0 {
t.Errorf("CollectCRDNames() returned %d names, want 0", len(names))
}
}
func TestWaitForCRDsEstablished_success(t *testing.T) {
log.SetLogger(zap.New(zap.UseDevMode(true)))
scheme := runtime.NewScheme()
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add apiextensions to scheme: %v", err)
}
// Create a CRD object in the fake client
crd := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "packages.cozystack.io"},
}}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
WithInterceptorFuncs(interceptor.Funcs{
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
if err := c.Get(ctx, key, obj, opts...); err != nil {
return err
}
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil
}
if u.GetKind() == "CustomResourceDefinition" {
_ = unstructured.SetNestedSlice(u.Object, []interface{}{
map[string]interface{}{
"type": "Established",
"status": "True",
},
}, "status", "conditions")
}
return nil
},
}).
Build()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = log.IntoContext(ctx, log.FromContext(context.Background()))
err := WaitForCRDsEstablished(ctx, fakeClient, []string{"packages.cozystack.io"})
if err != nil {
t.Fatalf("WaitForCRDsEstablished() error = %v", err)
}
}
func TestWaitForCRDsEstablished_timeout(t *testing.T) {
log.SetLogger(zap.New(zap.UseDevMode(true)))
scheme := runtime.NewScheme()
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add apiextensions to scheme: %v", err)
}
// CRD exists but never gets Established condition
crd := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "packages.cozystack.io"},
}}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
Build()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ctx = log.IntoContext(ctx, log.FromContext(context.Background()))
err := WaitForCRDsEstablished(ctx, fakeClient, []string{"packages.cozystack.io"})
if err == nil {
t.Fatal("WaitForCRDsEstablished() expected error on timeout, got nil")
}
if !strings.Contains(err.Error(), "packages.cozystack.io") {
t.Errorf("error should mention stuck CRD name, got: %v", err)
}
}
func TestWaitForCRDsEstablished_empty(t *testing.T) {
scheme := runtime.NewScheme()
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
ctx := context.Background()
err := WaitForCRDsEstablished(ctx, fakeClient, nil)
if err != nil {
t.Fatalf("WaitForCRDsEstablished() with empty names should return nil, got: %v", err)
}
}

View File

@@ -0,0 +1,76 @@
/*
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 manifestutil
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
)
// ParseManifestFile reads a YAML file and parses it into unstructured objects.
func ParseManifestFile(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 from a reader into unstructured objects.
// Empty documents and documents without a kind are skipped.
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)
}
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 {
if err == io.EOF {
continue
}
return nil, fmt.Errorf("failed to decode YAML document: %w", err)
}
if obj.GetKind() == "" {
continue
}
objects = append(objects, obj)
}
return objects, nil
}

View File

@@ -0,0 +1,161 @@
/*
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 manifestutil
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestReadYAMLObjects(t *testing.T) {
tests := []struct {
name string
input string
wantCount int
wantErr bool
}{
{
name: "single document",
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
`,
wantCount: 1,
},
{
name: "multiple documents",
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: test1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
wantCount: 2,
},
{
name: "empty input",
input: "",
wantCount: 0,
},
{
name: "decoder rejects document without kind",
input: `apiVersion: v1
metadata:
name: test
`,
wantErr: true,
},
{
name: "whitespace-only document between separators is skipped",
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: test1
---
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
wantCount: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objects, err := ReadYAMLObjects(strings.NewReader(tt.input))
if (err != nil) != tt.wantErr {
t.Errorf("ReadYAMLObjects() error = %v, wantErr %v", err, tt.wantErr)
return
}
if len(objects) != tt.wantCount {
t.Errorf("ReadYAMLObjects() returned %d objects, want %d", len(objects), tt.wantCount)
}
})
}
}
func TestReadYAMLObjects_preservesFields(t *testing.T) {
input := `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: packages.cozystack.io
spec:
group: cozystack.io
`
objects, err := ReadYAMLObjects(strings.NewReader(input))
if err != nil {
t.Fatalf("ReadYAMLObjects() error = %v", err)
}
if len(objects) != 1 {
t.Fatalf("expected 1 object, got %d", len(objects))
}
obj := objects[0]
if obj.GetKind() != "CustomResourceDefinition" {
t.Errorf("kind = %q, want %q", obj.GetKind(), "CustomResourceDefinition")
}
if obj.GetName() != "packages.cozystack.io" {
t.Errorf("name = %q, want %q", obj.GetName(), "packages.cozystack.io")
}
if obj.GetAPIVersion() != "apiextensions.k8s.io/v1" {
t.Errorf("apiVersion = %q, want %q", obj.GetAPIVersion(), "apiextensions.k8s.io/v1")
}
}
func TestParseManifestFile(t *testing.T) {
tmpDir := t.TempDir()
manifestPath := filepath.Join(tmpDir, "test.yaml")
content := `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`
if err := os.WriteFile(manifestPath, []byte(content), 0600); err != nil {
t.Fatalf("failed to write test manifest: %v", err)
}
objects, err := ParseManifestFile(manifestPath)
if err != nil {
t.Fatalf("ParseManifestFile() error = %v", err)
}
if len(objects) != 2 {
t.Errorf("ParseManifestFile() returned %d objects, want 2", len(objects))
}
}
func TestParseManifestFile_notFound(t *testing.T) {
_, err := ParseManifestFile("/nonexistent/path/test.yaml")
if err == nil {
t.Error("ParseManifestFile() expected error for nonexistent file, got nil")
}
}

View File

@@ -17,3 +17,13 @@ spec:
bucketClaimName: {{ .Release.Name }}
credentialsSecretName: {{ .Release.Name }}
protocol: s3
---
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketAccess
metadata:
name: {{ .Release.Name }}-readonly
spec:
bucketAccessClassName: {{ $seaweedfs }}-readonly
bucketClaimName: {{ .Release.Name }}
credentialsSecretName: {{ .Release.Name }}-readonly
protocol: s3

View File

@@ -10,6 +10,7 @@ rules:
resourceNames:
- {{ .Release.Name }}
- {{ .Release.Name }}-credentials
- {{ .Release.Name }}-readonly
verbs: ["get", "list", "watch"]
- apiGroups:
- networking.k8s.io

View File

@@ -0,0 +1,7 @@
apiVersion: v2
name: harbor
description: Managed Harbor container registry
icon: /logos/harbor.svg
type: application
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
appVersion: "2.14.2"

View File

@@ -0,0 +1,7 @@
NAME=harbor
include ../../../hack/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
../../../hack/update-crd.sh

View File

@@ -0,0 +1,47 @@
# Managed Harbor Container Registry
Harbor is an open source trusted cloud native registry project that stores, signs, and scans content.
## Parameters
### Common parameters
| Name | Description | Type | Value |
| -------------- | -------------------------------------------------------------------------------------------- | -------- | ----- |
| `host` | Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host). | `string` | `""` |
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
### Component configuration
| Name | Description | Type | Value |
| ----------------------------- | -------------------------------------------------------------------------------------------------------- | ---------- | ------- |
| `core` | Core API server configuration. | `object` | `{}` |
| `core.resources` | Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `core.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` |
| `core.resources.memory` | Amount of memory allocated. | `quantity` | `""` |
| `core.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `small` |
| `registry` | Container image registry configuration. | `object` | `{}` |
| `registry.resources` | Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `registry.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` |
| `registry.resources.memory` | Amount of memory allocated. | `quantity` | `""` |
| `registry.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `small` |
| `jobservice` | Background job service configuration. | `object` | `{}` |
| `jobservice.resources` | Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `jobservice.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` |
| `jobservice.resources.memory` | Amount of memory allocated. | `quantity` | `""` |
| `jobservice.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `nano` |
| `trivy` | Trivy vulnerability scanner configuration. | `object` | `{}` |
| `trivy.enabled` | Enable or disable the vulnerability scanner. | `bool` | `true` |
| `trivy.size` | Persistent Volume size for vulnerability database cache. | `quantity` | `5Gi` |
| `trivy.resources` | Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `trivy.resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` |
| `trivy.resources.memory` | Amount of memory allocated. | `quantity` | `""` |
| `trivy.resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `nano` |
| `database` | PostgreSQL database configuration. | `object` | `{}` |
| `database.size` | Persistent Volume size for database storage. | `quantity` | `5Gi` |
| `database.replicas` | Number of database instances. | `int` | `2` |
| `redis` | Redis cache configuration. | `object` | `{}` |
| `redis.size` | Persistent Volume size for cache storage. | `quantity` | `1Gi` |
| `redis.replicas` | Number of Redis replicas. | `int` | `2` |

View File

@@ -0,0 +1 @@
../../../library/cozy-lib

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,19 @@
{{- $seaweedfs := .Values._namespace.seaweedfs }}
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketClaim
metadata:
name: {{ .Release.Name }}-registry
spec:
bucketClassName: {{ $seaweedfs }}
protocols:
- s3
---
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketAccess
metadata:
name: {{ .Release.Name }}-registry
spec:
bucketAccessClassName: {{ $seaweedfs }}
bucketClaimName: {{ .Release.Name }}-registry
credentialsSecretName: {{ .Release.Name }}-registry-bucket
protocol: s3

View File

@@ -0,0 +1,46 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ .Release.Name }}-dashboard-resources
rules:
- apiGroups:
- ""
resources:
- services
resourceNames:
- {{ .Release.Name }}
verbs: ["get", "list", "watch"]
- apiGroups:
- ""
resources:
- secrets
resourceNames:
- {{ .Release.Name }}-credentials
verbs: ["get", "list", "watch"]
- apiGroups:
- networking.k8s.io
resources:
- ingresses
resourceNames:
- {{ .Release.Name }}-ingress
verbs: ["get", "list", "watch"]
- apiGroups:
- cozystack.io
resources:
- workloadmonitors
resourceNames:
- {{ .Release.Name }}-core
- {{ .Release.Name }}-registry
- {{ .Release.Name }}-portal
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ .Release.Name }}-dashboard-resources
subjects:
{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "super-admin" .Release.Namespace) }}
roleRef:
kind: Role
name: {{ .Release.Name }}-dashboard-resources
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,201 @@
{{- $host := .Values._namespace.host }}
{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }}
{{- $adminPassword := randAlphaNum 16 }}
{{- $redisPassword := randAlphaNum 32 }}
{{- if $existingSecret }}
{{- $adminPassword = index $existingSecret.data "admin-password" | b64dec }}
{{- if hasKey $existingSecret.data "redis-password" }}
{{- $redisPassword = index $existingSecret.data "redis-password" | b64dec }}
{{- end }}
{{- end }}
{{- $existingCoreSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-core" .Release.Name) }}
{{- $tokenKey := "" }}
{{- $tokenCert := "" }}
{{- if $existingCoreSecret }}
{{- if hasKey $existingCoreSecret.data "tls.key" }}
{{- $tokenKey = index $existingCoreSecret.data "tls.key" | b64dec }}
{{- end }}
{{- if hasKey $existingCoreSecret.data "tls.crt" }}
{{- $tokenCert = index $existingCoreSecret.data "tls.crt" | b64dec }}
{{- end }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-credentials
stringData:
admin-password: {{ $adminPassword | quote }}
redis-password: {{ $redisPassword | quote }}
url: https://{{ $harborHost }}
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: {{ .Release.Name }}-system
labels:
sharding.fluxcd.io/key: tenants
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-harbor-application-default-harbor-system
namespace: cozy-system
interval: 5m
timeout: 15m
install:
remediation:
retries: -1
upgrade:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
- kind: Secret
name: {{ .Release.Name }}-credentials
valuesKey: redis-password
targetPath: redis.password
- kind: Secret
name: {{ .Release.Name }}-credentials
valuesKey: redis-password
targetPath: harbor.redis.external.password
values:
bucket:
secretName: {{ .Release.Name }}-registry-bucket
db:
replicas: {{ .Values.database.replicas }}
size: {{ .Values.database.size }}
{{- with .Values.storageClass }}
storageClass: {{ . }}
{{- end }}
redis:
replicas: {{ .Values.redis.replicas }}
size: {{ .Values.redis.size }}
{{- with .Values.storageClass }}
storageClass: {{ . }}
{{- end }}
harbor:
fullnameOverride: {{ .Release.Name }}
harborAdminPassword: {{ $adminPassword | quote }}
externalURL: https://{{ $harborHost }}
expose:
type: clusterIP
clusterIP:
name: {{ .Release.Name }}
tls:
enabled: false
persistence:
enabled: true
resourcePolicy: "keep"
imageChartStorage:
type: s3
s3:
existingSecret: {{ .Release.Name }}-registry-s3
region: us-east-1
bucket: {{ .Release.Name }}-registry
secure: false
v4auth: true
{{- if .Values.trivy.enabled }}
persistentVolumeClaim:
trivy:
size: {{ .Values.trivy.size }}
{{- with .Values.storageClass }}
storageClass: {{ . }}
{{- end }}
{{- end }}
portal:
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list "nano" (dict) $) | nindent 10 }}
core:
{{- if and $tokenKey $tokenCert }}
tokenKey: {{ $tokenKey | quote }}
tokenCert: {{ $tokenCert | quote }}
{{- end }}
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.core.resourcesPreset .Values.core.resources $) | nindent 10 }}
registry:
registry:
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.registry.resourcesPreset .Values.registry.resources $) | nindent 12 }}
controller:
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.registry.resourcesPreset .Values.registry.resources $) | nindent 12 }}
jobservice:
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.jobservice.resourcesPreset .Values.jobservice.resources $) | nindent 10 }}
trivy:
enabled: {{ .Values.trivy.enabled }}
{{- if .Values.trivy.enabled }}
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.trivy.resourcesPreset .Values.trivy.resources $) | nindent 10 }}
{{- end }}
database:
type: external
external:
host: "{{ .Release.Name }}-db-rw"
port: "5432"
username: app
coreDatabase: app
sslmode: require
existingSecret: "{{ .Release.Name }}-db-app"
redis:
type: external
external:
addr: "rfs-{{ .Release.Name }}-redis:26379"
sentinelMasterSet: "mymaster"
coreDatabaseIndex: "0"
jobserviceDatabaseIndex: "1"
registryDatabaseIndex: "2"
trivyAdapterIndex: "5"
metrics:
enabled: true
serviceMonitor:
enabled: true
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-core
spec:
replicas: 1
minReplicas: 1
kind: harbor
type: core
selector:
release: {{ $.Release.Name }}-system
component: core
version: {{ $.Chart.Version }}
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-registry
spec:
replicas: 1
minReplicas: 1
kind: harbor
type: registry
selector:
release: {{ $.Release.Name }}-system
component: registry
version: {{ $.Chart.Version }}
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-portal
spec:
replicas: 1
minReplicas: 1
kind: harbor
type: portal
selector:
release: {{ $.Release.Name }}-system
component: portal
version: {{ $.Chart.Version }}

View File

@@ -0,0 +1,37 @@
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "900"
nginx.ingress.kubernetes.io/proxy-send-timeout: "900"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
spec:
ingressClassName: {{ $ingress }}
tls:
- hosts:
- {{ $harborHost | quote }}
secretName: {{ .Release.Name }}-ingress-tls
rules:
- host: {{ $harborHost | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}
port:
number: 80

View File

@@ -0,0 +1,315 @@
{
"title": "Chart Values",
"type": "object",
"properties": {
"core": {
"description": "Core API server configuration.",
"type": "object",
"default": {},
"properties": {
"resources": {
"description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "Number of CPU cores allocated.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Amount of memory allocated.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted.",
"type": "string",
"default": "small",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
}
}
},
"database": {
"description": "PostgreSQL database configuration.",
"type": "object",
"default": {},
"required": [
"replicas",
"size"
],
"properties": {
"replicas": {
"description": "Number of database instances.",
"type": "integer",
"default": 2
},
"size": {
"description": "Persistent Volume size for database storage.",
"default": "5Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"host": {
"description": "Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).",
"type": "string",
"default": ""
},
"jobservice": {
"description": "Background job service configuration.",
"type": "object",
"default": {},
"properties": {
"resources": {
"description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "Number of CPU cores allocated.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Amount of memory allocated.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted.",
"type": "string",
"default": "nano",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
}
}
},
"redis": {
"description": "Redis cache configuration.",
"type": "object",
"default": {},
"required": [
"replicas",
"size"
],
"properties": {
"replicas": {
"description": "Number of Redis replicas.",
"type": "integer",
"default": 2
},
"size": {
"description": "Persistent Volume size for cache storage.",
"default": "1Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"registry": {
"description": "Container image registry configuration.",
"type": "object",
"default": {},
"properties": {
"resources": {
"description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "Number of CPU cores allocated.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Amount of memory allocated.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted.",
"type": "string",
"default": "small",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
}
}
},
"storageClass": {
"description": "StorageClass used to store the data.",
"type": "string",
"default": ""
},
"trivy": {
"description": "Trivy vulnerability scanner configuration.",
"type": "object",
"default": {},
"required": [
"enabled",
"size"
],
"properties": {
"enabled": {
"description": "Enable or disable the vulnerability scanner.",
"type": "boolean",
"default": true
},
"resources": {
"description": "Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "Number of CPU cores allocated.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Amount of memory allocated.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted.",
"type": "string",
"default": "nano",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
},
"size": {
"description": "Persistent Volume size for vulnerability database cache.",
"default": "5Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
}
}
}

View File

@@ -0,0 +1,84 @@
##
## @section Common parameters
##
## @param {string} [host] - Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).
host: ""
## @param {string} storageClass - StorageClass used to store the data.
storageClass: ""
##
## @section Component configuration
##
## @typedef {struct} Resources - Resource configuration.
## @field {quantity} [cpu] - Number of CPU cores allocated.
## @field {quantity} [memory] - Amount of memory allocated.
## @enum {string} ResourcesPreset - Default sizing preset.
## @value nano
## @value micro
## @value small
## @value medium
## @value large
## @value xlarge
## @value 2xlarge
## @typedef {struct} Core - Core API server configuration.
## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted.
## @param {Core} core - Core API server configuration.
core:
resources: {}
resourcesPreset: "small"
## @typedef {struct} Registry - Container image registry configuration.
## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted.
## @param {Registry} registry - Container image registry configuration.
registry:
resources: {}
resourcesPreset: "small"
## @typedef {struct} Jobservice - Background job service configuration.
## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted.
## @param {Jobservice} jobservice - Background job service configuration.
jobservice:
resources: {}
resourcesPreset: "nano"
## @typedef {struct} Trivy - Trivy vulnerability scanner configuration.
## @field {bool} enabled - Enable or disable the vulnerability scanner.
## @field {quantity} size - Persistent Volume size for vulnerability database cache.
## @field {Resources} [resources] - Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
## @field {ResourcesPreset} [resourcesPreset] - Default sizing preset used when `resources` is omitted.
## @param {Trivy} trivy - Trivy vulnerability scanner configuration.
trivy:
enabled: true
size: 5Gi
resources: {}
resourcesPreset: "nano"
## @typedef {struct} Database - PostgreSQL database configuration (provisioned via CloudNativePG).
## @field {quantity} size - Persistent Volume size for database storage.
## @field {int} replicas - Number of database instances.
## @param {Database} database - PostgreSQL database configuration.
database:
size: 5Gi
replicas: 2
## @typedef {struct} Redis - Redis cache configuration (provisioned via redis-operator).
## @field {quantity} size - Persistent Volume size for cache storage.
## @field {int} replicas - Number of Redis replicas.
## @param {Redis} redis - Redis cache configuration.
redis:
size: 1Gi
replicas: 2

View File

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

View File

@@ -1,4 +1,4 @@
KUBERNETES_VERSION = v1.33
KUBERNETES_VERSION = v1.35
KUBERNETES_PKG_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml)
include ../../../hack/common-envs.mk

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 major.minor version to deploy | `string` | `v1.33` |
| `version` | Kubernetes major.minor version to deploy | `string` | `v1.35` |
| `host` | External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty. | `string` | `""` |

View File

@@ -0,0 +1,4 @@
# Konnectivity proxy version overrides per Kubernetes minor version.
# When empty or absent, Kamaji auto-derives v0.{minor}.0 from the Kubernetes version.
# Add entries here only when the auto-derived image tag does not exist in the registry.
"v1.35": "v0.34.0"

View File

@@ -1,6 +1,6 @@
"v1.33": "v1.33.0"
"v1.32": "v1.32.10"
"v1.35": "v1.35.1"
"v1.34": "v1.34.4"
"v1.33": "v1.33.8"
"v1.32": "v1.32.12"
"v1.31": "v1.31.14"
"v1.30": "v1.30.14"
"v1.29": "v1.29.15"
"v1.28": "v1.28.15"

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:7deeee117e7eec599cb453836ca95eadd131dfc8c875dc457ef29dc1433395e0
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:3753b735b0315bee90de54cb25cfebc63bd2cc90ad11ca4fdc0e70439abd5096

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:1997d623bb60a5b540027a4e0716e7ca84f668ee09f31290e9c43324f0003137
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:434aa3b8e2a3cbf6681426b174e1c4fde23bafd12a6cccd046b5cb1749092ec4

View File

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

View File

@@ -5,3 +5,10 @@
{{- end }}
{{- index $versionMap .Values.version }}
{{- end }}
{{- define "kubernetes.konnectivityVersion" }}
{{- $konnVersionMap := .Files.Get "files/konnectivity-versions.yaml" | fromYaml }}
{{- if hasKey $konnVersionMap .Values.version }}
{{- index $konnVersionMap .Values.version }}
{{- end }}
{{- end }}

View File

@@ -126,8 +126,16 @@ spec:
dataStoreName: "{{ $etcd }}"
addons:
konnectivity:
{{- $konnVersion := include "kubernetes.konnectivityVersion" $ | trim }}
{{- if $konnVersion }}
agent:
version: {{ $konnVersion }}
{{- end }}
server:
port: 8132
{{- if $konnVersion }}
version: {{ $konnVersion }}
{{- end }}
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.controlPlane.konnectivity.server.resourcesPreset .Values.controlPlane.konnectivity.server.resources $) | nindent 10 }}
kubelet:
cgroupfs: systemd

View File

@@ -1,4 +1,5 @@
{{- $targetTenant := .Values._namespace.monitoring }}
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
{{- if .Values.addons.monitoringAgents.enabled }}
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
@@ -49,7 +50,7 @@ spec:
cluster: {{ .Release.Name }}
tenant: {{ .Release.Namespace }}
remoteWrite:
url: http://vminsert-shortterm.{{ $targetTenant }}.svc:8480/insert/0/prometheus
url: http://vminsert-shortterm.{{ $targetTenant }}.svc.{{ $clusterDomain }}:8480/insert/0/prometheus
fluent-bit:
readinessProbe:
httpGet:
@@ -72,7 +73,7 @@ spec:
[OUTPUT]
Name http
Match kube.*
Host vlogs-generic.{{ $targetTenant }}.svc
Host vlogs-generic.{{ $targetTenant }}.svc.{{ $clusterDomain }}
port 9428
compress gzip
uri /insert/jsonline?_stream_fields=stream,kubernetes_pod_name,kubernetes_container_name,kubernetes_namespace_name&_msg_field=log&_time_field=date

View File

@@ -621,14 +621,14 @@
"version": {
"description": "Kubernetes major.minor version to deploy",
"type": "string",
"default": "v1.33",
"default": "v1.35",
"enum": [
"v1.35",
"v1.34",
"v1.33",
"v1.32",
"v1.31",
"v1.30",
"v1.29",
"v1.28"
"v1.30"
]
}
}

View File

@@ -48,15 +48,15 @@ nodeGroups:
##
## @enum {string} Version
## @value v1.35
## @value v1.34
## @value v1.33
## @value v1.32
## @value v1.31
## @value v1.30
## @value v1.29
## @value v1.28
## @param {Version} version - Kubernetes major.minor version to deploy
version: "v1.33"
version: "v1.35"
## @param {string} host - External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty.

View File

@@ -95,10 +95,6 @@ spec:
{{- with .Values.storageClass }}
storageClassName: {{ . }}
{{- end }}
promExporter:
enabled: true
podMonitor:
enabled: true
{{- if .Values.external }}
service:
merge:

View File

@@ -0,0 +1,3 @@
.helmignore
/logos
/Makefile

View File

@@ -0,0 +1,7 @@
apiVersion: v2
name: openbao
description: Managed OpenBAO secrets management service
icon: /logos/openbao.svg
type: application
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
appVersion: "2.5.0"

View File

@@ -0,0 +1,5 @@
include ../../../hack/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
../../../hack/update-crd.sh

View File

@@ -0,0 +1,27 @@
# Managed OpenBAO Service
OpenBAO is an open-source secrets management solution forked from HashiCorp Vault.
It provides identity-based secrets and encryption management for cloud infrastructure.
## Parameters
### Common parameters
| Name | Description | Type | Value |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------- |
| `replicas` | Number of OpenBAO replicas. HA with Raft is automatically enabled when replicas > 1. Switching between standalone (file storage) and HA (Raft storage) modes requires data migration. | `int` | `1` |
| `resources` | Explicit CPU and memory configuration for each OpenBAO replica. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
| `resources.cpu` | CPU available to each replica. | `quantity` | `""` |
| `resources.memory` | Memory (RAM) available to each replica. | `quantity` | `""` |
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `small` |
| `size` | Persistent Volume Claim size for data storage. | `quantity` | `10Gi` |
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
### Application-specific parameters
| Name | Description | Type | Value |
| ---- | -------------------------- | ------ | ------ |
| `ui` | Enable the OpenBAO web UI. | `bool` | `true` |

View File

@@ -0,0 +1 @@
../../../library/cozy-lib

View File

@@ -0,0 +1,11 @@
<svg width="144" height="144" viewBox="0 0 144 144" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="144" height="144" rx="24" fill="url(#paint0_linear)"/>
<rect width="144" height="144" rx="24" fill="black" fill-opacity="0.3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M72 30C53.222 30 38 45.222 38 64v8c-3.314 0-6 2.686-6 6v30c0 3.314 2.686 6 6 6h68c3.314 0 6-2.686 6-6V78c0-3.314-2.686-6-6-6v-8C106 45.222 90.778 30 72 30zm-8 42v-8c0-4.418 3.582-8 8-8s8 3.582 8 8v8H64zm26 0v-8c0-8.837-7.163-16-16-16s-16 7.163-16 16v8h-2v28h60V72H90zm-22 14a4 4 0 118 0 4 4 0 01-8 0zm4-8a8 8 0 100 16 8 8 0 000-16z" fill="white"/>
<defs>
<linearGradient id="paint0_linear" x1="10" y1="15.5" x2="144" y2="131.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#87d6be"/>
<stop offset="1" stop-color="#79c0ab"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@@ -0,0 +1,49 @@
{{/*
Copyright Broadcom, Inc. All Rights Reserved.
SPDX-License-Identifier: APACHE-2.0
*/}}
{{/* vim: set filetype=mustache: */}}
{{/*
Return a resource request/limit object based on a given preset.
These presets are for basic testing and not meant to be used in production
{{ include "resources.preset" (dict "type" "nano") -}}
*/}}
{{- define "resources.preset" -}}
{{- $presets := dict
"nano" (dict
"requests" (dict "cpu" "100m" "memory" "128Mi" "ephemeral-storage" "50Mi")
"limits" (dict "memory" "128Mi" "ephemeral-storage" "2Gi")
)
"micro" (dict
"requests" (dict "cpu" "250m" "memory" "256Mi" "ephemeral-storage" "50Mi")
"limits" (dict "memory" "256Mi" "ephemeral-storage" "2Gi")
)
"small" (dict
"requests" (dict "cpu" "500m" "memory" "512Mi" "ephemeral-storage" "50Mi")
"limits" (dict "memory" "512Mi" "ephemeral-storage" "2Gi")
)
"medium" (dict
"requests" (dict "cpu" "500m" "memory" "1Gi" "ephemeral-storage" "50Mi")
"limits" (dict "memory" "1Gi" "ephemeral-storage" "2Gi")
)
"large" (dict
"requests" (dict "cpu" "1" "memory" "2Gi" "ephemeral-storage" "50Mi")
"limits" (dict "memory" "2Gi" "ephemeral-storage" "2Gi")
)
"xlarge" (dict
"requests" (dict "cpu" "2" "memory" "4Gi" "ephemeral-storage" "50Mi")
"limits" (dict "memory" "4Gi" "ephemeral-storage" "2Gi")
)
"2xlarge" (dict
"requests" (dict "cpu" "4" "memory" "8Gi" "ephemeral-storage" "50Mi")
"limits" (dict "memory" "8Gi" "ephemeral-storage" "2Gi")
)
}}
{{- if hasKey $presets .type -}}
{{- index $presets .type | toYaml -}}
{{- else -}}
{{- printf "ERROR: Preset key '%s' invalid. Allowed values are %s" .type (join "," (keys $presets)) | fail -}}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,31 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ .Release.Name }}-dashboard-resources
rules:
- apiGroups:
- ""
resources:
- services
resourceNames:
- {{ .Release.Name }}
- {{ .Release.Name }}-internal
verbs: ["get", "list", "watch"]
- apiGroups:
- cozystack.io
resources:
- workloadmonitors
resourceNames:
- {{ .Release.Name }}
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ .Release.Name }}-dashboard-resources
subjects:
{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "use" .Release.Namespace) }}
roleRef:
kind: Role
name: {{ .Release.Name }}-dashboard-resources
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,99 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: {{ .Release.Name }}-system
labels:
sharding.fluxcd.io/key: tenants
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-openbao-application-default-openbao-system
namespace: cozy-system
interval: 5m
timeout: 10m
install:
remediation:
retries: -1
upgrade:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
values:
openbao:
fullnameOverride: {{ .Release.Name }}
global:
tlsDisable: true
server:
podManagementPolicy: Parallel
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 10 }}
dataStorage:
enabled: true
size: {{ .Values.size }}
{{- with .Values.storageClass }}
storageClass: {{ . }}
{{- end }}
{{- if gt (int .Values.replicas) 1 }}
standalone:
enabled: false
ha:
enabled: true
replicas: {{ .Values.replicas }}
raft:
enabled: true
setNodeId: true
config: |
ui = {{ .Values.ui }}
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = true
}
storage "raft" {
path = "/openbao/data"
{{- range $i := until (int $.Values.replicas) }}
retry_join {
leader_api_addr = "http://{{ $.Release.Name }}-{{ $i }}.{{ $.Release.Name }}-internal:8200"
}
{{- end }}
}
service_registration "kubernetes" {}
{{- else }}
standalone:
enabled: true
config: |
ui = {{ .Values.ui }}
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = true
}
storage "file" {
path = "/openbao/data"
}
# Note: service_registration "kubernetes" {} is intentionally omitted
# in standalone mode — it requires an HA-capable storage backend and
# causes a fatal error with storage "file".
ha:
enabled: false
{{- end }}
{{- if .Values.external }}
service:
type: LoadBalancer
{{- end }}
ui:
enabled: {{ .Values.ui }}
{{- if .Values.external }}
serviceType: LoadBalancer
{{- end }}
injector:
enabled: false
csi:
enabled: false

View File

@@ -0,0 +1,13 @@
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1
kind: openbao
type: openbao
selector:
app.kubernetes.io/instance: {{ $.Release.Name }}-system
version: {{ $.Chart.Version }}

View File

@@ -0,0 +1,87 @@
{
"title": "Chart Values",
"type": "object",
"properties": {
"external": {
"description": "Enable external access from outside the cluster.",
"type": "boolean",
"default": false
},
"replicas": {
"description": "Number of OpenBAO replicas. HA with Raft is automatically enabled when replicas \u003e 1. Switching between standalone (file storage) and HA (Raft storage) modes requires data migration.",
"type": "integer",
"default": 1
},
"resources": {
"description": "Explicit CPU and memory configuration for each OpenBAO replica. When omitted, the preset defined in `resourcesPreset` is applied.",
"type": "object",
"default": {},
"properties": {
"cpu": {
"description": "CPU available to each replica.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"memory": {
"description": "Memory (RAM) available to each replica.",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"resourcesPreset": {
"description": "Default sizing preset used when `resources` is omitted.",
"type": "string",
"default": "small",
"enum": [
"nano",
"micro",
"small",
"medium",
"large",
"xlarge",
"2xlarge"
]
},
"size": {
"description": "Persistent Volume Claim size for data storage.",
"default": "10Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"x-kubernetes-int-or-string": true
},
"storageClass": {
"description": "StorageClass used to store the data.",
"type": "string",
"default": ""
},
"ui": {
"description": "Enable the OpenBAO web UI.",
"type": "boolean",
"default": true
}
}
}

View File

@@ -0,0 +1,41 @@
##
## @section Common parameters
##
## @typedef {struct} Resources - Explicit CPU and memory configuration for each OpenBAO replica.
## @field {quantity} [cpu] - CPU available to each replica.
## @field {quantity} [memory] - Memory (RAM) available to each replica.
## @enum {string} ResourcesPreset - Default sizing preset.
## @value nano
## @value micro
## @value small
## @value medium
## @value large
## @value xlarge
## @value 2xlarge
## @param {int} replicas - Number of OpenBAO replicas. HA with Raft is automatically enabled when replicas > 1. Switching between standalone (file storage) and HA (Raft storage) modes requires data migration.
replicas: 1
## @param {Resources} [resources] - Explicit CPU and memory configuration for each OpenBAO replica. When omitted, the preset defined in `resourcesPreset` is applied.
resources: {}
## @param {ResourcesPreset} resourcesPreset="small" - Default sizing preset used when `resources` is omitted.
resourcesPreset: "small"
## @param {quantity} size - Persistent Volume Claim size for data storage.
size: 10Gi
## @param {string} storageClass - StorageClass used to store the data.
storageClass: ""
## @param {bool} external - Enable external access from outside the cluster.
external: false
##
## @section Application-specific parameters
##
## @param {bool} ui - Enable the OpenBAO web UI.
ui: true

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