Compare commits

..

64 Commits

Author SHA1 Message Date
Andrei Kvapil
f49269e808 [bucket] Add script for locking bucket
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-30 17:38:24 +01:00
Andrei Kvapil
2d6e50bbeb [ci] Fix auto-release workflow
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-30 12:20:40 +01:00
Andrei Kvapil
dea42d035f [tenant] Run cleanup job from system namespace (#1774)
## What this PR does

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

### Release note

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

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

## Summary by CodeRabbit

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-30 12:03:05 +01:00
Andrei Kvapil
f3722d0b2a [tenant] Allow egress to parent ingress pods (#1765)
## What this PR does

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

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

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

### Release note

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

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

## Summary by CodeRabbit

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-30 11:59:33 +01:00
Andrei Kvapil
f4e43469dc [kubevirt-operator] Fix typo in VMNotRunningFor10Minutes alert (#1770)
## What this PR does

Fix case sensitivity in status check for VMNotRunningFor10Minutes alert
rule.

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

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

### Release note

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

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

## Summary by CodeRabbit

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-30 11:58:10 +01:00
Andrei Kvapil
7e86a04fab [cozystack-operator] Add deployment files (#1761)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does


### Release note

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

```release-note
[cozystack-operator] Add deployment files
```

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

* **New Features**
* Deployable CozyStack operator with configurable enablement, image, and
platform-source settings
* Operator-managed package source and variant-based platform
installation options

* **Chores**
* Release/packaging targets added to automate image and package
publishing and update manifests
* Configuration schema extended to include operator-related fields and
versioning controls

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-30 11:57:23 +01:00
Andrei Kvapil
935addebe9 [installer,dx] Rename cozypkg to cozyhr (#1763)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does


This PR renames cozypkg to cozyhr
https://github.com/cozystack/cozyhr

### Release note

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

```release-note
[installer,dx] Rename cozypkg to cozyhr
```
2025-12-30 11:56:59 +01:00
Andrei Kvapil
a902eaec91 [platform] Add PackageSources for cozystack-operator (#1760)
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


![dependencies](https://github.com/user-attachments/assets/b3cfac33-6543-4f73-85c5-1d21c35f8eab)

### 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] Add PackageSources for cozystack-operator 
```

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

## Summary by CodeRabbit

* **New Features**
* Added support for 76 new package sources, including operators,
applications, and system components for the CozyStack platform ecosystem
(e.g., backup controller, database operators, monitoring tools,
networking, storage, and virtualization packages).
* Introduced a template aggregation mechanism to manage package source
definitions.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-30 11:56:47 +01:00
Andrei Kvapil
c200a45824 [cozypkg] Add tool for managing Package and PackageSources (#1756)
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
[cozypkg] Add tool for managing Package and PackageSources
```

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

* **New Features**
  * New cozy CLI with entrypoint and root command.
* Interactive "add" command to install packages from args or files, with
dependency resolution, topological install order, variant selection, and
progress feedback.
* "del" command to remove packages safely, including dependent analysis,
confirmation prompts, and safe delete ordering.
* "list" command to show available or installed packages, with optional
per-component output and concise status lines.
* "dependencies" command to emit Graphviz DOT graphs of
package/component relationships.
* Common flags: kubeconfig, file inputs, --installed and --components
where applicable.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-30 11:56:31 +01:00
Andrei Kvapil
c06f399d20 [cozystack-operator] Add Package and PackageSource reconcilers (#1755)
depends on https://github.com/cozystack/cozystack/pull/1741

## What this PR does


### Release note

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

```release-note
[cozystack-operator] Add Package and PackageSource reconcilers
```

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

* **New Features**
* Launches CozyStack operator with CLI-configurable runtime
(metrics/health/leader election/HTTP2), optional pre-reconcile steps,
optional automatic Flux install, and platform-source URL support
(OCI/Git).
* Adds PackageSource and Package controllers for artifact generation,
variant resolution, dependency tracking, HelmRelease orchestration,
namespace reconciliation, orphan cleanup, and status propagation.
  * Package status now exposes per-dependency readiness.

* **Chores**
* Embeds Flux manifests and adds a local Flux install workflow; updates
manifest generation path and Makefile targets.
* **Other**
  * PackageSource CRD shortName simplified to "pks".

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-30 11:55:52 +01:00
Andrei Kvapil
f28d639aea [cozystack-operator] Add Package and PackageSource reconcilers
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-30 11:55:21 +01:00
Nikita
63baadfd9a [multus] Increase memory limit (#1773)
## What this PR does
Increases multus memory limit.
Based on multiple community reports stating that multus tend to consume
a lot of memory during startup after a node reboot.

### Release note
```release-note
Multus memory limit increased.
```
2025-12-30 13:55:05 +03:00
Andrei Kvapil
2d4e568efe [cozystack-operator] Introduce Cozystack-operator core logic (#1741)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does


### Release note

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

```release-note
[cozystack-operator] Introduce Cozystack-operator core logic
```

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

* **New Features**
* Operator entrypoint with flag-driven configuration (metrics, probes,
leader election, Flux pre-install, version, platform-source options).
* Optional pre-start Flux installation and automatic provisioning of
platform sources from OCI or Git/HTTP(S)/SSH.

* **Chores**
* Embedded Flux manifests packaged for simplified installation; build
tooling updated to produce new manifest outputs.

* **Behavior**
* Pre-start Flux and platform-source installation failures are treated
as fatal.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-30 11:48:13 +01:00
Timofei Larkin
b94fc6c9c8 [tenant] Run cleanup job from system namespace
## What this PR does

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

### Release note

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

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-30 12:48:48 +03:00
nbykov0
36ef62af52 [multus] Increase memory limit
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-29 21:33:13 +03:00
Andrey Kolkov
715509f57e [backups] Scaffold a Velero strategy (#1742)
## What this PR does

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

###

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

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

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

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-29 22:12:59 +04:00
Andrei Kvapil
db8a006565 Apply review suggestions
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-29 14:38:31 +01:00
Aleksei Sviridkin
7fab919e07 [kubevirt-operator] Fix typo in VMNotRunningFor10Minutes alert
Fix case sensitivity in status check: "running" → "Running".
The metric uses "Running" (capital R), so the lowercase check was
always triggering alerts even for running VMs.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2025-12-29 16:06:08 +03:00
Andrei Kvapil
c43db3b7cc [cozystack-operator] Add deployment files
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-29 14:04:10 +01:00
Aleksei Sviridkin
14803a4162 [cilium] Update Cilium to v1.18.5 (#1769)
## What this PR does

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

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

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

### Release note

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

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

## Summary by CodeRabbit

## Release Notes

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

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-29 16:04:02 +03:00
Andrei Kvapil
3a1f4ab854 [cozystack-controller] Fix: move crds to definitions (#1759)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does


### Release note

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

```release-note
[cozystack-controller] Fix: move crds to definitions
```

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

## Summary by CodeRabbit

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-29 13:56:09 +01:00
Aleksei Sviridkin
307f20f8af [cilium] Update Cilium to v1.18.5
Update Cilium from v1.17.8 to v1.18.5.

Breaking changes addressed:
- Remove deprecated enableRuntimeDeviceDetection option from
  values-kubeovn.yaml (now default behavior in 1.18)
- Use SED_INPLACE variable for macOS compatibility in Makefile

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2025-12-29 15:16:40 +03:00
Aleksei Sviridkin
5af0d132a5 [scripts] Add SED_INPLACE for macOS compatibility
Add SED_INPLACE variable to common-envs.mk that detects the OS and uses
the appropriate sed in-place syntax: `sed -i ''` on macOS (Darwin) and
`sed -i` on Linux.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2025-12-29 15:16:25 +03:00
Timofei Larkin
a03042e017 [platform] Add alphabetical sorting to registry resource lists (#1764)
## What this PR does

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

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

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

### Release note

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

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

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

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

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

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-29 12:52:13 +04:00
Aleksei Sviridkin
4dfdbfeb62 fix(tenant): allow egress to parent ingress pods
Nested Kubernetes clusters with exposeMethod: Proxied cannot reach their
own external domains because the clusterwide egress policy blocks traffic
to ingress pods in parent namespaces.

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2025-12-26 17:44:23 +03:00
Aleksei Sviridkin
da56c88aa7 [registry] Remove unused ObjectMetaWrapper
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2025-12-26 13:06:35 +03:00
Aleksei Sviridkin
e9cad4507b [registry] Use strings.Compare for sorting
- Simplify ByName with strings.Compare
- Avoid string allocation in ByNamespacedName by comparing fields separately

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2025-12-26 13:01:23 +03:00
Aleksei Sviridkin
19e06f7beb [registry] Fix linter errors
- Remove unused helmReleaseGVR variables
- Fix non-constant format strings in klog.Errorf calls
- Simplify embedded field selectors (ListMeta, ObjectMeta, Generic)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2025-12-26 12:52:49 +03:00
Aleksei Sviridkin
0b16f83eae [registry] Add alphabetical sorting to resource lists
Add sorting helper package with generic functions for sorting
resources by name (cluster-scoped) or namespace/name (namespace-scoped).

Apply sorting to:
- TenantNamespace (by name)
- TenantSecret (refactored to use helper)
- TenantModule (new)
- Application (new)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2025-12-26 12:48:25 +03:00
Timofei Larkin
8cf68149cb [backups] Scaffold a Velero strategy
## What this PR does

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

###

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

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-26 10:14:29 +03:00
Andrei Kvapil
cbc998b2ec Rename cozypkg to cozyhr
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-25 16:54:22 +01:00
Andrei Kvapil
fe9fca6abf [platform] Add PackageSources for cozystack-operator
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-25 15:48:52 +01:00
Andrei Kvapil
d3f1b821f7 [cozypkg] Add tool for managing Package and PackageSources
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-25 15:47:10 +01:00
Andrei Kvapil
7165cea57a Fix: move controller crds to definitions
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-25 12:56:53 +01:00
Andrei Kvapil
2990f0520a [cozystack-operator] Introduce Cozystack-operator core logic
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-25 10:59:48 +01:00
Andrei Kvapil
d7931fdb20 [agents] Add instructions for working with unresolved code review comments (#1710)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does


### Release note

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

```release-note
[agents] Add instructions for working with unresolved code review comments
```
2025-12-25 10:45:02 +01:00
Andrei Kvapil
0697c0221b [workflow] Push tags as cozystack-bot to trigger GitHub workflows
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-25 10:39:21 +01:00
Andrei Kvapil
59f62b7834 [workflows] Add auto patch release workflow (#1754)
This PR adds a new GitHub workflow that automatically creates patch
releases for maintenance branches.

## What it does

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

## Why releases instead of tags

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

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

## Summary by CodeRabbit

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-24 21:11:07 +01:00
Andrei Kvapil
f28b53d8dc [workflow] Add GitHub Action to update release notes from changelogs (#1752)
This PR adds a GitHub Actions workflow that automatically updates
release notes from changelog files when changes are merged to main.

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

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

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

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

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

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

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

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

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

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

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

### Release note

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



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

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

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-24 15:15:25 +01:00
Andrei Kvapil
9d85cfb647 [workflow] Add GitHub Action to update release notes from changelogs
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-24 15:02:04 +01:00
Andrei Kvapil
db800b510e [workflows] Add auto patch release workflow
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-24 14:52:40 +01:00
Andrei Kvapil
202410a438 Add changelogs to v.0.39.1 (#1750)
<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Release note

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

```release-note
Add changelogs for v.0.39.0 and v.0.39.1
```

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

## Summary by CodeRabbit

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

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

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-24 14:11:24 +01:00
Andrei Kvapil
f7877b9bce [cozystack-operator] Introduce API objects: packages and packagesources (#1740)
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

See the examples (not part of this PR)


PackageSource:
-
43cef5d50e/packages/core/platform/templates/sources/backup-controller.yaml

Package:
-
43cef5d50e/packages/core/platform/templates/packages/isp-full.yaml

Platform Package:
-
43cef5d50e/packages/core/installer/example/platform.yaml

### 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-operator] Introduce API objects: packages and packagesources
```

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

* **New Features**
* Introduced Package and PackageSource custom resources to manage
software packages and their configurations, with support for variants,
components, library management, and dependency handling.

* **Bug Fixes**
  * Made the Chart field optional in CozystackResourceDefinitionRelease.

* **Chores**
  * Updated code generation tooling and CRD artifact management.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-24 14:10:45 +01:00
Andrei Kvapil
ab6c6bad16 Update go modules (#1736)
This change is extracted from
- https://github.com/cozystack/cozystack/pull/1641

and reworked to work standalone

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

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

## What this PR does


### Release note

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

```release-note
[cozystack] Update go modules
```

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

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

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

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

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

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

### Release note

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

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

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

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

and reworked to work standalone

## What this PR does

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

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

### Release note

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

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

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

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

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

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-24 14:09:38 +01:00
Andrey Kolkov
517369629f Add changelogs for v0.39.0 and v.0.39.1
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2025-12-24 12:57:54 +04:00
Andrei Kvapil
00cef35214 Fix review comments
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-23 19:54:58 +01:00
Andrei Kvapil
e213b068e8 Update go modules
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-23 19:45:47 +01:00
Andrei Kvapil
98488100e4 [cozystack-operator] Introduce API objects: packages and packagesources
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-23 19:22:33 +01:00
Nikita
0ed52f670d [system/cilium] Enable topology-aware routing for services (#1734) 2025-12-23 17:11:22 +03:00
Timofei Larkin
b6900a258a Add changelogs for v0.38.3 and v.0.38.4 (#1743) 2025-12-23 17:04:05 +04:00
Nikita
c919ea6e39 [cilium] Enable automatic pod rollout on configmap updates (#1728)
Enable automatic restart of cilium and cilium-operator pods when
cilium-config ConfigMap is updated.

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

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

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

## Summary by CodeRabbit

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-23 15:41:12 +03:00
Nikita
7fa875db52 [virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config (#1693)
This PR adds nodeAffinity configuration to virtual-machine and
vm-instance charts to support dedicated nodes for Windows VMs.

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

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

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

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

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-23 15:39:19 +03:00
Andrey Kolkov
cf15ad1073 Add changelogs for v0.38.3 and v.0.38.4
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2025-12-23 16:33:55 +04:00
Timofei Larkin
ba50d01877 [vm] Always expose VMs with a service
## What this PR does

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

### Release note

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

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-19 14:51:09 +03:00
Andrei Kvapil
7e7716aa44 Fix review comments
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-19 10:13:36 +01:00
Andrei Kvapil
669bf3d2f5 [registry] Add application labels and update filtering mechanism
- Add three application metadata labels to HelmRelease:
  - apps.cozystack.io/application.kind
  - apps.cozystack.io/application.group
  - apps.cozystack.io/application.name

- Replace shouldIncludeHelmRelease filtering with label-based filtering
  in Get, List, and Update methods

- Always add kind and group label requirements in List for precise filtering

- Update CozystackResourceDefinitionController to watch only HelmReleases
  with cozystack.io/ui=true label

- Update LineageControllerWebhook to extract metadata directly from
  HelmRelease labels instead of using mapping configuration

- Add functionality to update HelmRelease chart from CozystackResourceDefinition
  using label selectors

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-19 10:13:36 +01:00
nbykov0
8d43f993e4 [system/cilium] Enable topology-aware routing for services
Signed-off-by: nbykov0 <166552198+nbykov0@users.noreply.github.com>
2025-12-19 00:57:44 +03:00
Andrei Kvapil
97e8d2aa49 [cilium] Enable automatic pod rollout on configmap updates
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-16 10:42:10 +01:00
Andrei Kvapil
c0e0ef0f7c [core] Extract Talos package from installer
Move Talos-related functionality from packages/core/installer to a separate packages/core/talos package:
- Move Talos profiles, matchbox configuration and build scripts
- Update installer Makefile to remove Talos-related targets
- Update root Makefile to build talos package separately
- Update matchbox Dockerfile paths to reference talos package

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-15 08:04:41 +01:00
Andrei Kvapil
8866a307bf [agents] Add instructions for working with unresolved code review comments
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-10 21:10:56 +01:00
Andrei Kvapil
3975da93c6 [virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2025-12-09 13:27:55 +01:00
473 changed files with 18524 additions and 7107 deletions

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

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

View File

@@ -46,7 +46,12 @@ jobs:
fetch-depth: 0
- name: Create tag on merge commit
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
git tag -f ${{ steps.get_tag.outputs.tag }} ${{ github.sha }}
git push -f origin ${{ steps.get_tag.outputs.tag }}

View File

@@ -58,7 +58,7 @@ jobs:
DOCKER_CONFIG: ${{ runner.temp }}/.docker
- name: Build Talos image
run: make -C packages/core/installer talos-nocloud
run: make -C packages/core/talos talos-nocloud
- name: Save git diff as patch
if: "!contains(github.event.pull_request.labels.*.name, 'release')"

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ build: build-deps
make -C packages/system/bucket image
make -C packages/system/objectstorage-controller image
make -C packages/core/testing image
make -C packages/core/talos image
make -C packages/core/platform image
make -C packages/core/installer image
make manifests
@@ -41,7 +42,7 @@ manifests:
(cd packages/core/installer/; helm template -n cozy-installer installer .) > _out/assets/cozystack-installer.yaml
assets:
make -C packages/core/installer assets
make -C packages/core/talos assets
test:
make -C packages/core/testing apply

View File

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

View File

@@ -121,3 +121,99 @@ func (in *JobStatus) DeepCopy() *JobStatus {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Velero) DeepCopyInto(out *Velero) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Velero.
func (in *Velero) DeepCopy() *Velero {
if in == nil {
return nil
}
out := new(Velero)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Velero) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroList) DeepCopyInto(out *VeleroList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Velero, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroList.
func (in *VeleroList) DeepCopy() *VeleroList {
if in == nil {
return nil
}
out := new(VeleroList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *VeleroList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroSpec) DeepCopyInto(out *VeleroSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroSpec.
func (in *VeleroSpec) DeepCopy() *VeleroSpec {
if in == nil {
return nil
}
out := new(VeleroSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroStatus) DeepCopyInto(out *VeleroStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroStatus.
func (in *VeleroStatus) DeepCopy() *VeleroStatus {
if in == nil {
return nil
}
out := new(VeleroStatus)
in.DeepCopyInto(out)
return out
}

View File

@@ -68,7 +68,7 @@ type PackageSpec struct {
Components map[string]PackageComponent `json:"components,omitempty"`
}
// PackageRelease defines overrides for a specific component
// PackageComponent defines overrides for a specific component
type PackageComponent struct {
// Enabled indicates whether this component should be installed
// If false, the component will be disabled even if it's defined in the PackageSource
@@ -86,4 +86,15 @@ type PackageStatus struct {
// Conditions represents the latest available observations of a Package's state
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Dependencies tracks the readiness status of each dependency
// Key is the dependency package name, value indicates if the dependency is ready
// +optional
Dependencies map[string]DependencyStatus `json:"dependencies,omitempty"`
}
// DependencyStatus represents the readiness status of a dependency
type DependencyStatus struct {
// Ready indicates whether the dependency is ready
Ready bool `json:"ready"`
}

View File

@@ -21,7 +21,7 @@ import (
)
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster,shortName={pkgsrc,pkgsrcs}
// +kubebuilder:resource:scope=Cluster,shortName={pks}
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Variants",type="string",JSONPath=".status.variants",description="Package variants (comma-separated)"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status"
@@ -81,19 +81,6 @@ type Variant struct {
Components []Component `json:"components,omitempty"`
}
// DependencyTarget defines a named group of packages that can be referenced
// by other package sources via dependsOn
type DependencyTarget struct {
// Name is the unique identifier for this dependency target
// +required
Name string `json:"name"`
// Packages is a list of package names that belong to this target
// These packages will be added as dependencies when this target is referenced
// +required
Packages []string `json:"packages"`
}
// Library defines a Helm library chart
type Library struct {
// Name is the optional name for library placed in charts

View File

@@ -309,21 +309,16 @@ func (in *CozystackResourceDefinitionSpec) DeepCopy() *CozystackResourceDefiniti
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DependencyTarget) DeepCopyInto(out *DependencyTarget) {
func (in *DependencyStatus) DeepCopyInto(out *DependencyStatus) {
*out = *in
if in.Packages != nil {
in, out := &in.Packages, &out.Packages
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyTarget.
func (in *DependencyTarget) DeepCopy() *DependencyTarget {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyStatus.
func (in *DependencyStatus) DeepCopy() *DependencyStatus {
if in == nil {
return nil
}
out := new(DependencyTarget)
out := new(DependencyStatus)
in.DeepCopyInto(out)
return out
}
@@ -587,6 +582,13 @@ func (in *PackageStatus) DeepCopyInto(out *PackageStatus) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Dependencies != nil {
in, out := &in.Dependencies, &out.Dependencies
*out = make(map[string]DependencyStatus, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageStatus.

View File

@@ -147,7 +147,7 @@ func main() {
os.Exit(1)
}
if err = (&backupcontroller.BackupJobStrategyReconciler{
if err = (&backupcontroller.BackupJobReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {

View File

@@ -72,6 +72,9 @@ Multiple -f flags can be specified, and they can point to files or directories.`
}
for _, pkg := range packages {
packageNames[pkg] = true
if oldPath, ok := packagesFromFiles[pkg]; ok {
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
}
packagesFromFiles[pkg] = filePath
}
}
@@ -208,25 +211,8 @@ func readPackagesFromYAMLFile(filePath string) ([]string, error) {
continue
}
// Try to parse as PackageList
if obj.GetKind() == "PackageList" {
items, found, err := unstructured.NestedSlice(obj.Object, "items")
if err == nil && found {
for _, item := range items {
if itemMap, ok := item.(map[string]interface{}); ok {
if metadata, ok := itemMap["metadata"].(map[string]interface{}); ok {
if name, ok := metadata["name"].(string); ok && name != "" {
packages = append(packages, name)
}
}
}
}
}
continue
}
// Try to parse as PackageSourceList
if obj.GetKind() == "PackageSourceList" {
// Try to parse as PackageList or PackageSourceList
if obj.GetKind() == "PackageList" || obj.GetKind() == "PackageSourceList" {
items, found, err := unstructured.NestedSlice(obj.Object, "items")
if err == nil && found {
for _, item := range items {
@@ -243,10 +229,8 @@ func readPackagesFromYAMLFile(filePath string) ([]string, error) {
}
}
if len(packages) == 0 {
return nil, fmt.Errorf("no valid packages found in file")
}
// Return empty list if no packages found - don't error out
// The check for whether any packages were specified at all is handled later in RunE
return packages, nil
}
@@ -481,33 +465,33 @@ func installPackage(ctx context.Context, k8sClient client.Client, packageSourceN
packageVariants[pkgName] = variant
}
// Now create all Package resources
for _, pkgName := range installOrder {
// Skip if already installed
if _, exists := installedMap[pkgName]; exists {
continue
}
variant := packageVariants[pkgName]
// Create Package
pkg := &cozyv1alpha1.Package{
ObjectMeta: metav1.ObjectMeta{
Name: pkgName,
},
Spec: cozyv1alpha1.PackageSpec{
Variant: variant,
},
}
if err := k8sClient.Create(ctx, pkg); err != nil {
return fmt.Errorf("failed to create Package %s: %w", pkgName, err)
}
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", pkgName)
// Now create all Package resources
for _, pkgName := range installOrder {
// Skip if already installed
if _, exists := installedMap[pkgName]; exists {
continue
}
return nil
variant := packageVariants[pkgName]
// Create Package
pkg := &cozyv1alpha1.Package{
ObjectMeta: metav1.ObjectMeta{
Name: pkgName,
},
Spec: cozyv1alpha1.PackageSpec{
Variant: variant,
},
}
if err := k8sClient.Create(ctx, pkg); err != nil {
return fmt.Errorf("failed to create Package %s: %w", pkgName, err)
}
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", pkgName)
}
return nil
}
// selectVariantInteractive prompts user to select a variant

View File

@@ -17,12 +17,15 @@ limitations under the License.
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@@ -51,6 +54,8 @@ Multiple -f flags can be specified, and they can point to files or directories.`
// Collect package names from arguments and files
packageNames := make(map[string]bool)
packagesFromFiles := make(map[string]string) // packageName -> filePath
for _, arg := range args {
packageNames[arg] = true
}
@@ -63,6 +68,10 @@ Multiple -f flags can be specified, and they can point to files or directories.`
}
for _, pkg := range packages {
packageNames[pkg] = true
if oldPath, ok := packagesFromFiles[pkg]; ok {
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
}
packagesFromFiles[pkg] = filePath
}
}
@@ -95,12 +104,51 @@ Multiple -f flags can be specified, and they can point to files or directories.`
return fmt.Errorf("failed to create k8s client: %w", err)
}
// Check which requested packages are installed
var installedPackages cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &installedPackages); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
installedMap := make(map[string]bool)
for _, pkg := range installedPackages.Items {
installedMap[pkg.Name] = true
}
// Warn about requested packages that are not installed
for pkgName := range packageNames {
if !installedMap[pkgName] {
fmt.Fprintf(os.Stderr, "⚠ Package %s is not installed, skipping\n", pkgName)
}
}
// Find all packages to delete (including dependents)
packagesToDelete, err := findPackagesToDelete(ctx, k8sClient, packageNames)
if err != nil {
return fmt.Errorf("failed to analyze dependencies: %w", err)
}
if len(packagesToDelete) == 0 {
fmt.Fprintf(os.Stderr, "No packages found to delete\n")
return nil
}
// Show packages to be deleted and ask for confirmation
if err := confirmDeletion(packagesToDelete, packageNames); err != nil {
return err
}
// Delete packages in reverse topological order (dependents first, then dependencies)
deleteOrder, err := getDeleteOrder(ctx, k8sClient, packagesToDelete)
if err != nil {
return fmt.Errorf("failed to determine delete order: %w", err)
}
// Delete each package
for packageName := range packageNames {
for _, packageName := range deleteOrder {
pkg := &cozyv1alpha1.Package{}
pkg.Name = packageName
if err := k8sClient.Delete(ctx, pkg); err != nil {
if client.IgnoreNotFound(err) == nil {
if apierrors.IsNotFound(err) {
fmt.Fprintf(os.Stderr, "⚠ Package %s not found, skipping\n", packageName)
continue
}
@@ -113,6 +161,233 @@ Multiple -f flags can be specified, and they can point to files or directories.`
},
}
// findPackagesToDelete finds all packages that need to be deleted, including dependents
func findPackagesToDelete(ctx context.Context, k8sClient client.Client, requestedPackages map[string]bool) (map[string]bool, error) {
// Get all installed Packages
var installedPackages cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &installedPackages); err != nil {
return nil, fmt.Errorf("failed to list Packages: %w", err)
}
installedMap := make(map[string]bool)
for _, pkg := range installedPackages.Items {
installedMap[pkg.Name] = true
}
// Get all PackageSources to build dependency graph
var packageSources cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &packageSources); err != nil {
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
}
// Build reverse dependency graph (dependents -> dependencies)
// This tells us: for each package, which packages depend on it
reverseDeps := make(map[string][]string)
for _, ps := range packageSources.Items {
// Only consider installed packages
if !installedMap[ps.Name] {
continue
}
for _, variant := range ps.Spec.Variants {
for _, dep := range variant.DependsOn {
// Only consider installed dependencies
if installedMap[dep] {
reverseDeps[dep] = append(reverseDeps[dep], ps.Name)
}
}
}
}
// Find all packages to delete (requested + their dependents)
packagesToDelete := make(map[string]bool)
visited := make(map[string]bool)
var findDependents func(string)
findDependents = func(pkgName string) {
if visited[pkgName] {
return
}
visited[pkgName] = true
// Only add if it's installed
if installedMap[pkgName] {
packagesToDelete[pkgName] = true
}
// Recursively find all dependents
for _, dependent := range reverseDeps[pkgName] {
if installedMap[dependent] {
findDependents(dependent)
}
}
}
// Start from requested packages
for pkgName := range requestedPackages {
if !installedMap[pkgName] {
continue
}
findDependents(pkgName)
}
return packagesToDelete, nil
}
// confirmDeletion shows the list of packages to be deleted and asks for user confirmation
func confirmDeletion(packagesToDelete map[string]bool, requestedPackages map[string]bool) error {
// Separate requested packages from dependents
var requested []string
var dependents []string
for pkg := range packagesToDelete {
if requestedPackages[pkg] {
requested = append(requested, pkg)
} else {
dependents = append(dependents, pkg)
}
}
fmt.Fprintf(os.Stderr, "\nThe following packages will be deleted:\n\n")
if len(requested) > 0 {
fmt.Fprintf(os.Stderr, "Requested packages:\n")
for _, pkg := range requested {
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
}
fmt.Fprintf(os.Stderr, "\n")
}
if len(dependents) > 0 {
fmt.Fprintf(os.Stderr, "Dependent packages (will also be deleted):\n")
for _, pkg := range dependents {
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
}
fmt.Fprintf(os.Stderr, "\n")
}
fmt.Fprintf(os.Stderr, "Total: %d package(s)\n\n", len(packagesToDelete))
fmt.Fprintf(os.Stderr, "Do you want to continue? [y/N]: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read input: %w", err)
}
input = strings.TrimSpace(strings.ToLower(input))
if input != "y" && input != "yes" {
return fmt.Errorf("deletion cancelled")
}
return nil
}
// getDeleteOrder returns packages in reverse topological order (dependents first, then dependencies)
// This ensures we delete dependents before their dependencies
func getDeleteOrder(ctx context.Context, k8sClient client.Client, packagesToDelete map[string]bool) ([]string, error) {
// Get all PackageSources to build dependency graph
var packageSources cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &packageSources); err != nil {
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
}
// Build forward dependency graph (package -> dependencies)
dependencyGraph := make(map[string][]string)
for _, ps := range packageSources.Items {
if !packagesToDelete[ps.Name] {
continue
}
deps := make(map[string]bool)
for _, variant := range ps.Spec.Variants {
for _, dep := range variant.DependsOn {
if packagesToDelete[dep] {
deps[dep] = true
}
}
}
var depList []string
for dep := range deps {
depList = append(depList, dep)
}
dependencyGraph[ps.Name] = depList
}
// Build reverse graph for topological sort
reverseGraph := make(map[string][]string)
allNodes := make(map[string]bool)
for node, deps := range dependencyGraph {
allNodes[node] = true
for _, dep := range deps {
allNodes[dep] = true
reverseGraph[dep] = append(reverseGraph[dep], node)
}
}
// Add nodes that have no dependencies
for pkg := range packagesToDelete {
if !allNodes[pkg] {
allNodes[pkg] = true
dependencyGraph[pkg] = []string{}
}
}
// Calculate in-degrees
inDegree := make(map[string]int)
for node := range allNodes {
inDegree[node] = 0
}
for node, deps := range dependencyGraph {
inDegree[node] = len(deps)
}
// Kahn's algorithm - start with nodes that have no dependencies
var queue []string
for node, degree := range inDegree {
if degree == 0 {
queue = append(queue, node)
}
}
var result []string
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
// Process dependents
for _, dependent := range reverseGraph[node] {
inDegree[dependent]--
if inDegree[dependent] == 0 {
queue = append(queue, dependent)
}
}
}
// Check for cycles: if not all nodes were processed, there's a cycle
if len(result) != len(allNodes) {
// Find unprocessed nodes
processed := make(map[string]bool)
for _, node := range result {
processed[node] = true
}
var unprocessed []string
for node := range allNodes {
if !processed[node] {
unprocessed = append(unprocessed, node)
}
}
return nil, fmt.Errorf("dependency cycle detected: the following packages form a cycle and cannot be deleted: %v", unprocessed)
}
// Reverse the result to get dependents first, then dependencies
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return result, nil
}
func init() {
rootCmd.AddCommand(delCmd)
delCmd.Flags().StringArrayVarP(&delCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")

View File

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

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
@@ -89,49 +90,52 @@ func listPackageSources(ctx context.Context, k8sClient client.Client, showCompon
return fmt.Errorf("failed to list PackageSources: %w", err)
}
// Use tabwriter for better column alignment
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
defer w.Flush()
// Print header
fmt.Fprintf(os.Stdout, "%-50s %-30s %-10s %s\n", "NAME", "VARIANTS", "READY", "STATUS")
fmt.Fprintf(os.Stdout, "%-50s %-30s %-10s %s\n", strings.Repeat("-", 50), strings.Repeat("-", 30), strings.Repeat("-", 10), strings.Repeat("-", 50))
fmt.Fprintln(w, "NAME\tVARIANTS\tREADY\tSTATUS")
// Print rows
for _, ps := range psList.Items {
// Get variants
var variants []string
for _, variant := range ps.Spec.Variants {
variants = append(variants, variant.Name)
}
variantsStr := strings.Join(variants, ",")
if len(variantsStr) > 28 {
variantsStr = variantsStr[:25] + "..."
}
// Print rows
for _, ps := range psList.Items {
// Get variants
var variants []string
for _, variant := range ps.Spec.Variants {
variants = append(variants, variant.Name)
}
variantsStr := strings.Join(variants, ",")
if len(variantsStr) > 28 {
variantsStr = variantsStr[:25] + "..."
}
// Get Ready condition
ready := "Unknown"
status := ""
for _, condition := range ps.Status.Conditions {
if condition.Type == "Ready" {
ready = string(condition.Status)
status = condition.Message
if len(status) > 48 {
status = status[:45] + "..."
}
break
// Get Ready condition
ready := "Unknown"
status := ""
for _, condition := range ps.Status.Conditions {
if condition.Type == "Ready" {
ready = string(condition.Status)
status = condition.Message
if len(status) > 48 {
status = status[:45] + "..."
}
break
}
}
fmt.Fprintf(os.Stdout, "%-50s %-30s %-10s %s\n", ps.Name, variantsStr, ready, status)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", ps.Name, variantsStr, ready, status)
// Show components if requested
if showComponents {
for _, variant := range ps.Spec.Variants {
for _, component := range variant.Components {
fmt.Fprintf(os.Stdout, " %-48s %-30s %-10s %s\n",
fmt.Sprintf("%s.%s", ps.Name, component.Name),
variant.Name, "", "")
}
// Show components if requested
if showComponents {
for _, variant := range ps.Spec.Variants {
for _, component := range variant.Components {
fmt.Fprintf(w, " %s\t%s\t\t\n",
fmt.Sprintf("%s.%s", ps.Name, component.Name),
variant.Name)
}
}
}
}
return nil
}
@@ -142,9 +146,25 @@ func listPackages(ctx context.Context, k8sClient client.Client, showComponents b
return fmt.Errorf("failed to list Packages: %w", err)
}
// Fetch all PackageSource resources once if components are requested
var psMap map[string]*cozyv1alpha1.PackageSource
if showComponents {
var psList cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &psList); err != nil {
return fmt.Errorf("failed to list PackageSources: %w", err)
}
psMap = make(map[string]*cozyv1alpha1.PackageSource)
for i := range psList.Items {
psMap[psList.Items[i].Name] = &psList.Items[i]
}
}
// Use tabwriter for better column alignment
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
defer w.Flush()
// Print header
fmt.Fprintf(os.Stdout, "%-50s %-20s %-10s %s\n", "NAME", "VARIANT", "READY", "STATUS")
fmt.Fprintf(os.Stdout, "%-50s %-20s %-10s %s\n", strings.Repeat("-", 50), strings.Repeat("-", 20), strings.Repeat("-", 10), strings.Repeat("-", 50))
fmt.Fprintln(w, "NAME\tVARIANT\tREADY\tSTATUS")
// Print rows
for _, pkg := range pkgList.Items {
@@ -167,20 +187,19 @@ func listPackages(ctx context.Context, k8sClient client.Client, showComponents b
}
}
fmt.Fprintf(os.Stdout, "%-50s %-20s %-10s %s\n", pkg.Name, variant, ready, status)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", pkg.Name, variant, ready, status)
// Show components if requested
if showComponents {
// Get PackageSource to show components
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(ctx, client.ObjectKey{Name: pkg.Name}, ps); err == nil {
// Look up PackageSource from map instead of making API call
if ps, exists := psMap[pkg.Name]; exists {
// Find the variant
for _, v := range ps.Spec.Variants {
if v.Name == variant {
for _, component := range v.Components {
fmt.Fprintf(os.Stdout, " %-48s %-20s %-10s %s\n",
fmt.Fprintf(w, " %s\t%s\t\t\n",
fmt.Sprintf("%s.%s", pkg.Name, component.Name),
variant, "", "")
variant)
}
break
}

View File

@@ -229,6 +229,14 @@ func main() {
os.Exit(1)
}
if err = (&controller.CozystackResourceDefinitionHelmReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionHelmReconciler")
os.Exit(1)
}
dashboardManager := &dashboard.Manager{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),

View File

@@ -18,10 +18,8 @@ package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net/url"
"os"
"strings"
"time"
@@ -35,7 +33,7 @@ import (
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@@ -57,18 +55,6 @@ var (
setupLog = ctrl.Log.WithName("setup")
)
// stringSliceFlag is a custom flag type that allows multiple values
type stringSliceFlag []string
func (f *stringSliceFlag) String() string {
return strings.Join(*f, ",")
}
func (f *stringSliceFlag) Set(value string) error {
*f = append(*f, value)
return nil
}
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
@@ -87,9 +73,9 @@ func main() {
var enableHTTP2 bool
var installFlux bool
var cozystackVersion string
var installFluxResources stringSliceFlag
var platformSource string
var platformSourceURL string
var platformSourceName string
var platformSourceRef string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
@@ -101,11 +87,11 @@ func main() {
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop")
flag.Var(&installFluxResources, "install-flux-resource", "Install Flux resource (JSON format). Can be specified multiple times. Applied after Flux installation.")
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
"Version of Cozystack")
flag.StringVar(&platformSource, "platform-source", "", "Platform source URL (oci:// or git://). If specified, generates OCIRepository or GitRepository resource.")
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
opts := zap.Options{
Development: true,
@@ -117,6 +103,13 @@ func main() {
config := ctrl.GetConfigOrDie()
// Create a direct client (without cache) for pre-start operations
directClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
setupLog.Error(err, "unable to create direct client")
os.Exit(1)
}
// Start the controller manager
setupLog.Info("Starting controller manager")
mgr, err := ctrl.NewManager(config, ctrl.Options{
@@ -153,37 +146,22 @@ func main() {
installCtx, installCancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer installCancel()
// The namespace will be automatically extracted from the embedded manifests
if err := fluxinstall.Install(installCtx, mgr.GetClient(), fluxinstall.WriteEmbeddedManifests); err != nil {
setupLog.Error(err, "failed to install Flux, continuing anyway")
// Don't exit - allow operator to start even if Flux install fails
// This allows the operator to work in environments where Flux is already installed
} else {
setupLog.Info("Flux installation completed successfully")
}
}
// Install Flux resources after Flux installation
if len(installFluxResources) > 0 {
setupLog.Info("Installing Flux resources", "count", len(installFluxResources))
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer installCancel()
if err := installFluxResourcesFunc(installCtx, mgr.GetClient(), installFluxResources); err != nil {
setupLog.Error(err, "failed to install Flux resources, continuing anyway")
// Don't exit - allow operator to start even if resource installation fails
} else {
setupLog.Info("Flux resources installation completed successfully")
// Use direct client for pre-start operations (cache is not ready yet)
if err := fluxinstall.Install(installCtx, directClient, fluxinstall.WriteEmbeddedManifests); err != nil {
setupLog.Error(err, "failed to install Flux")
os.Exit(1)
}
setupLog.Info("Flux installation completed successfully")
}
// Generate and install platform source resource if specified
if platformSource != "" {
setupLog.Info("Generating platform source resource", "source", platformSource, "name", platformSourceName)
if platformSourceURL != "" {
setupLog.Info("Generating platform source resource", "url", platformSourceURL, "name", platformSourceName, "ref", platformSourceRef)
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer installCancel()
if err := installPlatformSourceResource(installCtx, mgr.GetClient(), platformSource, platformSourceName); err != nil {
// Use direct client for pre-start operations (cache is not ready yet)
if err := installPlatformSourceResource(installCtx, directClient, platformSourceURL, platformSourceName, platformSourceRef); err != nil {
setupLog.Error(err, "failed to install platform source resource")
os.Exit(1)
} else {
@@ -228,120 +206,58 @@ func main() {
}
}
// installFluxResourcesFunc installs Flux resources from JSON strings
func installFluxResourcesFunc(ctx context.Context, k8sClient client.Client, resources []string) error {
logger := log.FromContext(ctx)
for i, resourceJSON := range resources {
logger.Info("Installing Flux resource", "index", i+1, "total", len(resources))
// Parse JSON into unstructured object
var obj unstructured.Unstructured
if err := json.Unmarshal([]byte(resourceJSON), &obj.Object); err != nil {
return fmt.Errorf("failed to parse resource JSON at index %d: %w", i, err)
}
// Validate that it has required fields
if obj.GetAPIVersion() == "" {
return fmt.Errorf("resource at index %d missing apiVersion", i)
}
if obj.GetKind() == "" {
return fmt.Errorf("resource at index %d missing kind", i)
}
if obj.GetName() == "" {
return fmt.Errorf("resource at index %d missing metadata.name", i)
}
// Apply the resource (create or update)
logger.Info("Applying Flux resource",
"apiVersion", obj.GetAPIVersion(),
"kind", obj.GetKind(),
"name", obj.GetName(),
"namespace", obj.GetNamespace(),
)
// Use server-side apply or create/update
existing := &unstructured.Unstructured{}
existing.SetGroupVersionKind(obj.GroupVersionKind())
key := client.ObjectKey{
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
}
err := k8sClient.Get(ctx, key, existing)
if err != nil {
if client.IgnoreNotFound(err) == nil {
// Resource doesn't exist, create it
if err := k8sClient.Create(ctx, &obj); err != nil {
return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
}
logger.Info("Created Flux resource", "kind", obj.GetKind(), "name", obj.GetName())
} else {
return fmt.Errorf("failed to check if resource exists: %w", err)
}
} else {
// Resource exists, update it
obj.SetResourceVersion(existing.GetResourceVersion())
if err := k8sClient.Update(ctx, &obj); err != nil {
return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
}
logger.Info("Updated Flux resource", "kind", obj.GetKind(), "name", obj.GetName())
}
}
return nil
}
// installPlatformSourceResource generates and installs a Flux source resource (OCIRepository or GitRepository)
// based on the platform source URL
func installPlatformSourceResource(ctx context.Context, k8sClient client.Client, sourceURL, resourceName string) error {
func installPlatformSourceResource(ctx context.Context, k8sClient client.Client, sourceURL, resourceName, refSpec string) error {
logger := log.FromContext(ctx)
// Parse the source URL to determine type
sourceType, repoURL, ref, err := parsePlatformSource(sourceURL)
sourceType, repoURL, err := parsePlatformSourceURL(sourceURL)
if err != nil {
return fmt.Errorf("failed to parse platform source URL: %w", err)
}
var obj *unstructured.Unstructured
// Parse reference specification
refMap, err := parseRefSpec(refSpec)
if err != nil {
return fmt.Errorf("failed to parse reference specification: %w", err)
}
var obj client.Object
switch sourceType {
case "oci":
obj, err = generateOCIRepository(resourceName, repoURL, ref)
obj, err = generateOCIRepository(resourceName, repoURL, refMap)
if err != nil {
return fmt.Errorf("failed to generate OCIRepository: %w", err)
}
case "git":
obj, err = generateGitRepository(resourceName, repoURL, ref)
obj, err = generateGitRepository(resourceName, repoURL, refMap)
if err != nil {
return fmt.Errorf("failed to generate GitRepository: %w", err)
}
default:
return fmt.Errorf("unsupported source type: %s (expected oci:// or git://)", sourceType)
return fmt.Errorf("unsupported source type: %s (expected oci:// or https://)", sourceType)
}
// Apply the resource (create or update)
logger.Info("Applying platform source resource",
"apiVersion", obj.GetAPIVersion(),
"kind", obj.GetKind(),
"apiVersion", obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
"kind", obj.GetObjectKind().GroupVersionKind().Kind,
"name", obj.GetName(),
"namespace", obj.GetNamespace(),
)
existing := &unstructured.Unstructured{}
existing.SetGroupVersionKind(obj.GroupVersionKind())
key := client.ObjectKey{
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
}
existing := obj.DeepCopyObject().(client.Object)
key := client.ObjectKeyFromObject(obj)
err = k8sClient.Get(ctx, key, existing)
if err != nil {
if client.IgnoreNotFound(err) == nil {
// Resource doesn't exist, create it
if err := k8sClient.Create(ctx, obj); err != nil {
return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
}
logger.Info("Created platform source resource", "kind", obj.GetKind(), "name", obj.GetName())
logger.Info("Created platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
} else {
return fmt.Errorf("failed to check if resource exists: %w", err)
}
@@ -349,132 +265,195 @@ func installPlatformSourceResource(ctx context.Context, k8sClient client.Client,
// Resource exists, update it
obj.SetResourceVersion(existing.GetResourceVersion())
if err := k8sClient.Update(ctx, obj); err != nil {
return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
}
logger.Info("Updated platform source resource", "kind", obj.GetKind(), "name", obj.GetName())
logger.Info("Updated platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
}
return nil
}
// parsePlatformSource parses the source URL and returns the type, repository URL, and reference
// parsePlatformSourceURL parses the source URL and returns the source type and repository URL.
// Supports formats:
// - oci://registry.example.com/repo@sha256:digest
// - oci://registry.example.com/repo (ref will be empty)
// - git://github.com/user/repo@branch
// - git://github.com/user/repo (ref will default to "main")
// - https://github.com/user/repo@branch (treated as git)
func parsePlatformSource(sourceURL string) (sourceType, repoURL, ref string, err error) {
// Normalize the URL by trimming whitespace
// - oci://registry.example.com/repo
// - https://github.com/user/repo
// - http://github.com/user/repo
// - ssh://git@github.com/user/repo
func parsePlatformSourceURL(sourceURL string) (sourceType, repoURL string, err error) {
sourceURL = strings.TrimSpace(sourceURL)
// Check for oci:// prefix
if strings.HasPrefix(sourceURL, "oci://") {
// Remove oci:// prefix
rest := strings.TrimPrefix(sourceURL, "oci://")
// Check for @sha256: digest (look for @ followed by sha256:)
// We need to find the last @ before sha256: to handle paths with @ symbols
sha256Idx := strings.Index(rest, "@sha256:")
if sha256Idx != -1 {
repoURL = "oci://" + rest[:sha256Idx]
ref = rest[sha256Idx+1:] // sha256:digest
} else {
// Check for @ without sha256: (might be a tag)
if atIdx := strings.LastIndex(rest, "@"); atIdx != -1 {
// Could be a tag, but for OCI we expect sha256: digest
// For now, treat everything after @ as the ref
repoURL = "oci://" + rest[:atIdx]
ref = rest[atIdx+1:]
} else {
repoURL = "oci://" + rest
ref = "" // No digest specified
}
}
return "oci", repoURL, ref, nil
return "oci", sourceURL, nil
}
// Check for git:// prefix or treat as git for http/https
if strings.HasPrefix(sourceURL, "git://") || strings.HasPrefix(sourceURL, "http://") || strings.HasPrefix(sourceURL, "https://") || strings.HasPrefix(sourceURL, "ssh://") {
// Parse URL to extract ref if present
parsedURL, err := url.Parse(sourceURL)
if err != nil {
return "", "", "", fmt.Errorf("invalid URL: %w", err)
}
// Check for @ref in the path (e.g., git://host/path@branch)
path := parsedURL.Path
if idx := strings.LastIndex(path, "@"); idx != -1 {
repoURL = fmt.Sprintf("%s://%s%s", parsedURL.Scheme, parsedURL.Host, path[:idx])
if parsedURL.RawQuery != "" {
repoURL += "?" + parsedURL.RawQuery
}
ref = path[idx+1:]
} else {
// Default to main branch if no ref specified
repoURL = sourceURL
ref = "main"
}
// Normalize git:// to https:// for GitRepository
if strings.HasPrefix(repoURL, "git://") {
repoURL = strings.Replace(repoURL, "git://", "https://", 1)
}
return "git", repoURL, ref, nil
if strings.HasPrefix(sourceURL, "https://") || strings.HasPrefix(sourceURL, "http://") || strings.HasPrefix(sourceURL, "ssh://") {
return "git", sourceURL, nil
}
return "", "", "", fmt.Errorf("unsupported source URL scheme (expected oci:// or git://): %s", sourceURL)
return "", "", fmt.Errorf("unsupported source URL scheme (expected oci://, https://, http://, or ssh://): %s", sourceURL)
}
// parseRefSpec parses a reference specification string in the format "key1=value1,key2=value2".
// Returns a map of key-value pairs.
func parseRefSpec(refSpec string) (map[string]string, error) {
result := make(map[string]string)
refSpec = strings.TrimSpace(refSpec)
if refSpec == "" {
return result, nil
}
pairs := strings.Split(refSpec, ",")
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
// Split on first '=' only to allow '=' in values (e.g., digest=sha256:...)
idx := strings.Index(pair, "=")
if idx == -1 {
return nil, fmt.Errorf("invalid reference specification format: %q (expected key=value)", pair)
}
key := strings.TrimSpace(pair[:idx])
value := strings.TrimSpace(pair[idx+1:])
if key == "" {
return nil, fmt.Errorf("empty key in reference specification: %q", pair)
}
if value == "" {
return nil, fmt.Errorf("empty value for key %q in reference specification", key)
}
result[key] = value
}
return result, nil
}
// Valid reference keys for OCI repositories
var validOCIRefKeys = map[string]bool{
"digest": true,
"semver": true,
"semverFilter": true,
"tag": true,
}
// Valid reference keys for Git repositories
var validGitRefKeys = map[string]bool{
"branch": true,
"tag": true,
"semver": true,
"name": true,
"commit": true,
}
// validateOCIRef validates reference keys for OCI repositories
func validateOCIRef(refMap map[string]string) error {
for key := range refMap {
if !validOCIRefKeys[key] {
return fmt.Errorf("invalid OCI reference key %q (valid keys: digest, semver, semverFilter, tag)", key)
}
}
// Validate digest format if provided
if digest, ok := refMap["digest"]; ok {
if !strings.HasPrefix(digest, "sha256:") {
return fmt.Errorf("digest must be in format 'sha256:<hash>', got: %s", digest)
}
}
return nil
}
// validateGitRef validates reference keys for Git repositories
func validateGitRef(refMap map[string]string) error {
for key := range refMap {
if !validGitRefKeys[key] {
return fmt.Errorf("invalid Git reference key %q (valid keys: branch, tag, semver, name, commit)", key)
}
}
// Validate commit format if provided (should be a hex string)
if commit, ok := refMap["commit"]; ok {
if len(commit) < 7 {
return fmt.Errorf("commit SHA should be at least 7 characters, got: %s", commit)
}
for _, c := range commit {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return fmt.Errorf("commit SHA should be a hexadecimal string, got: %s", commit)
}
}
}
return nil
}
// generateOCIRepository creates an OCIRepository resource
func generateOCIRepository(name, repoURL, digest string) (*unstructured.Unstructured, error) {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion("source.toolkit.fluxcd.io/v1")
obj.SetKind("OCIRepository")
obj.SetName(name)
obj.SetNamespace("cozy-system")
spec := map[string]interface{}{
"interval": "5m0s",
"url": repoURL,
func generateOCIRepository(name, repoURL string, refMap map[string]string) (*sourcev1.OCIRepository, error) {
if err := validateOCIRef(refMap); err != nil {
return nil, err
}
if digest != "" {
// Ensure digest starts with sha256:
if !strings.HasPrefix(digest, "sha256:") {
digest = "sha256:" + digest
}
spec["ref"] = map[string]interface{}{
"digest": digest,
}
obj := &sourcev1.OCIRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1.GroupVersion.String(),
Kind: sourcev1.OCIRepositoryKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "cozy-system",
},
Spec: sourcev1.OCIRepositorySpec{
URL: repoURL,
Interval: metav1.Duration{Duration: 5 * time.Minute},
},
}
if err := unstructured.SetNestedField(obj.Object, spec, "spec"); err != nil {
return nil, fmt.Errorf("failed to set spec: %w", err)
// Set reference if any ref options are provided
if len(refMap) > 0 {
obj.Spec.Reference = &sourcev1.OCIRepositoryRef{
Digest: refMap["digest"],
SemVer: refMap["semver"],
SemverFilter: refMap["semverFilter"],
Tag: refMap["tag"],
}
}
return obj, nil
}
// generateGitRepository creates a GitRepository resource
func generateGitRepository(name, repoURL, ref string) (*unstructured.Unstructured, error) {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion("source.toolkit.fluxcd.io/v1")
obj.SetKind("GitRepository")
obj.SetName(name)
obj.SetNamespace("cozy-system")
func generateGitRepository(name, repoURL string, refMap map[string]string) (*sourcev1.GitRepository, error) {
if err := validateGitRef(refMap); err != nil {
return nil, err
}
spec := map[string]interface{}{
"interval": "5m0s",
"url": repoURL,
"ref": map[string]interface{}{
"branch": ref,
obj := &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1.GroupVersion.String(),
Kind: sourcev1.GitRepositoryKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "cozy-system",
},
Spec: sourcev1.GitRepositorySpec{
URL: repoURL,
Interval: metav1.Duration{Duration: 5 * time.Minute},
},
}
if err := unstructured.SetNestedField(obj.Object, spec, "spec"); err != nil {
return nil, fmt.Errorf("failed to set spec: %w", err)
// Set reference if any ref options are provided
if len(refMap) > 0 {
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
Branch: refMap["branch"],
Tag: refMap["tag"],
SemVer: refMap["semver"],
Name: refMap["name"],
Commit: refMap["commit"],
}
}
return obj, nil

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.39.1
-->
## Features and Improvements
* **[monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant monitoring**: Introduced the SLACK_SEVERITY_FILTER environment variable in the Alerta deployment to enable filtering of alert severities for Slack notifications based on the disabledSeverity configuration. Additionally, added a VMAgent resource template for scraping metrics within tenant namespaces, improving monitoring granularity and control. This enhancement allows administrators to configure which alert severities are sent to Slack and enables tenant-specific metrics collection for better observability ([**@IvanHunters**](https://github.com/IvanHunters) in #1712).
---
**Full Changelog**: [v0.39.0...v0.39.1](https://github.com/cozystack/cozystack/compare/v0.39.0...v0.39.1)

12
go.mod
View File

@@ -7,8 +7,8 @@ go 1.25.0
require (
github.com/emicklei/dot v1.10.0
github.com/fluxcd/helm-controller/api v1.4.3
github.com/fluxcd/source-controller/api v1.6.2
github.com/fluxcd/source-watcher/api/v2 v2.0.2
github.com/fluxcd/source-controller/api v1.7.4
github.com/fluxcd/source-watcher/api/v2 v2.0.3
github.com/go-logr/logr v1.4.3
github.com/go-logr/zapr v1.3.0
github.com/google/gofuzz v1.2.0
@@ -19,17 +19,16 @@ require (
github.com/spf13/cobra v1.9.1
go.uber.org/zap v1.27.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/apimachinery v0.34.2
k8s.io/apiserver v0.34.1
k8s.io/client-go v0.34.1
k8s.io/component-base v0.34.1
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
sigs.k8s.io/controller-runtime v0.22.2
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
)
@@ -50,7 +49,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
github.com/fluxcd/pkg/apis/kustomize v1.13.0 // indirect
github.com/fluxcd/pkg/apis/meta v1.22.0 // indirect
github.com/fluxcd/pkg/apis/meta v1.23.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -121,6 +120,7 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/kms v0.34.1 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect

16
go.sum
View File

@@ -43,12 +43,12 @@ github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2T
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
github.com/fluxcd/pkg/apis/kustomize v1.13.0 h1:GGf0UBVRIku+gebY944icVeEIhyg1P/KE3IrhOyJJnE=
github.com/fluxcd/pkg/apis/kustomize v1.13.0/go.mod h1:TLKVqbtnzkhDuhWnAsN35977HvRfIjs+lgMuNro/LEc=
github.com/fluxcd/pkg/apis/meta v1.22.0 h1:EHWQH5ZWml7i8eZ/AMjm1jxid3j/PQ31p+hIwCt6crM=
github.com/fluxcd/pkg/apis/meta v1.22.0/go.mod h1:Kc1+bWe5p0doROzuV9XiTfV/oL3ddsemYXt8ZYWdVVg=
github.com/fluxcd/source-controller/api v1.6.2 h1:UmodAeqLIeF29HdTqf2GiacZyO+hJydJlepDaYsMvhc=
github.com/fluxcd/source-controller/api v1.6.2/go.mod h1:ZJcAi0nemsnBxjVgmJl0WQzNvB0rMETxQMTdoFosmMw=
github.com/fluxcd/source-watcher/api/v2 v2.0.2 h1:fWSxsDqYN7My2AEpQwbP7O6Qjix8nGBX+UE/qWHtZfM=
github.com/fluxcd/source-watcher/api/v2 v2.0.2/go.mod h1:Hs6ueayPt23jlkIr/d1pGPZ+OHiibQwWjxvU6xqljzg=
github.com/fluxcd/pkg/apis/meta v1.23.0 h1:fLis5YcHnOsyKYptzBtituBm5EWNx13I0bXQsy0FG4s=
github.com/fluxcd/pkg/apis/meta v1.23.0/go.mod h1:UWsIbBPCxYvoVklr2mV2uLFBf/n17dNAmKFjRfApdDo=
github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs=
github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY=
github.com/fluxcd/source-watcher/api/v2 v2.0.3 h1:SsVGAaMBxzvcgrOz/Kl6c2ybMHVqoiEFwtI+bDuSeSs=
github.com/fluxcd/source-watcher/api/v2 v2.0.3/go.mod h1:Nx3QZweVyuhaOtSNrw+oxifG+qrakPvjgNAN9qlUTb0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
@@ -320,8 +320,8 @@ k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPG
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4=
sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=

View File

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

View File

@@ -24,9 +24,8 @@ 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/crds
BACKUPSTRATEGY_CRDDIR=packages/system/backupstrategy-controller/definitions
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/crds
OPERATOR_CRDDIR=packages/core/installer/definitions
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions
COZY_RD_CRDDIR=packages/system/cozystack-resource-definition-crd/definition
BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions
BACKUPSTRATEGY_CRDDIR=packages/system/backupstrategy-controller/definitions

View File

@@ -0,0 +1,53 @@
package backupcontroller
import (
"context"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
)
// BackupVeleroStrategyReconciler reconciles BackupJob with a strategy referencing
// Velero.strategy.backups.cozystack.io objects.
type BackupJobReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *BackupJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
j := &backupsv1alpha1.BackupJob{}
err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, j)
if err != nil {
if apierrors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
if j.Spec.StrategyRef.APIGroup == nil || *j.Spec.StrategyRef.APIGroup != strategyv1alpha1.GroupVersion.Group {
return ctrl.Result{}, nil
}
switch j.Spec.StrategyRef.Kind {
case strategyv1alpha1.JobStrategyKind:
return r.reconcileJob(ctx, j)
case strategyv1alpha1.VeleroStrategyKind:
return r.reconcileVelero(ctx, j)
default:
return ctrl.Result{}, nil
}
}
// SetupWithManager registers our controller with the Manager and sets up watches.
func (r *BackupJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&backupsv1alpha1.BackupJob{}).
Complete(r)
}

View File

@@ -3,29 +3,13 @@ package backupcontroller
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
)
// BackupJobStrategyReconciler reconciles BackupJob with a strategy referencing
// Job.strategy.backups.cozystack.io objects.
type BackupJobStrategyReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *BackupJobStrategyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
func (r *BackupJobReconciler) reconcileJob(ctx context.Context, j *backupsv1alpha1.BackupJob) (ctrl.Result, error) {
_ = log.FromContext(ctx)
return ctrl.Result{}, nil
}
// SetupWithManager registers our controller with the Manager and sets up watches.
func (r *BackupJobStrategyReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&backupsv1alpha1.BackupJob{}).
Complete(r)
}

View File

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

View File

@@ -5,13 +5,11 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"slices"
"sync"
"time"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
@@ -39,11 +37,8 @@ type CozystackResourceDefinitionReconciler struct {
}
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
if err := r.reconcileCozyRDAndUpdateHelmReleases(ctx); err != nil {
return ctrl.Result{}, err
}
// Continue with debounced restart logic
// Only handle debounced restart logic
// HelmRelease reconciliation is handled by CozystackResourceDefinitionHelmReconciler
return r.debouncedRestart(ctx)
}
@@ -192,138 +187,3 @@ func sortCozyRDs(a, b cozyv1alpha1.CozystackResourceDefinition) int {
}
return 1
}
// reconcileCozyRDAndUpdateHelmReleases reconciles all CozystackResourceDefinitions and updates HelmReleases from them
func (r *CozystackResourceDefinitionReconciler) reconcileCozyRDAndUpdateHelmReleases(ctx context.Context) error {
logger := log.FromContext(ctx)
// List all CozystackResourceDefinitions
crdList := &cozyv1alpha1.CozystackResourceDefinitionList{}
if err := r.List(ctx, crdList); err != nil {
logger.Error(err, "failed to list CozystackResourceDefinitions")
return err
}
// Update HelmReleases for each CRD
for i := range crdList.Items {
crd := &crdList.Items[i]
if err := r.updateHelmReleasesForCRD(ctx, crd); err != nil {
logger.Error(err, "failed to update HelmReleases for CRD", "crd", crd.Name)
// Continue with other CRDs even if one fails
}
}
return nil
}
// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from CozystackResourceDefinition
func (r *CozystackResourceDefinitionReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
logger := log.FromContext(ctx)
// Use application labels to find HelmReleases
// Labels: apps.cozystack.io/application.kind and apps.cozystack.io/application.group
applicationKind := crd.Spec.Application.Kind
// Validate that applicationKind is non-empty
if applicationKind == "" {
logger.Error(fmt.Errorf("Application.Kind is empty"), "Skipping HelmRelease update: invalid CozystackResourceDefinition", "crd", crd.Name)
return nil
}
applicationGroup := "apps.cozystack.io" // All applications use this group
// Build label selector for HelmReleases
// Only reconcile HelmReleases with cozystack.io/ui=true label
labelSelector := client.MatchingLabels{
"apps.cozystack.io/application.kind": applicationKind,
"apps.cozystack.io/application.group": applicationGroup,
"cozystack.io/ui": "true",
}
// List all HelmReleases with matching labels
hrList := &helmv2.HelmReleaseList{}
if err := r.List(ctx, hrList, labelSelector); err != nil {
logger.Error(err, "failed to list HelmReleases", "kind", applicationKind, "group", applicationGroup)
return err
}
logger.V(4).Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items))
// Update each HelmRelease
for i := range hrList.Items {
hr := &hrList.Items[i]
if err := r.updateHelmReleaseChart(ctx, hr, crd); err != nil {
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
continue
}
}
return nil
}
// updateHelmReleaseChart updates the chart in HelmRelease based on CozystackResourceDefinition
func (r *CozystackResourceDefinitionReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error {
logger := log.FromContext(ctx)
hrCopy := hr.DeepCopy()
updated := false
// Validate Chart configuration exists
if crd.Spec.Release.Chart.Name == "" {
logger.V(4).Info("Skipping HelmRelease chart update: Chart.Name is empty", "crd", crd.Name)
return nil
}
// Validate SourceRef fields
if crd.Spec.Release.Chart.SourceRef.Kind == "" ||
crd.Spec.Release.Chart.SourceRef.Name == "" ||
crd.Spec.Release.Chart.SourceRef.Namespace == "" {
logger.Error(fmt.Errorf("invalid SourceRef in CRD"), "Skipping HelmRelease chart update: SourceRef fields are incomplete",
"crd", crd.Name,
"kind", crd.Spec.Release.Chart.SourceRef.Kind,
"name", crd.Spec.Release.Chart.SourceRef.Name,
"namespace", crd.Spec.Release.Chart.SourceRef.Namespace)
return nil
}
// Get version and reconcileStrategy from CRD or use defaults
version := ">= 0.0.0-0"
reconcileStrategy := "Revision"
// TODO: Add Version and ReconcileStrategy fields to CozystackResourceDefinitionChart if needed
// Build expected SourceRef
expectedSourceRef := helmv2.CrossNamespaceObjectReference{
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
Name: crd.Spec.Release.Chart.SourceRef.Name,
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
}
if hrCopy.Spec.Chart == nil {
// Need to create Chart spec
hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{
Spec: helmv2.HelmChartTemplateSpec{
Chart: crd.Spec.Release.Chart.Name,
Version: version,
ReconcileStrategy: reconcileStrategy,
SourceRef: expectedSourceRef,
},
}
updated = true
} else {
// Update existing Chart spec
if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name ||
hrCopy.Spec.Chart.Spec.SourceRef != expectedSourceRef {
hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name
hrCopy.Spec.Chart.Spec.SourceRef = expectedSourceRef
updated = true
}
}
if updated {
logger.V(4).Info("Updating HelmRelease chart", "name", hr.Name, "namespace", hr.Namespace)
if err := r.Update(ctx, hrCopy); err != nil {
return fmt.Errorf("failed to update HelmRelease: %w", err)
}
}
return nil
}

View File

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

View File

@@ -28,8 +28,6 @@ import (
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -160,7 +158,6 @@ func readYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) {
// applyManifests applies Kubernetes objects using server-side apply.
func applyManifests(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error {
logger := log.FromContext(ctx)
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
// Separate CRDs and namespaces from other resources
var stageOne []*unstructured.Unstructured // CRDs and Namespaces
@@ -177,7 +174,7 @@ func applyManifests(ctx context.Context, k8sClient client.Client, objects []*uns
// Apply stage one (CRDs and Namespaces) first
if len(stageOne) > 0 {
logger.Info("Applying cluster definitions", "count", len(stageOne))
if err := applyObjects(ctx, k8sClient, decoder, stageOne); err != nil {
if err := applyObjects(ctx, k8sClient, stageOne); err != nil {
return fmt.Errorf("failed to apply cluster definitions: %w", err)
}
@@ -188,7 +185,7 @@ func applyManifests(ctx context.Context, k8sClient client.Client, objects []*uns
// Apply stage two (everything else)
if len(stageTwo) > 0 {
logger.Info("Applying resources", "count", len(stageTwo))
if err := applyObjects(ctx, k8sClient, decoder, stageTwo); err != nil {
if err := applyObjects(ctx, k8sClient, stageTwo); err != nil {
return fmt.Errorf("failed to apply resources: %w", err)
}
}
@@ -197,7 +194,7 @@ func applyManifests(ctx context.Context, k8sClient client.Client, objects []*uns
}
// applyObjects applies a list of objects using server-side apply.
func applyObjects(ctx context.Context, k8sClient client.Client, decoder runtime.Decoder, objects []*unstructured.Unstructured) error {
func applyObjects(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error {
for _, obj := range objects {
// Use server-side apply with force ownership and field manager
// FieldManager is required for apply patch operations
@@ -237,6 +234,7 @@ func isClusterDefinition(obj *unstructured.Unstructured) bool {
// injectKubernetesServiceEnv injects KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT
// environment variables into all containers of Deployment, StatefulSet, and DaemonSet objects
// if these variables are set in the operator's environment.
// Errors are logged but do not stop processing of other objects.
func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
kubernetesHost := os.Getenv("KUBERNETES_SERVICE_HOST")
kubernetesPort := os.Getenv("KUBERNETES_SERVICE_PORT")
@@ -246,6 +244,7 @@ func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
return nil
}
var firstErr error
for _, obj := range objects {
kind := obj.GetKind()
if kind != "Deployment" && kind != "StatefulSet" && kind != "DaemonSet" {
@@ -254,35 +253,62 @@ func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
// Navigate to spec.template.spec.containers
spec, found, err := unstructured.NestedMap(obj.Object, "spec", "template", "spec")
if !found || err != nil {
if !found {
continue
}
if err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to get spec for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
// Update containers
containers, found, err := unstructured.NestedSlice(spec, "containers")
if found && err == nil {
if err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to get containers for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
if found {
containers = updateContainersEnv(containers, kubernetesHost, kubernetesPort)
if err := unstructured.SetNestedSlice(spec, containers, "containers"); err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to set containers for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
}
// Update initContainers
initContainers, found, err := unstructured.NestedSlice(spec, "initContainers")
if found && err == nil {
if err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to get initContainers for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
if found {
initContainers = updateContainersEnv(initContainers, kubernetesHost, kubernetesPort)
if err := unstructured.SetNestedSlice(spec, initContainers, "initContainers"); err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to set initContainers for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
}
// Update spec in the object
if err := unstructured.SetNestedMap(obj.Object, spec, "spec", "template", "spec"); err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("failed to update spec for %s/%s: %w", kind, obj.GetName(), err)
}
continue
}
}
return nil
return firstErr
}
// updateContainersEnv updates environment variables for a slice of containers.

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -113,15 +114,21 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
return ctrl.Result{}, err
}
// Update dependencies status
if err := r.updateDependenciesStatus(ctx, pkg, variant); err != nil {
logger.Error(err, "failed to update dependencies status")
// Don't return error, continue with reconciliation
}
// Validate variant dependencies before creating HelmReleases
// If dependencies are missing, we don't create new HelmReleases but don't delete existing ones
if err := r.validateVariantDependencies(ctx, pkg, variant); err != nil {
logger.Info("variant dependencies not ready, skipping HelmRelease creation", "package", pkg.Name, "error", err)
// Check if all dependencies are ready based on status
if !r.areDependenciesReady(pkg, variant) {
logger.Info("variant dependencies not ready, skipping HelmRelease creation", "package", pkg.Name)
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "DependenciesNotReady",
Message: fmt.Sprintf("Variant dependencies not ready: %v", err),
Message: "One or more dependencies are not ready",
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
@@ -152,10 +159,20 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
strings.ReplaceAll(variantName, ".", "-"),
strings.ReplaceAll(component.Name, ".", "-"))
// Determine namespace (from Install or default to cozy-system)
// Namespace must be set
namespace := component.Install.Namespace
if namespace == "" {
namespace = "cozy-system"
logger.Error(fmt.Errorf("component %s has empty namespace in Install section", component.Name), "namespace validation failed")
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "InvalidConfiguration",
Message: fmt.Sprintf("Component %s has empty namespace in Install section", component.Name),
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, fmt.Errorf("component %s has empty namespace in Install section", component.Name)
}
// Determine release name (from Install or use component name)
@@ -199,10 +216,24 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}
// Set ownerReference
gvk, err := apiutil.GVKForObject(pkg, r.Scheme)
if err != nil {
logger.Error(err, "failed to get GVK for Package")
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "InternalError",
Message: fmt.Sprintf("Failed to get GVK for Package: %v", err),
})
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, fmt.Errorf("failed to get GVK for Package: %w", err)
}
hr.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: pkg.APIVersion,
Kind: pkg.Kind,
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: pkg.Name,
UID: pkg.UID,
Controller: func() *bool { b := true; return &b }(),
@@ -227,12 +258,21 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
if err := r.Status().Update(ctx, pkg); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, err
// Return nil to stop reconciliation, error is recorded in status
return ctrl.Result{}, nil
}
if len(dependsOn) > 0 {
hr.Spec.DependsOn = dependsOn
}
// Set valuesFiles annotation
if len(component.ValuesFiles) > 0 {
if hr.Annotations == nil {
hr.Annotations = make(map[string]string)
}
hr.Annotations["cozyhr.cozystack.io/values-files"] = strings.Join(component.ValuesFiles, ",")
}
if err := r.createOrUpdateHelmRelease(ctx, hr); err != nil {
logger.Error(err, "failed to reconcile HelmRelease", "name", releaseName, "namespace", namespace)
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
@@ -272,12 +312,16 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
logger.Info("reconciled Package", "name", pkg.Name, "helmReleaseCount", helmReleaseCount)
// Trigger reconcile for Packages that depend on this Package
if err := r.triggerDependentPackages(ctx, pkg.Name); err != nil {
logger.Error(err, "failed to trigger dependent Packages", "package", pkg.Name)
// Update dependencies status for Packages that depend on this Package
// This ensures they get re-enqueued when their dependency becomes ready
if err := r.updateDependentPackagesDependencies(ctx, pkg.Name); err != nil {
logger.V(1).Error(err, "failed to update dependent packages dependencies", "package", pkg.Name)
// Don't return error, this is best-effort
}
// Dependent Packages will be automatically enqueued by the watch handler
// when this Package's status is updated (see SetupWithManager watch handler)
return ctrl.Result{}, nil
}
@@ -332,6 +376,47 @@ func (r *PackageReconciler) createOrUpdateHelmRelease(ctx context.Context, hr *h
return r.Update(ctx, existing)
}
// getVariantForPackage retrieves the Variant for a given Package
// Returns the Variant and an error if not found
// If c is nil, uses the reconciler's client
func (r *PackageReconciler) getVariantForPackage(ctx context.Context, pkg *cozyv1alpha1.Package, c client.Client) (*cozyv1alpha1.Variant, error) {
// Use provided client or fall back to reconciler's client
cl := c
if cl == nil {
cl = r.Client
}
// Determine variant name (default to "default" if not specified)
variantName := pkg.Spec.Variant
if variantName == "" {
variantName = "default"
}
// Get the PackageSource
packageSource := &cozyv1alpha1.PackageSource{}
if err := cl.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
if apierrors.IsNotFound(err) {
return nil, fmt.Errorf("PackageSource %s not found", pkg.Name)
}
return nil, fmt.Errorf("failed to get PackageSource %s: %w", pkg.Name, err)
}
// Find the variant in PackageSource
var variant *cozyv1alpha1.Variant
for i := range packageSource.Spec.Variants {
if packageSource.Spec.Variants[i].Name == variantName {
variant = &packageSource.Spec.Variants[i]
break
}
}
if variant == nil {
return nil, fmt.Errorf("variant %s not found in PackageSource %s", variantName, pkg.Name)
}
return variant, nil
}
// buildDependsOn builds DependsOn list for a component
// Includes:
// 1. Dependencies from component.Install.DependsOn (with namespace from referenced component)
@@ -351,7 +436,7 @@ func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha
}
compNamespace := comp.Install.Namespace
if compNamespace == "" {
compNamespace = "cozy-system"
return nil, fmt.Errorf("component %s has empty namespace in Install section", comp.Name)
}
compReleaseName := comp.Install.ReleaseName
if compReleaseName == "" {
@@ -407,31 +492,9 @@ func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha
}
// Get the variant from dependent Package
depVariantName := depPackage.Spec.Variant
if depVariantName == "" {
depVariantName = "default"
}
// Get the PackageSource
depPackageSource := &cozyv1alpha1.PackageSource{}
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackageSource); err != nil {
if apierrors.IsNotFound(err) {
return nil, fmt.Errorf("dependent PackageSource %s not found", depPackageName)
}
return nil, fmt.Errorf("failed to get dependent PackageSource %s: %w", depPackageName, err)
}
// Find the variant in PackageSource
var depVariant *cozyv1alpha1.Variant
for i := range depPackageSource.Spec.Variants {
if depPackageSource.Spec.Variants[i].Name == depVariantName {
depVariant = &depPackageSource.Spec.Variants[i]
break
}
}
if depVariant == nil {
return nil, fmt.Errorf("dependent variant %s not found in PackageSource %s", depVariantName, depPackageName)
depVariant, err := r.getVariantForPackage(ctx, depPackage, nil)
if err != nil {
return nil, fmt.Errorf("failed to get variant for dependent Package %s: %w", depPackageName, err)
}
// Add all components with Install from dependent variant
@@ -449,7 +512,7 @@ func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha
depCompNamespace := depComp.Install.Namespace
if depCompNamespace == "" {
depCompNamespace = "cozy-system"
return nil, fmt.Errorf("component %s in dependent Package %s has empty namespace in Install section", depComp.Name, depPackageName)
}
depCompReleaseName := depComp.Install.ReleaseName
if depCompReleaseName == "" {
@@ -468,13 +531,86 @@ func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha
return dependsOn, nil
}
// validateVariantDependencies validates that all variant dependencies exist
// Returns error if any dependency is missing
func (r *PackageReconciler) validateVariantDependencies(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
// updateDependenciesStatus updates the dependencies status in Package status
// It checks the readiness of each dependency and updates pkg.Status.Dependencies
// Old dependency keys that are no longer in the dependency list are removed
func (r *PackageReconciler) updateDependenciesStatus(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
logger := log.FromContext(ctx)
// Initialize dependencies map if nil
if pkg.Status.Dependencies == nil {
pkg.Status.Dependencies = make(map[string]cozyv1alpha1.DependencyStatus)
}
// Build set of current dependencies (excluding ignored ones)
currentDeps := make(map[string]bool)
if len(variant.DependsOn) > 0 {
for _, depPackageName := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == depPackageName {
ignore = true
break
}
}
if ignore {
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
continue
}
currentDeps[depPackageName] = true
}
}
// Remove old dependencies that are no longer in the list
for depName := range pkg.Status.Dependencies {
if !currentDeps[depName] {
delete(pkg.Status.Dependencies, depName)
logger.V(1).Info("removed old dependency from status", "package", pkg.Name, "dependency", depName)
}
}
// Update status for each current dependency
for depPackageName := range currentDeps {
// Get the Package
depPackage := &cozyv1alpha1.Package{}
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
if apierrors.IsNotFound(err) {
// Dependency not found, mark as not ready
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
Ready: false,
}
logger.V(1).Info("dependency not found, marking as not ready", "package", pkg.Name, "dependency", depPackageName)
continue
}
// Error getting dependency, keep existing status or mark as not ready
if _, exists := pkg.Status.Dependencies[depPackageName]; !exists {
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
Ready: false,
}
}
logger.V(1).Error(err, "failed to get dependency, keeping existing status", "package", pkg.Name, "dependency", depPackageName)
continue
}
// Check Ready condition
readyCondition := meta.FindStatusCondition(depPackage.Status.Conditions, "Ready")
isReady := readyCondition != nil && readyCondition.Status == metav1.ConditionTrue
// Update dependency status
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
Ready: isReady,
}
logger.V(1).Info("updated dependency status", "package", pkg.Name, "dependency", depPackageName, "ready", isReady)
}
return nil
}
// areDependenciesReady checks if all dependencies are ready based on status
func (r *PackageReconciler) areDependenciesReady(pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) bool {
if len(variant.DependsOn) == 0 {
return nil
return true
}
for _, depPackageName := range variant.DependsOn {
@@ -487,45 +623,92 @@ func (r *PackageReconciler) validateVariantDependencies(ctx context.Context, pkg
}
}
if ignore {
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
continue
}
// Get the Package
depPackage := &cozyv1alpha1.Package{}
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
if apierrors.IsNotFound(err) {
return fmt.Errorf("dependent Package %s not found", depPackageName)
// Check dependency status
depStatus, exists := pkg.Status.Dependencies[depPackageName]
if !exists || !depStatus.Ready {
return false
}
}
return true
}
// updateDependentPackagesDependencies updates dependencies status for all Packages that depend on the given Package
// This ensures dependent packages get re-enqueued when their dependency status changes
func (r *PackageReconciler) updateDependentPackagesDependencies(ctx context.Context, packageName string) error {
logger := log.FromContext(ctx)
// Get all Packages
packageList := &cozyv1alpha1.PackageList{}
if err := r.List(ctx, packageList); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
// Get the updated Package to check its readiness
updatedPkg := &cozyv1alpha1.Package{}
if err := r.Get(ctx, types.NamespacedName{Name: packageName}, updatedPkg); err != nil {
if apierrors.IsNotFound(err) {
return nil // Package not found, nothing to update
}
return fmt.Errorf("failed to get Package %s: %w", packageName, err)
}
// Check Ready condition of the updated Package
readyCondition := meta.FindStatusCondition(updatedPkg.Status.Conditions, "Ready")
isReady := readyCondition != nil && readyCondition.Status == metav1.ConditionTrue
// For each Package, check if it depends on the given Package
for _, pkg := range packageList.Items {
// Skip the Package itself
if pkg.Name == packageName {
continue
}
// Get variant
variant, err := r.getVariantForPackage(ctx, &pkg, nil)
if err != nil {
// Continue if PackageSource or variant not found (best-effort operation)
logger.V(1).Info("skipping package, failed to get variant", "package", pkg.Name, "error", err)
continue
}
// Check if this Package depends on the given Package
dependsOn := false
for _, dep := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == dep {
ignore = true
break
}
}
return fmt.Errorf("failed to get dependent Package %s: %w", depPackageName, err)
}
// Get the PackageSource
depPackageSource := &cozyv1alpha1.PackageSource{}
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackageSource); err != nil {
if apierrors.IsNotFound(err) {
return fmt.Errorf("dependent PackageSource %s not found", depPackageName)
if ignore {
continue
}
return fmt.Errorf("failed to get dependent PackageSource %s: %w", depPackageName, err)
}
// Get the variant from dependent Package
depVariantName := depPackage.Spec.Variant
if depVariantName == "" {
depVariantName = "default"
}
// Find the variant in PackageSource
var depVariant *cozyv1alpha1.Variant
for i := range depPackageSource.Spec.Variants {
if depPackageSource.Spec.Variants[i].Name == depVariantName {
depVariant = &depPackageSource.Spec.Variants[i]
if dep == packageName {
dependsOn = true
break
}
}
if depVariant == nil {
return fmt.Errorf("dependent variant %s not found in PackageSource %s", depVariantName, depPackageName)
if dependsOn {
// Update the dependency status in this Package
if pkg.Status.Dependencies == nil {
pkg.Status.Dependencies = make(map[string]cozyv1alpha1.DependencyStatus)
}
pkg.Status.Dependencies[packageName] = cozyv1alpha1.DependencyStatus{
Ready: isReady,
}
if err := r.Status().Update(ctx, &pkg); err != nil {
logger.V(1).Error(err, "failed to update dependency status for dependent Package", "package", pkg.Name, "dependency", packageName)
continue
}
logger.V(1).Info("updated dependency status for dependent Package", "package", pkg.Name, "dependency", packageName, "ready", isReady)
}
}
@@ -609,46 +792,16 @@ func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1
return nil
}
// createOrUpdateNamespace creates or updates a namespace
// createOrUpdateNamespace creates or updates a namespace using server-side apply
func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace) error {
existing := &corev1.Namespace{}
key := types.NamespacedName{Name: namespace.Name}
err := r.Get(ctx, key, existing)
if apierrors.IsNotFound(err) {
return r.Create(ctx, namespace)
} else if err != nil {
return err
}
// Preserve resource version
namespace.SetResourceVersion(existing.GetResourceVersion())
// Merge labels
labels := namespace.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
for k, v := range existing.GetLabels() {
if _, ok := labels[k]; !ok {
labels[k] = v
}
}
namespace.SetLabels(labels)
// Merge annotations
annotations := namespace.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
for k, v := range existing.GetAnnotations() {
if _, ok := annotations[k]; !ok {
annotations[k] = v
}
}
namespace.SetAnnotations(annotations)
return r.Update(ctx, namespace)
// Ensure TypeMeta is set for server-side apply
namespace.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Namespace"))
// Use server-side apply with field manager
// This is atomic and avoids race conditions from Get/Create/Update pattern
// Labels and annotations will be merged automatically by the server
// Each label/annotation key is treated as a separate field, so existing ones are preserved
return r.Patch(ctx, namespace, client.Apply, client.FieldOwner("cozystack-package-controller"))
}
// cleanupOrphanedHelmReleases removes HelmReleases that are no longer needed
@@ -671,7 +824,8 @@ func (r *PackageReconciler) cleanupOrphanedHelmReleases(ctx context.Context, pkg
namespace := component.Install.Namespace
if namespace == "" {
namespace = "cozy-system"
// Skip components with empty namespace (they shouldn't exist anyway)
continue
}
releaseName := component.Install.ReleaseName
@@ -710,95 +864,6 @@ func (r *PackageReconciler) cleanupOrphanedHelmReleases(ctx context.Context, pkg
return nil
}
// triggerDependentPackages triggers reconcile for all Packages that depend on the given Package
func (r *PackageReconciler) triggerDependentPackages(ctx context.Context, packageName string) error {
logger := log.FromContext(ctx)
// Get all Packages
packageList := &cozyv1alpha1.PackageList{}
if err := r.List(ctx, packageList); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
// For each Package, check if it depends on the given Package
for _, pkg := range packageList.Items {
// Skip the Package itself
if pkg.Name == packageName {
continue
}
// Get PackageSource
packageSource := &cozyv1alpha1.PackageSource{}
if err := r.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
if apierrors.IsNotFound(err) {
continue
}
logger.V(1).Error(err, "failed to get PackageSource", "package", pkg.Name)
continue
}
// Determine variant
variantName := pkg.Spec.Variant
if variantName == "" {
variantName = "default"
}
// Find variant
var variant *cozyv1alpha1.Variant
for i := range packageSource.Spec.Variants {
if packageSource.Spec.Variants[i].Name == variantName {
variant = &packageSource.Spec.Variants[i]
break
}
}
if variant == nil {
continue
}
// Check if this Package depends on the given Package
dependsOn := false
for _, dep := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == dep {
ignore = true
break
}
}
if ignore {
continue
}
if dep == packageName {
dependsOn = true
break
}
}
if dependsOn {
logger.V(1).Info("triggering reconcile for dependent Package", "package", pkg.Name, "dependency", packageName)
// Trigger reconcile by updating the Package (add annotation or just requeue)
// We can't directly requeue from here, but we can update the Package to trigger reconcile
// Actually, we can use the client to trigger an update, but that might cause infinite loop
// Better approach: use event handler or just log and let the watch handle it
// For now, we'll just log - the watch on PackageSource should handle it
// But we need a way to trigger reconcile...
// Let's add an annotation to trigger reconcile
if pkg.Annotations == nil {
pkg.Annotations = make(map[string]string)
}
pkg.Annotations["cozystack.io/trigger-reconcile"] = fmt.Sprintf("%d", metav1.Now().Unix())
if err := r.Update(ctx, &pkg); err != nil {
logger.V(1).Error(err, "failed to trigger reconcile for dependent Package", "package", pkg.Name)
}
}
}
return nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *PackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
@@ -843,41 +908,31 @@ func (r *PackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
if pkg.Name == updatedPkg.Name {
continue // Skip the Package itself
}
// Get PackageSource to check dependencies
packageSource := &cozyv1alpha1.PackageSource{}
if err := mgr.GetClient().Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
// Get variant to check dependencies
variant, err := r.getVariantForPackage(ctx, &pkg, mgr.GetClient())
if err != nil {
// Continue if PackageSource or variant not found
continue
}
// Determine variant
variantName := pkg.Spec.Variant
if variantName == "" {
variantName = "default"
}
// Find variant
for _, variant := range packageSource.Spec.Variants {
if variant.Name == variantName {
// Check if this variant depends on updatedPkg
for _, dep := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == dep {
ignore = true
break
}
}
if ignore {
continue
}
if dep == updatedPkg.Name {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: pkg.Name,
},
})
break
}
// Check if this variant depends on updatedPkg
for _, dep := range variant.DependsOn {
// Check if dependency is in IgnoreDependencies
ignore := false
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
if ignoreDep == dep {
ignore = true
break
}
}
if ignore {
continue
}
if dep == updatedPkg.Name {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: pkg.Name,
},
})
break
}
}

View File

@@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -84,11 +85,21 @@ func (r *PackageSourceReconciler) reconcileArtifactGenerators(ctx context.Contex
return nil
}
// Build library map from all variants
// Map key is the library name (from lib.Name or extracted from path)
// This allows components to reference libraries by name
libraryMap := make(map[string]cozyv1alpha1.Library)
// Namespace is always cozy-system
namespace := "cozy-system"
// ArtifactGenerator name is the package source name
agName := packageSource.Name
// Collect all OutputArtifacts
outputArtifacts := []sourcewatcherv1beta1.OutputArtifact{}
// Process all variants and their components
for _, variant := range packageSource.Spec.Variants {
// Build library map for this variant
// Map key is the library name (from lib.Name or extracted from path)
// This allows components in this variant to reference libraries by name
// Libraries are scoped per variant to avoid conflicts between variants
libraryMap := make(map[string]cozyv1alpha1.Library)
for _, lib := range variant.Libraries {
libName := lib.Name
if libName == "" {
@@ -100,18 +111,7 @@ func (r *PackageSourceReconciler) reconcileArtifactGenerators(ctx context.Contex
libraryMap[libName] = lib
}
}
}
// Namespace is always cozy-system
namespace := "cozy-system"
// ArtifactGenerator name is the package source name
agName := packageSource.Name
// Collect all OutputArtifacts
outputArtifacts := []sourcewatcherv1beta1.OutputArtifact{}
// Process all variants and their components
for _, variant := range packageSource.Spec.Variants {
for _, component := range variant.Components {
// Skip components without path
if component.Path == "" {
@@ -209,10 +209,14 @@ func (r *PackageSourceReconciler) reconcileArtifactGenerators(ctx context.Contex
}
// Set ownerReference
gvk, err := apiutil.GVKForObject(packageSource, r.Scheme)
if err != nil {
return fmt.Errorf("failed to get GVK for PackageSource: %w", err)
}
ag.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: packageSource.APIVersion,
Kind: packageSource.Kind,
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: packageSource.Name,
UID: packageSource.UID,
Controller: func() *bool { b := true; return &b }(),
@@ -306,61 +310,23 @@ func (r *PackageSourceReconciler) buildSourceFilePath(sourceName, basePath, path
return fmt.Sprintf("@%s/%s", sourceName, fullPath)
}
// createOrUpdate creates or updates a resource
// createOrUpdate creates or updates a resource using server-side apply
func (r *PackageSourceReconciler) createOrUpdate(ctx context.Context, obj client.Object) error {
existing := obj.DeepCopyObject().(client.Object)
key := client.ObjectKeyFromObject(obj)
err := r.Get(ctx, key, existing)
if apierrors.IsNotFound(err) {
return r.Create(ctx, obj)
} else if err != nil {
return err
}
// Preserve resource version
obj.SetResourceVersion(existing.GetResourceVersion())
// Merge labels and annotations
labels := obj.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
for k, v := range existing.GetLabels() {
if _, ok := labels[k]; !ok {
labels[k] = v
}
}
obj.SetLabels(labels)
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
for k, v := range existing.GetAnnotations() {
if _, ok := annotations[k]; !ok {
annotations[k] = v
}
}
obj.SetAnnotations(annotations)
// For ArtifactGenerator, explicitly update Spec (OutputArtifacts and Sources)
if ag, ok := obj.(*sourcewatcherv1beta1.ArtifactGenerator); ok {
if existingAG, ok := existing.(*sourcewatcherv1beta1.ArtifactGenerator); ok {
logger := log.FromContext(ctx)
logger.V(1).Info("updating ArtifactGenerator Spec", "name", ag.Name, "namespace", ag.Namespace,
"outputArtifactCount", len(ag.Spec.OutputArtifacts))
// Update Spec from obj (which contains the desired state with all OutputArtifacts)
existingAG.Spec = ag.Spec
// Preserve metadata updates we made above
existingAG.SetLabels(ag.GetLabels())
existingAG.SetAnnotations(ag.GetAnnotations())
existingAG.SetOwnerReferences(ag.GetOwnerReferences())
// Use existingAG for Update
obj = existingAG
// Ensure TypeMeta is set for server-side apply
// Use type assertion to set GVK if the object supports it
if runtimeObj, ok := obj.(runtime.Object); ok {
gvk, err := apiutil.GVKForObject(obj, r.Scheme)
if err != nil {
return fmt.Errorf("failed to get GVK for object: %w", err)
}
runtimeObj.GetObjectKind().SetGroupVersionKind(gvk)
}
return r.Update(ctx, obj)
// Use server-side apply with field manager
// This is atomic and avoids race conditions from Get/Create/Update pattern
// Labels, annotations, and spec will be merged automatically by the server
// Each field is treated separately, so existing ones are preserved
return r.Patch(ctx, obj, client.Apply, client.FieldOwner("cozystack-packagesource-controller"))
}
// updateStatus updates PackageSource status (variants and conditions from ArtifactGenerator)

View File

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

View File

@@ -0,0 +1,78 @@
#!/bin/sh
set -e
usage() {
echo "Usage: $0 <lock|unlock> <namespace> <bucket-name>"
echo ""
echo "Commands:"
echo " lock - Block deletion and modification of objects in the bucket"
echo " unlock - Restore full access to the bucket"
echo ""
echo "Example:"
echo " $0 lock tenant-root somebucket"
echo " $0 unlock tenant-root somebucket"
exit 1
}
if [ $# -ne 3 ]; then
usage
fi
ACTION="$1"
NAMESPACE="$2"
BUCKET_NAME="$3"
if [ "$ACTION" != "lock" ] && [ "$ACTION" != "unlock" ]; then
echo "Error: First argument must be 'lock' or 'unlock'"
usage
fi
# Check if bucket exists
if ! kubectl get buckets.apps.cozystack.io -n "$NAMESPACE" "$BUCKET_NAME" > /dev/null 2>&1; then
echo "Error: Bucket '$BUCKET_NAME' not found in namespace '$NAMESPACE'"
exit 1
fi
# Get secret and extract bucket config and bucket name using go-template + jq
SECRET_NAME="bucket-$BUCKET_NAME"
BUCKET_INFO=$(kubectl get secret -n "$NAMESPACE" "$SECRET_NAME" -o go-template='{{ .data.BucketInfo | base64decode }}')
BUCKET_CONFIG=$(echo "$BUCKET_INFO" | jq -r '.metadata.name')
S3_BUCKET_NAME=$(echo "$BUCKET_INFO" | jq -r '.spec.bucketName')
# Convert bc- prefix to ba- for bucket account username
BUCKET_ACCOUNT=$(echo "$BUCKET_CONFIG" | sed 's/^bc-/ba-/')
if [ -z "$BUCKET_ACCOUNT" ] || [ -z "$S3_BUCKET_NAME" ]; then
echo "Error: Could not extract bucket account or bucket name from secret '$SECRET_NAME'"
exit 1
fi
# Get seaweedfs namespace from namespace annotation
SEAWEEDFS_NS=$(kubectl get namespace "$NAMESPACE" -o jsonpath='{.metadata.annotations.namespace\.cozystack\.io/seaweedfs}')
if [ -z "$SEAWEEDFS_NS" ]; then
echo "Error: Could not find seaweedfs namespace annotation on namespace '$NAMESPACE'"
exit 1
fi
# Build the s3.configure command
ACTIONS="Read:$S3_BUCKET_NAME,Write:$S3_BUCKET_NAME,List:$S3_BUCKET_NAME,Tagging:$S3_BUCKET_NAME"
if [ "$ACTION" = "lock" ]; then
S3_CMD="s3.configure -actions $ACTIONS -user $BUCKET_ACCOUNT --delete --apply"
echo "Locking bucket '$BUCKET_NAME' in namespace '$NAMESPACE'..."
else
S3_CMD="s3.configure -actions $ACTIONS -user $BUCKET_ACCOUNT --apply"
echo "Unlocking bucket '$BUCKET_NAME' in namespace '$NAMESPACE'..."
fi
echo "Executing command in seaweedfs-master-0 (namespace: $SEAWEEDFS_NS):"
echo " $S3_CMD"
echo ""
# Execute the command
echo "$S3_CMD" | kubectl exec -i -n "$SEAWEEDFS_NS" seaweedfs-master-0 -- weed shell
echo ""
echo "Done. Bucket '$BUCKET_NAME' has been ${ACTION}ed."

View File

@@ -3,10 +3,15 @@ kind: HelmRelease
metadata:
name: {{ .Release.Name }}-system
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-bucket-application-default-bucket-system
namespace: cozy-system
chart:
spec:
chart: cozy-bucket
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
interval: 5m
timeout: 10m
install:

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,10 +8,15 @@ metadata:
spec:
interval: 5m
releaseName: csi
chartRef:
kind: ExternalArtifact
name: cozystack-kubernetes-application-kubevirt-kubernetes-kubevirt-csi-node
namespace: cozy-system
chart:
spec:
chart: cozy-kubevirt-csi-node
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,10 +9,15 @@ metadata:
spec:
interval: 5m
releaseName: vertical-pod-autoscaler-crds
chartRef:
kind: ExternalArtifact
name: cozystack-kubernetes-application-kubevirt-kubernetes-vertical-pod-autoscaler-crds
namespace: cozy-system
chart:
spec:
chart: cozy-vertical-pod-autoscaler-crds
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig

View File

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

View File

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

View File

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

View File

@@ -35,10 +35,15 @@ kind: HelmRelease
metadata:
name: {{ .Release.Name }}-system
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-nats-application-default-nats-system
namespace: cozy-system
chart:
spec:
chart: cozy-nats
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
interval: 5m
timeout: 10m
install:

View File

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

View File

@@ -13,10 +13,15 @@ metadata:
apps.cozystack.io/application.group: apps.cozystack.io
apps.cozystack.io/application.name: etcd
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-etcd-application-default-etcd
namespace: cozy-system
chart:
spec:
chart: etcd
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
version: '>= 0.0.0-0'
interval: 5m
timeout: 10m
install:

View File

@@ -12,10 +12,15 @@ metadata:
apps.cozystack.io/application.group: apps.cozystack.io
apps.cozystack.io/application.name: info
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-info-application-default-info
namespace: cozy-system
chart:
spec:
chart: info
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
version: '>= 0.0.0-0'
interval: 5m
timeout: 10m
install:

View File

@@ -13,10 +13,15 @@ metadata:
apps.cozystack.io/application.group: apps.cozystack.io
apps.cozystack.io/application.name: ingress
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-ingress-application-default-ingress
namespace: cozy-system
chart:
spec:
chart: ingress
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
version: '>= 0.0.0-0'
interval: 5m
timeout: 10m
install:

View File

@@ -13,10 +13,15 @@ metadata:
apps.cozystack.io/application.group: apps.cozystack.io
apps.cozystack.io/application.name: monitoring
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-monitoring-application-default-monitoring
namespace: cozy-system
chart:
spec:
chart: monitoring
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
version: '>= 0.0.0-0'
interval: 5m
timeout: 10m
install:

View File

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

View File

@@ -13,10 +13,15 @@ metadata:
apps.cozystack.io/application.group: apps.cozystack.io
apps.cozystack.io/application.name: seaweedfs
spec:
chartRef:
kind: ExternalArtifact
name: cozystack-seaweedfs-application-default-seaweedfs
namespace: cozy-system
chart:
spec:
chart: seaweedfs
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
version: '>= 0.0.0-0'
interval: 5m
timeout: 10m
install:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
../../../internal/fluxinstall/manifests

View File

@@ -0,0 +1,13 @@
{{- define "cozy.kubernetes_envs" }}
{{- $cozyDeployment := lookup "apps/v1" "Deployment" "cozy-system" "cozystack" }}
{{- $cozyContainers := dig "spec" "template" "spec" "containers" dict $cozyDeployment }}
{{- range $cozyContainers }}
{{- if eq .name "cozystack" }}
{{- range .env }}
{{- if has .name (list "KUBERNETES_SERVICE_HOST" "KUBERNETES_SERVICE_PORT") }}
- {{ toJson . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@

View File

@@ -1,77 +1,57 @@
NAME=installer
NAMESPACE=cozy-system
TALOS_VERSION=$(shell awk '/^version:/ {print $$2}' images/talos/profiles/installer.yaml)
include ../../../scripts/common-envs.mk
pre-checks:
../../../hack/pre-checks.sh
show:
cozypkg show -n $(NAMESPACE) $(NAME) --plain
cozyhr show -n $(NAMESPACE) $(NAME) --plain
apply:
cozypkg show -n $(NAMESPACE) $(NAME) --plain | kubectl apply -f-
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl apply -f-
diff:
cozypkg show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f -
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f -
update:
hack/gen-profiles.sh
image: pre-checks image-cozystack
image: pre-checks image-matchbox image-talos image-operator image-packages
image-talos:
test -f ../../../_out/assets/installer-amd64.tar || make talos-installer
skopeo copy docker-archive:../../../_out/assets/installer-amd64.tar docker://$(REGISTRY)/talos:$(call settag,$(TALOS_VERSION))
image-matchbox:
test -f ../../../_out/assets/kernel-amd64 || make talos-kernel
test -f ../../../_out/assets/initramfs-metal-amd64.xz || make talos-initramfs
docker buildx build -f images/matchbox/Dockerfile ../../.. \
--tag $(REGISTRY)/matchbox:$(call settag,$(TAG)) \
--tag $(REGISTRY)/matchbox:$(call settag,$(TALOS_VERSION)-$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/matchbox:latest \
image-cozystack:
docker buildx build -f images/cozystack/Dockerfile ../../.. \
--tag $(REGISTRY)/installer:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/installer:latest \
--cache-to type=inline \
--metadata-file images/matchbox.json \
--metadata-file images/installer.json \
$(BUILDX_ARGS)
echo "$(REGISTRY)/matchbox:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/matchbox.json -o json -r)" \
> ../../extra/bootbox/images/matchbox.tag
rm -f images/matchbox.json
assets: talos-iso talos-nocloud talos-metal talos-kernel talos-initramfs
talos-initramfs talos-kernel talos-installer talos-iso talos-nocloud talos-metal:
mkdir -p ../../../_out/assets
cat images/talos/profiles/$(subst talos-,,$@).yaml | \
docker run --rm -i -v /dev:/dev --privileged "ghcr.io/siderolabs/imager:$(TALOS_VERSION)" --tar-to-stdout - | \
tar -C ../../../_out/assets -xzf-
IMAGE="$(REGISTRY)/installer:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/installer.json -o json -r)" \
yq -i '.cozystack.image = strenv(IMAGE)' values.yaml
rm -f images/installer.json
update-version:
TAG="$(call settag,$(TAG))" \
yq -i '.cozystackOperator.cozystackVersion = strenv(TAG)' values.yaml
yq -i '.cozystackOperator.cozystackVersion = strenv(TAG)' values.yaml
image-operator:
docker buildx build -f images/cozystack-operator/Dockerfile ../../.. \
--tag $(REGISTRY)/cozystack-operator:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/cozystack-operator:latest \
--cache-to type=inline \
--metadata-file images/cozystack-operator.json \
$(BUILDX_ARGS)
--tag $(REGISTRY)/cozystack-operator:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/cozystack-operator:latest \
--cache-to type=inline \
--metadata-file images/cozystack-operator.json \
$(BUILDX_ARGS)
IMAGE="$(REGISTRY)/cozystack-operator:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/cozystack-operator.json -o json -r)" \
yq -i '.cozystackOperator.image = strenv(IMAGE)' values.yaml
yq -i '.cozystackOperator.image = strenv(IMAGE)' values.yaml
rm -f images/cozystack-operator.json
image-packages: update-version
mkdir -p ../../../_out/assets images
flux push artifact \
oci://$(REGISTRY)/platform-packages:$(call settag,$(TAG)) \
--path=../../../packages \
--source=https://github.com/cozystack/cozystack \
--revision="$$(git describe --tags):$$(git rev-parse HEAD)" \
2>&1 | tee images/cozystack-packages.log
oci://$(REGISTRY)/platform-packages:$(call settag,$(TAG)) \
--path=../../../packages \
--source=https://github.com/cozystack/cozystack \
--revision="$$(git describe --tags):$$(git rev-parse HEAD)" \
2>&1 | tee images/cozystack-packages.log
export REPO="oci://$(REGISTRY)/platform-packages"; \
export DIGEST=$$(awk -F@ '/artifact successfully pushed/ {print $$2}' images/cozystack-packages.log; rm -f images/cozystack-packages.log); \
test -n "$$DIGEST" && yq -i '.cozystackOperator.platformSource = (strenv(REPO) + "@" + strenv(DIGEST))' values.yaml
test -n "$$DIGEST" && yq -i '.cozystackOperator.platformSource = (strenv(REPO) + "@" + strenv(DIGEST))' values.yaml

View File

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

View File

@@ -57,7 +57,7 @@ spec:
properties:
components:
additionalProperties:
description: PackageRelease defines overrides for a specific component
description: PackageComponent defines overrides for a specific component
properties:
enabled:
description: |-
@@ -148,6 +148,21 @@ spec:
- type
type: object
type: array
dependencies:
additionalProperties:
description: DependencyStatus represents the readiness status of
a dependency
properties:
ready:
description: Ready indicates whether the dependency is ready
type: boolean
required:
- ready
type: object
description: |-
Dependencies tracks the readiness status of each dependency
Key is the dependency package name, value indicates if the dependency is ready
type: object
type: object
type: object
served: true

View File

@@ -12,8 +12,7 @@ spec:
listKind: PackageSourceList
plural: packagesources
shortNames:
- pkgsrc
- pkgsrcs
- pks
singular: packagesource
scope: Cluster
versions:

View File

@@ -0,0 +1,41 @@
FROM golang:1.24-alpine AS k8s-await-election-builder
ARG K8S_AWAIT_ELECTION_GITREPO=https://github.com/LINBIT/k8s-await-election
ARG K8S_AWAIT_ELECTION_VERSION=0.4.1
# TARGETARCH is a docker special variable: https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
ARG TARGETARCH
RUN apk add --no-cache git make
RUN git clone ${K8S_AWAIT_ELECTION_GITREPO} /usr/local/go/k8s-await-election/ \
&& cd /usr/local/go/k8s-await-election \
&& git reset --hard v${K8S_AWAIT_ELECTION_VERSION} \
&& make \
&& mv ./out/k8s-await-election-${TARGETARCH} /k8s-await-election
FROM golang:1.25-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
RUN apk add --no-cache make git
RUN apk add helm --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community
COPY . /src/
WORKDIR /src
RUN go mod download
FROM alpine:3.22
RUN wget -O- https://github.com/cozystack/cozyhr/raw/refs/heads/main/hack/install.sh | sh -s -- -v 1.5.0
RUN apk add --no-cache make kubectl helm coreutils git jq openssl
COPY --from=builder /src/scripts /cozystack/scripts
COPY --from=builder /src/packages/core /cozystack/packages/core
COPY --from=builder /src/packages/system /cozystack/packages/system
COPY --from=k8s-await-election-builder /k8s-await-election /usr/bin/k8s-await-election
WORKDIR /cozystack
ENTRYPOINT ["/usr/bin/k8s-await-election", "/cozystack/scripts/installer.sh" ]

View File

@@ -0,0 +1 @@
_out

View File

@@ -1,3 +1,4 @@
{{- if .Values.cozystackOperator.enabled }}
---
apiVersion: v1
kind: Namespace
@@ -60,7 +61,10 @@ spec:
- --disable-telemetry
{{- end }}
- --platform-source-name=cozystack-platform
- --platform-source={{ .Values.cozystackOperator.platformSource }}
- --platform-source-url={{ .Values.cozystackOperator.platformSourceUrl }}
{{- if .Values.cozystackOperator.platformSourceRef }}
- --platform-source-ref={{ .Values.cozystackOperator.platformSourceRef }}
{{- end }}
env:
- name: KUBERNETES_SERVICE_HOST
value: localhost
@@ -115,3 +119,4 @@ spec:
valuesFiles:
- values.yaml
- values-isp-hosted.yaml
{{- end }}

View File

@@ -0,0 +1,81 @@
{{- if not .Values.cozystackOperator.enabled }}
---
apiVersion: v1
kind: Namespace
metadata:
name: cozy-system
labels:
cozystack.io/system: "true"
pod-security.kubernetes.io/enforce: privileged
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: cozystack
namespace: cozy-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cozystack
subjects:
- kind: ServiceAccount
name: cozystack
namespace: cozy-system
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cozystack
namespace: cozy-system
spec:
replicas: 1
selector:
matchLabels:
app: cozystack
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
template:
metadata:
labels:
app: cozystack
spec:
hostNetwork: true
serviceAccountName: cozystack
containers:
- name: cozystack
image: "{{ .Values.cozystack.image }}"
env:
- name: KUBERNETES_SERVICE_HOST
value: localhost
- name: INSTALL_FLUX
value: "true"
- name: KUBERNETES_SERVICE_PORT
value: "7445"
- name: K8S_AWAIT_ELECTION_ENABLED
value: "1"
- name: K8S_AWAIT_ELECTION_NAME
value: cozystack
- name: K8S_AWAIT_ELECTION_LOCK_NAME
value: cozystack
- name: K8S_AWAIT_ELECTION_LOCK_NAMESPACE
value: cozy-system
- name: K8S_AWAIT_ELECTION_IDENTITY
valueFrom:
fieldRef:
fieldPath: metadata.name
tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoSchedule"
- key: "node.cilium.io/agent-not-ready"
operator: "Exists"
effect: "NoSchedule"
{{- end }}

View File

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

View File

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

View File

@@ -1,30 +1,34 @@
NAME=cozystack-platform
NAME=platform
NAMESPACE=cozy-system
include ../../../scripts/common-envs.mk
include ../../../scripts/package.mk
image: image-migrations
show:
cozyhr show -n $(NAMESPACE) $(NAME) --plain
image-migrations:
docker buildx build images/migrations \
--tag $(REGISTRY)/platform-migrations:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/platform-migrations:latest \
apply:
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl apply -f-
kubectl delete helmreleases.helm.toolkit.fluxcd.io -l cozystack.io/marked-for-deletion=true -A
reconcile: apply
namespaces-show:
cozyhr show -n $(NAMESPACE) $(NAME) --plain -s templates/namespaces.yaml
namespaces-apply:
cozyhr show -n $(NAMESPACE) $(NAME) --plain -s templates/namespaces.yaml | kubectl apply -f-
diff:
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f-
image: image-assets
image-assets:
docker buildx build -f images/cozystack-assets/Dockerfile ../../.. \
--tag $(REGISTRY)/cozystack-assets:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/cozystack-assets:latest \
--cache-to type=inline \
--metadata-file images/migrations.json \
--metadata-file images/cozystack-assets.json \
$(BUILDX_ARGS)
IMAGE="$(REGISTRY)/platform-migrations:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/migrations.json -o json -r)" \
yq -i '.migrations.image = strenv(IMAGE)' values.yaml
rm -f images/migrations.json
generate:
@echo "Generating target version from migrations..."
@LAST_MIGRATION=$$(ls -1 images/migrations/migrations/ 2>/dev/null | grep -E '^[0-9]+$$' | sort -n | tail -1); \
if [ -z "$$LAST_MIGRATION" ]; then \
echo "Error: No migration files found" >&2; \
exit 1; \
fi; \
TARGET_VERSION=$$((LAST_MIGRATION + 1)); \
echo "Last migration: $$LAST_MIGRATION, Target version: $$TARGET_VERSION"; \
yq -i '.migrations.targetVersion = $$TARGET_VERSION' values.yaml; \
echo "Updated targetVersion to $$TARGET_VERSION in values.yaml"
IMAGE="$(REGISTRY)/cozystack-assets:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/cozystack-assets.json -o json -r)" \
yq -i '.assets.image = strenv(IMAGE)' values.yaml
rm -f images/cozystack-assets.json

View File

@@ -1,4 +1,4 @@
FROM golang:1.24-alpine AS builder
FROM golang:1.25-alpine AS builder
ARG TARGETOS
ARG TARGETARCH

View File

@@ -1,13 +0,0 @@
FROM alpine:3.22
RUN wget -O- https://github.com/cozystack/cozypkg/raw/refs/heads/main/hack/install.sh | sh -s -- -v 1.4.0
RUN apk add --no-cache kubectl helm coreutils git jq ca-certificates bash curl
COPY migrations /migrations
COPY run-migrations.sh /usr/bin/run-migrations.sh
WORKDIR /migrations
ENTRYPOINT ["/usr/bin/run-migrations.sh"]

View File

@@ -1,25 +0,0 @@
#!/bin/sh
# Migration 1 --> 2
# Fix mariadb-operator secrets
if kubectl get -n cozy-mariadb-operator secret/mariadb-operator-webhook-cert; then
kubectl annotate -n cozy-mariadb-operator secret/mariadb-operator-webhook-cert meta.helm.sh/release-namespace=cozy-mariadb-operator meta.helm.sh/release-name=mariadb-operator
kubectl label -n cozy-mariadb-operator secret/mariadb-operator-webhook-cert app.kubernetes.io/managed-by=Helm
fi
# Gratefully remove fluxcd release and keep resources
if kubectl get hr -n cozy-fluxcd cozy-fluxcd 2>/dev/null; then
kubectl patch hr -n cozy-fluxcd cozy-fluxcd -p '{"spec": {"suspend": true}, "metadata": {"finalizers": null}}' --type=merge
kubectl delete hr -n cozy-fluxcd cozy-fluxcd
fi
kubectl delete secret -n cozy-fluxcd -l name=fluxcd
# Fix kubeovn crds
kubeovn_crds=$(kubectl get crd -o name | grep '\.kubeovn\.io$')
if [ -n "$kubeovn_crds" ]; then
kubectl annotate $kubeovn_crds meta.helm.sh/release-namespace=cozy-kubeovn meta.helm.sh/release-name=kubeovn
kubectl label $kubeovn_crds app.kubernetes.io/managed-by=Helm
fi
# Write version to cozystack-version config
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=2 --dry-run=client -o yaml | kubectl apply -f-

View File

@@ -1,15 +0,0 @@
#!/bin/sh
# Migration 10 --> 11
# Force reconcile hr keycloak-configure
if kubectl get helmrelease keycloak-configure -n cozy-keycloak; then
kubectl delete po -l app=source-controller -n cozy-fluxcd
timestamp=$(date --rfc-3339=ns)
kubectl annotate helmrelease keycloak-configure -n cozy-keycloak \
reconcile.fluxcd.io/forceAt="$timestamp" \
reconcile.fluxcd.io/requestedAt="$timestamp" \
--overwrite
fi
# Write version to cozystack-version config
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=11 --dry-run=client -o yaml | kubectl apply -f-

View File

@@ -1,21 +0,0 @@
#!/bin/sh
# Migration 11 --> 12
# Recreate daemonset kube-rbac-proxy
if kubectl get daemonset kube-rbac-proxy -n cozy-monitoring; then
kubectl delete daemonset kube-rbac-proxy --cascade=orphan -n cozy-monitoring
fi
if kubectl get helmrelease monitoring-agents -n cozy-monitoring; then
timestamp=$(date --rfc-3339=ns)
kubectl annotate helmrelease monitoring-agents -n cozy-monitoring \
reconcile.fluxcd.io/forceAt="$timestamp" \
reconcile.fluxcd.io/requestedAt="$timestamp" \
--overwrite
fi
kubectl delete pods -l app.kubernetes.io/component=kube-rbac-proxy -n cozy-monitoring
# Write version to cozystack-version config
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=12 --dry-run=client -o yaml | kubectl apply -f-

View File

@@ -1,35 +0,0 @@
#!/bin/sh
# Migration 12 --> 13
# Copy configuration from ingress to cozystack configmap
if kubectl get hr -n tenant-root tenant-root > /dev/null; then
expose_services=$(
kubectl get hr -n tenant-root ingress -o go-template='{{ with .spec }}{{ with .values }}{{ if .dashboard }}dashboard,{{ end }}{{ if .cdiUploadProxy }}cdi-uploadproxy,{{ end }}{{ if .virtExportProxy }}vm-exportproxy,{{ end }}{{ end }}{{ end }}'
)
expose_services=$(echo "$expose_services" | awk '{sub(/,$/,""); print}')
expose_external_ips=$(
kubectl get hr -n tenant-root ingress -o go-template='{{ with .spec }}{{ with .values }}{{ if .externalIPs }}{{ range .externalIPs }}{{ . }},{{ end }}{{ end }}{{ end }}{{ end }}'
)
expose_external_ips=$(echo "$expose_external_ips" | awk '{sub(/,$/,""); print}')
existing_expose_external_ips=$(kubectl get cm -n cozy-system cozystack -o go-template='{{ index .data "expose-external-ips" }}')
existing_expose_services=$(kubectl get cm -n cozy-system cozystack -o go-template='{{ index .data "expose-services" }}')
if [ "$existing_expose_external_ips" == "<no value>" ]; then
kubectl patch cm -n cozy-system cozystack --type merge -p="{\"data\":{\"expose-external-ips\":\"$expose_external_ips\"}}"
fi
if [ "$existing_expose_services" == "<no value>" ]; then
kubectl patch cm -n cozy-system cozystack --type merge -p="{\"data\":{\"expose-services\":\"$expose_services\"}}"
fi
kubectl patch hr -n tenant-root ingress --type json -p='[{"op": "remove", "path": "/spec/values/dashboard"}]' || true
kubectl patch hr -n tenant-root ingress --type json -p='[{"op": "remove", "path": "/spec/values/cdiUploadProxy"}]' || true
kubectl patch hr -n tenant-root ingress --type json -p='[{"op": "remove", "path": "/spec/values/virtExportProxy"}]' || true
kubectl patch hr -n tenant-root ingress --type json -p='[{"op": "remove", "path": "/spec/values/externalIPs"}]' || true
kubectl patch hr -n tenant-root ingress --type merge -p='{"spec":{"chart":{"spec":{"version":"1.6.0"}}}}'
fi
# Write version to cozystack-version config
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=13 --dry-run=client -o yaml | kubectl apply -f-

View File

@@ -1,10 +0,0 @@
#!/bin/sh
# Migration 13 --> 14
# Upgrade tenants.apps to new chart version
kubectl get tenants.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
kubectl patch tenants.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"1.10.0"}'
done
# Write version to cozystack-version config
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=14 --dry-run=client -o yaml | kubectl apply -f-

View File

@@ -1,10 +0,0 @@
#!/bin/sh
# Migration 14 --> 15
# Delete the `capi-providers` HelmRelease in the `cozy-cluster-api` namespace if present
if kubectl get hr -n cozy-cluster-api capi-providers >/dev/null 2>&1; then
kubectl delete hr -n cozy-cluster-api capi-providers
fi
# Write version to cozystack-version config
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=15 --dry-run=client -o yaml | kubectl apply -f-

View File

@@ -1,28 +0,0 @@
#!/bin/sh
# Migration 15 --> 16
if kubectl get validatingwebhookconfigurations.admissionregistration.k8s.io kamaji-validating-webhook-configuration; then
kubectl delete validatingwebhookconfigurations.admissionregistration.k8s.io kamaji-validating-webhook-configuration
fi
kubectl get kamajicontrolplane -A -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,VERSION:.spec.version,VERSION:.status.version --no-headers |
while read namespace name version status; do
if [ "$status" = "v1.32.4" ]; then
continue
fi
(set -x; kubectl patch kamajicontrolplane "$name" -n "$namespace" --type merge -p '{"spec":{"version":"1.32.4"}}')
(set -x; kubectl patch kamajicontrolplane "$name" -n "$namespace" --type merge -p '{"status":{"version":"v1.32.4"}}' --subresource status)
(set -x; kubectl patch tcp "$name" -n "$namespace" --type merge -p '{"spec":{"kubernetes":{"version":"1.32.4"}}}')
(set -x; kubectl patch tcp "$name" -n "$namespace" --type merge -p '{"status":{"kubernetesResources":{"version":{"version":"v1.32.4"}}}} ' --subresource status)
done
# Upgrade kubernetes.apps to new chart version
kubectl get kuberneteses.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
kubectl patch kuberneteses.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"0.24.0"}'
done
if kubectl get helmrelease kamaji -n cozy-kamaji; then
cozypkg reconcile kamaji -n cozy-kamaji --force
fi
# Write version to cozystack-version config
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=16 --dry-run=client -o yaml | kubectl apply -f-

View File

@@ -1,95 +0,0 @@
#!/bin/sh
# Migration 16 --> 17
#
# fix-nested-resources-map.sh prints kubectl patch commands.
# * Replaces each resources section with {cpu,memory} merged from requests + limits
# * Adds/Replaces .appVersion with "*"
set -e
CRDS='
clickhouses.apps.cozystack.io
etcds.apps.cozystack.io
ferretdb.apps.cozystack.io
httpcaches.apps.cozystack.io
kafkas.apps.cozystack.io
kuberneteses.apps.cozystack.io
monitorings.apps.cozystack.io
mysqls.apps.cozystack.io
natses.apps.cozystack.io
postgreses.apps.cozystack.io
rabbitmqs.apps.cozystack.io
redises.apps.cozystack.io
seaweedfses.apps.cozystack.io
tcpbalancers.apps.cozystack.io
virtualmachines.apps.cozystack.io
vminstances.apps.cozystack.io
vpns.apps.cozystack.io
'
for KIND in $CRDS; do
kubectl get "$KIND" -A -o json | jq -r --arg kind "$KIND" '
.items[]
| . as $obj
| ($obj.metadata.namespace // "") as $ns # namespace (empty string for cluster-scoped)
| $obj.metadata.name as $name # object name
# -------------------------------------------------------------------------
# Build an array with every JSON path ending with "resources"
# -------------------------------------------------------------------------
| [ $obj
| paths
| select(.[-1] == "resources")
] as $rpaths
# -------------------------------------------------------------------------
# Iterate through each resources path
# -------------------------------------------------------------------------
| foreach $rpaths[] as $rpath (null;
# Current resources object
($obj | getpath($rpath)) as $res
# requests + limits merged; requests override limits on key collision
| ($res.requests? // {}) as $req
| ($res.limits? // {}) as $lim
| ($req + $lim) as $flat
# Keep cpu & memory only
| ($flat
| with_entries(select(.key|test("^(cpu|memory)$")))
) as $value
| select(($value|length) > 0) # skip if nothing to patch
# ---------------------------------------------------------------------
# RFC6901-encoded JSON Pointer to the resources section
# ---------------------------------------------------------------------
| ("/" + ($rpath
| map(
tostring
| gsub("~";"~0")
| gsub("/";"~1")
)
| join("/")
)
) as $pointer
# ---------------------------------------------------------------------
# Compose JSON Patch: 1) add/replace appVersion, 2) replace resources
# ---------------------------------------------------------------------
| [
{ op:"add", path:"/appVersion", value:"*" },
{ op:"replace", path:$pointer, value:$value }
] as $patch
# ---------------------------------------------------------------------
# Print one ready-to-run kubectl patch command
# ---------------------------------------------------------------------
| "kubectl " +
(if $ns != "" then "-n \($ns) " else "" end) +
"patch \($kind) \($name) --type=json -p '\''\($patch|tojson)'\''"
)
' | sh -ex
done
# Write version to cozystack-version config
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=17 --dry-run=client -o yaml | kubectl apply -f-

View File

@@ -1,10 +0,0 @@
#!/bin/sh
# Migration 17 --> 18
# Upgrade kubernetes.apps to new chart version
kubectl get kuberneteses.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
kubectl patch kuberneteses.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"0.26.1"}'
done
# Write version to cozystack-version config
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=18 --dry-run=client -o yaml | kubectl apply -f-

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