Compare commits

...

53 Commits

Author SHA1 Message Date
Andrei Kvapil
488b1bf27b build(cozyctl): add Makefile build and asset targets
Add cozyctl build target and cross-platform asset packaging targets
following the same pattern as cozypkg.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-23 17:47:45 +01:00
Andrei Kvapil
f4e9660b43 feat(cozyctl): add VM commands (console, vnc, migrate, port-forward)
VM interaction commands that resolve application type to VM name via
ApplicationDefinition discovery and exec virtctl for the actual work.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-23 17:47:45 +01:00
Andrei Kvapil
d9cfd5ac9e feat(cozyctl): add get command for applications and sub-resources
Dispatch logic for all resource types:
- Built-in: ns, modules, pvc
- Sub-resources: secrets, services, ingresses, workloads with -t flag
- Application types via dynamic ApplicationDefinition discovery

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-23 17:47:45 +01:00
Andrei Kvapil
5762ac4139 feat(cozyctl): add tabwriter-based resource printer
Output formatting for applications, namespaces, modules, PVCs,
secrets, services, ingresses, and workload monitors.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-23 17:47:45 +01:00
Andrei Kvapil
38f446c0d3 feat(cozyctl): add ApplicationDefinition discovery registry
Discover ApplicationDefinitions from the cluster and build a lookup
registry by plural/singular/kind names for dynamic resource resolution.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-23 17:47:45 +01:00
Andrei Kvapil
4de8e91864 feat(cozyctl): add CLI skeleton with root command and client factory
Introduce the cozyctl CLI tool for managing Cozystack applications
from the terminal. This initial commit includes:
- main.go entry point
- Cobra root command with --kubeconfig, --context, -n flags
- K8s client factory (typed + dynamic) with namespace resolution

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-02-23 17:47:44 +01:00
Andrei Kvapil
e3a5933f7b [kubevirt] Update kubevirt and CDI (#1833)
## What this PR does
Updates kubevirt to v1.6.3 and CDI to v1.64.0.
Please note that VMs would be live-migrated as a part of the update.

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-21 19:09:09 +03:00
Andrei Kvapil
02064888a4 feat(platform): make cluster issuer name and ACME solver configurable (#2077)
<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

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

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

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

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

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

  ## Breaking changes

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

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

### Release note

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

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

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

## Summary by CodeRabbit

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

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

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

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

### Release note

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

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

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

Changes:

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

### Release note

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


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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-20 12:55:11 +03:00
Andrei Kvapil
1c05999812 fix(cozystack-basics) Deny resourcequotas deletion for tenant admin (#2076)
<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Release note

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

```release-note
Fixed cozy:tenant:admin:base ClusterRole to deny deletion of tenant ResourceQuotas for the tenant admin and superadmin
```

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

## Summary by CodeRabbit

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

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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-19 23:27:05 +03:00
Andrei Kvapil
a6b498d7ec feat(dashboard) VMInstance dropdowns for disks and instanceType (#2071)
<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

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

Details

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

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

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


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

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


### Release note

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

```release-note
[vminstance] Add dropdowns for `instanceType` and `disk[].name` using `VirtualMachineClusterInstancetype` cluster resources and `VMDisk` from current namespace
```

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

## Summary by CodeRabbit

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

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

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

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

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

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

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

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

### Release note

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

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

## Summary by CodeRabbit

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

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

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

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

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

### Release note

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-19 14:33:56 +05:00
Andrei Kvapil
16db457536 fix(cozystack-basics) Preserve existing HelmRelease values during reconciliations (#2068)
<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

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

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

### Release note

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

```release-note
[cozystack-basics] Preserve existing `HelmRelease` values of `tenant-root` during reconciliations
```

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

## Summary by CodeRabbit

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-19 07:48:17 +01:00
Kirill Ilin
b5b2f95c3e [cozystack-basics] Preserve existing HelmRelease values during reconciliations
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-02-18 21:52:51 +05:00
91 changed files with 15256 additions and 956 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: manifests assets unit-tests helm-unit-tests verify-crds
.PHONY: manifests assets unit-tests helm-unit-tests
include hack/common-envs.mk
@@ -38,30 +38,30 @@ build: build-deps
manifests:
mkdir -p _out/assets
cat packages/core/installer/crds/*.yaml > _out/assets/cozystack-crds.yaml
cat internal/crdinstall/manifests/*.yaml > _out/assets/cozystack-crds.yaml
# Talos variant (default)
helm template installer packages/core/installer -n cozy-system \
-s templates/cozystack-operator.yaml \
-s templates/packagesource.yaml \
--show-only templates/cozystack-operator.yaml \
> _out/assets/cozystack-operator-talos.yaml
# Generic Kubernetes variant (k3s, kubeadm, RKE2)
helm template installer packages/core/installer -n cozy-system \
--set cozystackOperator.variant=generic \
--set cozystack.apiServerHost=REPLACE_ME \
-s templates/cozystack-operator.yaml \
-s templates/packagesource.yaml \
--show-only templates/cozystack-operator.yaml \
> _out/assets/cozystack-operator-generic.yaml
# Hosted variant (managed Kubernetes)
helm template installer packages/core/installer -n cozy-system \
--set cozystackOperator.variant=hosted \
-s templates/cozystack-operator.yaml \
-s templates/packagesource.yaml \
--show-only templates/cozystack-operator.yaml \
> _out/assets/cozystack-operator-hosted.yaml
cozypkg:
go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozypkg/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozypkg ./cmd/cozypkg
assets: assets-talos assets-cozypkg
cozyctl:
go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozyctl/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozyctl ./cmd/cozyctl
assets: assets-talos assets-cozypkg assets-cozyctl
assets-talos:
make -C packages/core/talos assets
@@ -76,15 +76,21 @@ assets-cozypkg-%:
cp LICENSE _out/bin/cozypkg-$*/LICENSE
tar -C _out/bin/cozypkg-$* -czf _out/assets/cozypkg-$*.tar.gz LICENSE cozypkg$(EXT)
assets-cozyctl: assets-cozyctl-linux-amd64 assets-cozyctl-linux-arm64 assets-cozyctl-darwin-amd64 assets-cozyctl-darwin-arm64 assets-cozyctl-windows-amd64 assets-cozyctl-windows-arm64
(cd _out/assets/ && sha256sum cozyctl-*.tar.gz) > _out/assets/cozyctl-checksums.txt
assets-cozyctl-%:
$(eval EXT := $(if $(filter windows,$(firstword $(subst -, ,$*))),.exe,))
mkdir -p _out/assets
GOOS=$(firstword $(subst -, ,$*)) GOARCH=$(lastword $(subst -, ,$*)) go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozyctl/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozyctl-$*/cozyctl$(EXT) ./cmd/cozyctl
cp LICENSE _out/bin/cozyctl-$*/LICENSE
tar -C _out/bin/cozyctl-$* -czf _out/assets/cozyctl-$*.tar.gz LICENSE cozyctl$(EXT)
test:
make -C packages/core/testing apply
make -C packages/core/testing test
verify-crds:
@diff --recursive packages/core/installer/crds/ internal/crdinstall/manifests/ --exclude='.*' \
|| (echo "ERROR: CRD manifests out of sync. Run 'make generate' to fix." && exit 1)
unit-tests: helm-unit-tests verify-crds
unit-tests: helm-unit-tests
helm-unit-tests:
hack/helm-unit-tests.sh

114
cmd/cozyctl/cmd/client.go Normal file
View File

@@ -0,0 +1,114 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/dynamic"
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"
)
func buildRestConfig() (*rest.Config, error) {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
if globalFlags.kubeconfig != "" {
rules.ExplicitPath = globalFlags.kubeconfig
}
overrides := &clientcmd.ConfigOverrides{}
if globalFlags.context != "" {
overrides.CurrentContext = globalFlags.context
}
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides).ClientConfig()
if err != nil {
return nil, fmt.Errorf("failed to load kubeconfig: %w", err)
}
return config, nil
}
func newScheme() *runtime.Scheme {
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
return scheme
}
func newClients() (client.Client, dynamic.Interface, error) {
config, err := buildRestConfig()
if err != nil {
return nil, nil, err
}
scheme := newScheme()
typedClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
return nil, nil, fmt.Errorf("failed to create k8s client: %w", err)
}
dynClient, err := dynamic.NewForConfig(config)
if err != nil {
return nil, nil, fmt.Errorf("failed to create dynamic client: %w", err)
}
return typedClient, dynClient, nil
}
func getNamespace() (string, error) {
if globalFlags.namespace != "" {
return globalFlags.namespace, nil
}
rules := clientcmd.NewDefaultClientConfigLoadingRules()
if globalFlags.kubeconfig != "" {
rules.ExplicitPath = globalFlags.kubeconfig
}
overrides := &clientcmd.ConfigOverrides{}
if globalFlags.context != "" {
overrides.CurrentContext = globalFlags.context
}
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
ns, _, err := clientConfig.Namespace()
if err != nil {
return "", fmt.Errorf("failed to determine namespace: %w", err)
}
if ns == "" {
ns = "default"
}
return ns, nil
}
// getRestConfig is a convenience function when only the rest.Config is needed
// (used by buildRestConfig but also available for other callers).
func getRestConfig() (*rest.Config, error) {
if globalFlags.kubeconfig != "" || globalFlags.context != "" {
return buildRestConfig()
}
config, err := ctrl.GetConfig()
if err != nil {
return nil, fmt.Errorf("failed to get kubeconfig: %w", err)
}
return config, nil
}

View File

@@ -0,0 +1,43 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"github.com/spf13/cobra"
)
var consoleCmd = &cobra.Command{
Use: "console <type> <name>",
Short: "Open a serial console to a VirtualMachine",
Long: `Open a serial console to a VirtualMachine using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
Args: cobra.ExactArgs(2),
RunE: runConsole,
}
func init() {
rootCmd.AddCommand(consoleCmd)
}
func runConsole(cmd *cobra.Command, args []string) error {
vmName, ns, err := resolveVMArgs(args)
if err != nil {
return err
}
virtctlArgs := []string{"virtctl", "console", vmName, "-n", ns}
return execVirtctl(virtctlArgs)
}

View File

@@ -0,0 +1,112 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"context"
"fmt"
"strings"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// AppDefInfo holds resolved information about an ApplicationDefinition.
type AppDefInfo struct {
Name string // e.g. "postgres"
Kind string // e.g. "Postgres"
Plural string // e.g. "postgreses"
Singular string // e.g. "postgres"
Prefix string // e.g. "postgres-"
IsModule bool
}
// AppDefRegistry provides fast lookup of ApplicationDefinitions by plural, singular, or kind.
type AppDefRegistry struct {
byPlural map[string]*AppDefInfo
bySingular map[string]*AppDefInfo
byKind map[string]*AppDefInfo
all []*AppDefInfo
}
// discoverAppDefs lists all ApplicationDefinitions from the cluster and builds a registry.
func discoverAppDefs(ctx context.Context, typedClient client.Client) (*AppDefRegistry, error) {
var list cozyv1alpha1.ApplicationDefinitionList
if err := typedClient.List(ctx, &list); err != nil {
return nil, fmt.Errorf("failed to list ApplicationDefinitions: %w", err)
}
reg := &AppDefRegistry{
byPlural: make(map[string]*AppDefInfo),
bySingular: make(map[string]*AppDefInfo),
byKind: make(map[string]*AppDefInfo),
}
for i := range list.Items {
ad := &list.Items[i]
info := &AppDefInfo{
Name: ad.Name,
Kind: ad.Spec.Application.Kind,
Plural: ad.Spec.Application.Plural,
Singular: ad.Spec.Application.Singular,
Prefix: ad.Spec.Release.Prefix,
IsModule: ad.Spec.Dashboard != nil && ad.Spec.Dashboard.Module,
}
reg.all = append(reg.all, info)
reg.byPlural[strings.ToLower(info.Plural)] = info
reg.bySingular[strings.ToLower(info.Singular)] = info
reg.byKind[strings.ToLower(info.Kind)] = info
}
return reg, nil
}
// Resolve looks up an AppDefInfo by name (case-insensitive), checking plural, singular, then kind.
func (r *AppDefRegistry) Resolve(name string) *AppDefInfo {
lower := strings.ToLower(name)
if info, ok := r.byPlural[lower]; ok {
return info
}
if info, ok := r.bySingular[lower]; ok {
return info
}
if info, ok := r.byKind[lower]; ok {
return info
}
return nil
}
// ResolveModule looks up an AppDefInfo among modules only.
func (r *AppDefRegistry) ResolveModule(name string) *AppDefInfo {
lower := strings.ToLower(name)
for _, info := range r.all {
if !info.IsModule {
continue
}
if strings.ToLower(info.Plural) == lower ||
strings.ToLower(info.Singular) == lower ||
strings.ToLower(info.Kind) == lower {
return info
}
}
return nil
}
// All returns all discovered AppDefInfo entries.
func (r *AppDefRegistry) All() []*AppDefInfo {
return r.all
}

361
cmd/cozyctl/cmd/get.go Normal file
View File

@@ -0,0 +1,361 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"context"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var getCmdFlags struct {
target string
}
var getCmd = &cobra.Command{
Use: "get <type> [name]",
Short: "Display one or many resources",
Long: `Display one or many resources.
Built-in types:
ns, namespaces Tenant namespaces (cluster-scoped)
modules Tenant modules
pvc, pvcs PersistentVolumeClaims
Sub-resource types (use -t to filter by parent application):
secrets Secrets
services, svc Services
ingresses, ing Ingresses
workloads WorkloadMonitors
Application types are discovered dynamically from ApplicationDefinitions.
Use -t type/name to filter sub-resources by a specific application.`,
Args: cobra.RangeArgs(1, 2),
RunE: runGet,
}
func init() {
rootCmd.AddCommand(getCmd)
getCmd.Flags().StringVarP(&getCmdFlags.target, "target", "t", "", "Filter sub-resources by application type/name")
}
func runGet(cmd *cobra.Command, args []string) error {
ctx := context.Background()
resourceType := args[0]
var resourceName string
if len(args) > 1 {
resourceName = args[1]
}
switch strings.ToLower(resourceType) {
case "ns", "namespace", "namespaces":
return getNamespaces(ctx, resourceName)
case "module", "modules":
return getModules(ctx, resourceName)
case "pvc", "pvcs", "persistentvolumeclaim", "persistentvolumeclaims":
return getPVCs(ctx, resourceName)
case "secret", "secrets":
return getSubResources(ctx, "secrets", resourceName)
case "service", "services", "svc":
return getSubResources(ctx, "services", resourceName)
case "ingress", "ingresses", "ing":
return getSubResources(ctx, "ingresses", resourceName)
case "workload", "workloads":
return getSubResources(ctx, "workloads", resourceName)
default:
return getApplications(ctx, resourceType, resourceName)
}
}
func getNamespaces(ctx context.Context, name string) error {
_, dynClient, err := newClients()
if err != nil {
return err
}
gvr := schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}
if name != "" {
item, err := dynClient.Resource(gvr).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get namespace %q: %w", name, err)
}
printNamespaces([]unstructured.Unstructured{*item})
return nil
}
list, err := dynClient.Resource(gvr).List(ctx, metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to list namespaces: %w", err)
}
if len(list.Items) == 0 {
printNoResources(os.Stderr, "namespaces")
return nil
}
printNamespaces(list.Items)
return nil
}
func getModules(ctx context.Context, name string) error {
_, dynClient, err := newClients()
if err != nil {
return err
}
ns, err := getNamespace()
if err != nil {
return err
}
gvr := schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantmodules"}
if name != "" {
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get module %q: %w", name, err)
}
printModules([]unstructured.Unstructured{*item})
return nil
}
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to list modules: %w", err)
}
if len(list.Items) == 0 {
printNoResources(os.Stderr, "modules")
return nil
}
printModules(list.Items)
return nil
}
func getPVCs(ctx context.Context, name string) error {
_, dynClient, err := newClients()
if err != nil {
return err
}
ns, err := getNamespace()
if err != nil {
return err
}
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaims"}
if name != "" {
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get PVC %q: %w", name, err)
}
printPVCs([]unstructured.Unstructured{*item})
return nil
}
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to list PVCs: %w", err)
}
if len(list.Items) == 0 {
printNoResources(os.Stderr, "PVCs")
return nil
}
printPVCs(list.Items)
return nil
}
func getSubResources(ctx context.Context, subType string, name string) error {
typedClient, dynClient, err := newClients()
if err != nil {
return err
}
ns, err := getNamespace()
if err != nil {
return err
}
labelSelector, err := buildSubResourceSelector(ctx, typedClient, getCmdFlags.target)
if err != nil {
return err
}
switch subType {
case "secrets":
return getFilteredSecrets(ctx, dynClient, ns, name, labelSelector)
case "services":
return getFilteredServices(ctx, dynClient, ns, name, labelSelector)
case "ingresses":
return getFilteredIngresses(ctx, dynClient, ns, name, labelSelector)
case "workloads":
return getFilteredWorkloads(ctx, dynClient, ns, name, labelSelector)
default:
return fmt.Errorf("unknown sub-resource type: %s", subType)
}
}
func buildSubResourceSelector(ctx context.Context, typedClient client.Client, target string) (string, error) {
var selectors []string
if target == "" {
selectors = append(selectors, corev1alpha1.TenantResourceLabelKey+"="+corev1alpha1.TenantResourceLabelValue)
return strings.Join(selectors, ","), nil
}
parts := strings.SplitN(target, "/", 2)
if len(parts) != 2 {
return "", fmt.Errorf("invalid target format %q, expected type/name", target)
}
targetType, targetName := parts[0], parts[1]
// Discover ApplicationDefinitions to resolve the target type
registry, err := discoverAppDefs(ctx, typedClient)
if err != nil {
return "", err
}
// Check if this is a module reference
if strings.ToLower(targetType) == "module" {
info := registry.ResolveModule(targetName)
if info == nil {
return "", fmt.Errorf("unknown module %q", targetName)
}
selectors = append(selectors,
appsv1alpha1.ApplicationKindLabel+"="+info.Kind,
appsv1alpha1.ApplicationNameLabel+"="+targetName,
corev1alpha1.TenantResourceLabelKey+"="+corev1alpha1.TenantResourceLabelValue,
)
return strings.Join(selectors, ","), nil
}
info := registry.Resolve(targetType)
if info == nil {
return "", fmt.Errorf("unknown application type %q", targetType)
}
selectors = append(selectors,
appsv1alpha1.ApplicationKindLabel+"="+info.Kind,
appsv1alpha1.ApplicationNameLabel+"="+targetName,
corev1alpha1.TenantResourceLabelKey+"="+corev1alpha1.TenantResourceLabelValue,
)
return strings.Join(selectors, ","), nil
}
func getFilteredSecrets(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "secrets", printSecrets)
}
func getFilteredServices(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "services", printServices)
}
func getFilteredIngresses(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
gvr := schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "ingresses", printIngresses)
}
func getFilteredWorkloads(ctx context.Context, dynClient dynamic.Interface, ns, name, labelSelector string) error {
gvr := schema.GroupVersionResource{Group: "cozystack.io", Version: "v1alpha1", Resource: "workloadmonitors"}
return getFilteredResources(ctx, dynClient, gvr, ns, name, labelSelector, "workloads", printWorkloads)
}
func getFilteredResources(
ctx context.Context,
dynClient dynamic.Interface,
gvr schema.GroupVersionResource,
ns, name, labelSelector string,
typeName string,
printer func([]unstructured.Unstructured),
) error {
if name != "" {
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get %s %q: %w", typeName, name, err)
}
printer([]unstructured.Unstructured{*item})
return nil
}
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil {
return fmt.Errorf("failed to list %s: %w", typeName, err)
}
if len(list.Items) == 0 {
printNoResources(os.Stderr, typeName)
return nil
}
printer(list.Items)
return nil
}
func getApplications(ctx context.Context, resourceType, name string) error {
typedClient, dynClient, err := newClients()
if err != nil {
return err
}
ns, err := getNamespace()
if err != nil {
return err
}
registry, err := discoverAppDefs(ctx, typedClient)
if err != nil {
return err
}
info := registry.Resolve(resourceType)
if info == nil {
return fmt.Errorf("unknown resource type %q\nUse 'cozyctl get --help' for available types", resourceType)
}
gvr := schema.GroupVersionResource{Group: "apps.cozystack.io", Version: "v1alpha1", Resource: info.Plural}
if name != "" {
item, err := dynClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get %s %q: %w", info.Singular, name, err)
}
printApplications([]unstructured.Unstructured{*item})
return nil
}
list, err := dynClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to list %s: %w", info.Plural, err)
}
if len(list.Items) == 0 {
printNoResources(os.Stderr, info.Plural)
return nil
}
printApplications(list.Items)
return nil
}

View File

@@ -0,0 +1,43 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"github.com/spf13/cobra"
)
var migrateCmd = &cobra.Command{
Use: "migrate <type> <name>",
Short: "Live-migrate a VirtualMachine to another node",
Long: `Live-migrate a VirtualMachine to another node using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
Args: cobra.ExactArgs(2),
RunE: runMigrate,
}
func init() {
rootCmd.AddCommand(migrateCmd)
}
func runMigrate(cmd *cobra.Command, args []string) error {
vmName, ns, err := resolveVMArgs(args)
if err != nil {
return err
}
virtctlArgs := []string{"virtctl", "migrate", vmName, "-n", ns}
return execVirtctl(virtctlArgs)
}

View File

@@ -0,0 +1,51 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var portForwardCmd = &cobra.Command{
Use: "port-forward <type/name> [ports...]",
Short: "Forward ports to a VirtualMachineInstance",
Long: `Forward ports to a VirtualMachineInstance using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
Args: cobra.MinimumNArgs(2),
RunE: runPortForward,
}
func init() {
rootCmd.AddCommand(portForwardCmd)
}
func runPortForward(cmd *cobra.Command, args []string) error {
vmName, ns, err := resolveVMArgs(args[:1])
if err != nil {
return err
}
ports := args[1:]
if len(ports) == 0 {
return fmt.Errorf("at least one port is required")
}
virtctlArgs := []string{"virtctl", "port-forward", "vmi/" + vmName, "-n", ns}
virtctlArgs = append(virtctlArgs, ports...)
return execVirtctl(virtctlArgs)
}

250
cmd/cozyctl/cmd/printer.go Normal file
View File

@@ -0,0 +1,250 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func newTabWriter() *tabwriter.Writer {
return tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
}
func printApplications(items []unstructured.Unstructured) {
w := newTabWriter()
defer w.Flush()
fmt.Fprintln(w, "NAME\tVERSION\tREADY\tSTATUS")
for _, item := range items {
name := item.GetName()
version, _, _ := unstructured.NestedString(item.Object, "appVersion")
ready, status := extractCondition(item)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, version, ready, truncate(status, 48))
}
}
func printNamespaces(items []unstructured.Unstructured) {
w := newTabWriter()
defer w.Flush()
fmt.Fprintln(w, "NAME")
for _, item := range items {
fmt.Fprintln(w, item.GetName())
}
}
func printModules(items []unstructured.Unstructured) {
w := newTabWriter()
defer w.Flush()
fmt.Fprintln(w, "NAME\tVERSION\tREADY\tSTATUS")
for _, item := range items {
name := item.GetName()
version, _, _ := unstructured.NestedString(item.Object, "appVersion")
ready, status := extractCondition(item)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, version, ready, truncate(status, 48))
}
}
func printPVCs(items []unstructured.Unstructured) {
w := newTabWriter()
defer w.Flush()
fmt.Fprintln(w, "NAME\tSTATUS\tVOLUME\tCAPACITY\tSTORAGECLASS")
for _, item := range items {
name := item.GetName()
phase, _, _ := unstructured.NestedString(item.Object, "status", "phase")
volume, _, _ := unstructured.NestedString(item.Object, "spec", "volumeName")
capacity := ""
if cap, ok, _ := unstructured.NestedStringMap(item.Object, "status", "capacity"); ok {
capacity = cap["storage"]
}
sc, _, _ := unstructured.NestedString(item.Object, "spec", "storageClassName")
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", name, phase, volume, capacity, sc)
}
}
func printSecrets(items []unstructured.Unstructured) {
w := newTabWriter()
defer w.Flush()
fmt.Fprintln(w, "NAME\tTYPE\tDATA")
for _, item := range items {
name := item.GetName()
secretType, _, _ := unstructured.NestedString(item.Object, "type")
data, _, _ := unstructured.NestedMap(item.Object, "data")
fmt.Fprintf(w, "%s\t%s\t%d\n", name, secretType, len(data))
}
}
func printServices(items []unstructured.Unstructured) {
w := newTabWriter()
defer w.Flush()
fmt.Fprintln(w, "NAME\tTYPE\tCLUSTER-IP\tEXTERNAL-IP\tPORTS")
for _, item := range items {
name := item.GetName()
svcType, _, _ := unstructured.NestedString(item.Object, "spec", "type")
clusterIP, _, _ := unstructured.NestedString(item.Object, "spec", "clusterIP")
externalIP := "<none>"
if lbIngress, ok, _ := unstructured.NestedSlice(item.Object, "status", "loadBalancer", "ingress"); ok && len(lbIngress) > 0 {
var ips []string
for _, ingress := range lbIngress {
if m, ok := ingress.(map[string]interface{}); ok {
if ip, ok := m["ip"].(string); ok {
ips = append(ips, ip)
} else if hostname, ok := m["hostname"].(string); ok {
ips = append(ips, hostname)
}
}
}
if len(ips) > 0 {
externalIP = strings.Join(ips, ",")
}
}
ports := formatPorts(item)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", name, svcType, clusterIP, externalIP, ports)
}
}
func printIngresses(items []unstructured.Unstructured) {
w := newTabWriter()
defer w.Flush()
fmt.Fprintln(w, "NAME\tCLASS\tHOSTS\tADDRESS")
for _, item := range items {
name := item.GetName()
class, _, _ := unstructured.NestedString(item.Object, "spec", "ingressClassName")
var hosts []string
if rules, ok, _ := unstructured.NestedSlice(item.Object, "spec", "rules"); ok {
for _, rule := range rules {
if m, ok := rule.(map[string]interface{}); ok {
if host, ok := m["host"].(string); ok {
hosts = append(hosts, host)
}
}
}
}
hostsStr := "<none>"
if len(hosts) > 0 {
hostsStr = strings.Join(hosts, ",")
}
address := ""
if lbIngress, ok, _ := unstructured.NestedSlice(item.Object, "status", "loadBalancer", "ingress"); ok && len(lbIngress) > 0 {
var addrs []string
for _, ingress := range lbIngress {
if m, ok := ingress.(map[string]interface{}); ok {
if ip, ok := m["ip"].(string); ok {
addrs = append(addrs, ip)
} else if hostname, ok := m["hostname"].(string); ok {
addrs = append(addrs, hostname)
}
}
}
address = strings.Join(addrs, ",")
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, class, hostsStr, address)
}
}
func printWorkloads(items []unstructured.Unstructured) {
w := newTabWriter()
defer w.Flush()
fmt.Fprintln(w, "NAME\tKIND\tTYPE\tVERSION\tAVAILABLE\tOBSERVED\tOPERATIONAL")
for _, item := range items {
name := item.GetName()
kind, _, _ := unstructured.NestedString(item.Object, "spec", "kind")
wType, _, _ := unstructured.NestedString(item.Object, "spec", "type")
version, _, _ := unstructured.NestedString(item.Object, "spec", "version")
available, _, _ := unstructured.NestedInt64(item.Object, "status", "availableReplicas")
observed, _, _ := unstructured.NestedInt64(item.Object, "status", "observedReplicas")
operational, ok, _ := unstructured.NestedBool(item.Object, "status", "operational")
opStr := ""
if ok {
opStr = fmt.Sprintf("%t", operational)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%d\t%s\n", name, kind, wType, version, available, observed, opStr)
}
}
func printNoResources(w io.Writer, resourceType string) {
fmt.Fprintf(w, "No %s found\n", resourceType)
}
func extractCondition(item unstructured.Unstructured) (string, string) {
conditions, ok, _ := unstructured.NestedSlice(item.Object, "status", "conditions")
if !ok {
return "Unknown", ""
}
for _, c := range conditions {
cond, ok := c.(map[string]interface{})
if !ok {
continue
}
if cond["type"] == "Ready" {
ready, _ := cond["status"].(string)
message, _ := cond["message"].(string)
return ready, message
}
}
return "Unknown", ""
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen-3] + "..."
}
func formatPorts(item unstructured.Unstructured) string {
ports, ok, _ := unstructured.NestedSlice(item.Object, "spec", "ports")
if !ok || len(ports) == 0 {
return "<none>"
}
var parts []string
for _, p := range ports {
port, ok := p.(map[string]interface{})
if !ok {
continue
}
portNum, _, _ := unstructured.NestedInt64(port, "port")
protocol, _, _ := unstructured.NestedString(port, "protocol")
if protocol == "" {
protocol = "TCP"
}
nodePort, _, _ := unstructured.NestedInt64(port, "nodePort")
if nodePort > 0 {
parts = append(parts, fmt.Sprintf("%d:%d/%s", portNum, nodePort, protocol))
} else {
parts = append(parts, fmt.Sprintf("%d/%s", portNum, protocol))
}
}
return strings.Join(parts, ",")
}

57
cmd/cozyctl/cmd/root.go Normal file
View File

@@ -0,0 +1,57 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// Version is set at build time via -ldflags.
var Version = "dev"
var globalFlags struct {
kubeconfig string
context string
namespace string
}
var rootCmd = &cobra.Command{
Use: "cozyctl",
Short: "A CLI for managing Cozystack applications",
SilenceErrors: true,
SilenceUsage: true,
DisableAutoGenTag: true,
}
// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() error {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
return err
}
return nil
}
func init() {
rootCmd.Version = Version
rootCmd.PersistentFlags().StringVar(&globalFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file")
rootCmd.PersistentFlags().StringVar(&globalFlags.context, "context", "", "Kubernetes context to use")
rootCmd.PersistentFlags().StringVarP(&globalFlags.namespace, "namespace", "n", "", "Kubernetes namespace")
}

106
cmd/cozyctl/cmd/vm.go Normal file
View File

@@ -0,0 +1,106 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
)
// vmKindPrefix maps application Kind to the release prefix used by KubeVirt VMs.
func vmKindPrefix(kind string) (string, bool) {
switch kind {
case "VirtualMachine":
return "virtual-machine", true
case "VMInstance":
return "vm-instance", true
default:
return "", false
}
}
// resolveVMArgs takes CLI args (type, name or type/name), resolves the application type
// via discovery, validates it's a VM kind, and returns the full VM name and namespace.
func resolveVMArgs(args []string) (string, string, error) {
var resourceType, resourceName string
if len(args) == 1 {
// type/name format
parts := strings.SplitN(args[0], "/", 2)
if len(parts) != 2 {
return "", "", fmt.Errorf("expected type/name format, got %q", args[0])
}
resourceType, resourceName = parts[0], parts[1]
} else {
resourceType = args[0]
resourceName = args[1]
}
ctx := context.Background()
typedClient, _, err := newClients()
if err != nil {
return "", "", err
}
registry, err := discoverAppDefs(ctx, typedClient)
if err != nil {
return "", "", err
}
info := registry.Resolve(resourceType)
if info == nil {
return "", "", fmt.Errorf("unknown application type %q", resourceType)
}
prefix, ok := vmKindPrefix(info.Kind)
if !ok {
return "", "", fmt.Errorf("resource type %q (Kind=%s) is not a VirtualMachine or VMInstance", resourceType, info.Kind)
}
ns, err := getNamespace()
if err != nil {
return "", "", err
}
vmName := prefix + "-" + resourceName
return vmName, ns, nil
}
// execVirtctl replaces the current process with virtctl.
func execVirtctl(args []string) error {
virtctlPath, err := exec.LookPath("virtctl")
if err != nil {
return fmt.Errorf("virtctl not found in PATH: %w", err)
}
// Append kubeconfig/context flags if set
if globalFlags.kubeconfig != "" {
args = append(args, "--kubeconfig", globalFlags.kubeconfig)
}
if globalFlags.context != "" {
args = append(args, "--context", globalFlags.context)
}
if err := syscall.Exec(virtctlPath, args, os.Environ()); err != nil {
return fmt.Errorf("failed to exec virtctl: %w", err)
}
return nil
}

43
cmd/cozyctl/cmd/vnc.go Normal file
View File

@@ -0,0 +1,43 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"github.com/spf13/cobra"
)
var vncCmd = &cobra.Command{
Use: "vnc <type> <name>",
Short: "Open a VNC connection to a VirtualMachine",
Long: `Open a VNC connection to a VirtualMachine using virtctl. Only valid for VirtualMachine or VMInstance kinds.`,
Args: cobra.ExactArgs(2),
RunE: runVNC,
}
func init() {
rootCmd.AddCommand(vncCmd)
}
func runVNC(cmd *cobra.Command, args []string) error {
vmName, ns, err := resolveVMArgs(args)
if err != nil {
return err
}
virtctlArgs := []string{"virtctl", "vnc", vmName, "-n", ns}
return execVirtctl(virtctlArgs)
}

29
cmd/cozyctl/main.go Normal file
View File

@@ -0,0 +1,29 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"github.com/cozystack/cozystack/cmd/cozyctl/cmd"
)
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}

View File

@@ -108,7 +108,7 @@ func main() {
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
"Interval between telemetry data collection (e.g. 15m, 1h)")
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-platform", "Name for the generated platform source resource and PackageSource")
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
flag.StringVar(&cozyValuesSecretName, "cozy-values-secret-name", "cozystack-values", "The name of the secret containing cluster-wide configuration values.")
flag.StringVar(&cozyValuesSecretNamespace, "cozy-values-secret-namespace", "cozy-system", "The namespace of the secret containing cluster-wide configuration values.")
@@ -224,6 +224,29 @@ func main() {
}
}
// Create platform PackageSource when CRDs are managed by the operator and
// a platform source URL is configured. Without a URL there is no Flux source
// resource to reference, so creating a PackageSource would leave a dangling SourceRef.
if installCRDs && platformSourceURL != "" {
sourceRefKind := "OCIRepository"
sourceType, _, err := parsePlatformSourceURL(platformSourceURL)
if err != nil {
setupLog.Error(err, "failed to parse platform source URL for PackageSource")
os.Exit(1)
}
if sourceType == "git" {
sourceRefKind = "GitRepository"
}
setupLog.Info("Creating platform PackageSource", "platformSourceName", platformSourceName)
psCtx, psCancel := context.WithTimeout(mgrCtx, 2*time.Minute)
defer psCancel()
if err := installPlatformPackageSource(psCtx, directClient, platformSourceName, sourceRefKind); err != nil {
setupLog.Error(err, "failed to create platform PackageSource")
os.Exit(1)
}
setupLog.Info("Platform PackageSource creation completed successfully")
}
// Setup PackageSource reconciler
if err := (&operator.PackageSourceReconciler{
Client: mgr.GetClient(),
@@ -552,3 +575,79 @@ func generateGitRepository(name, repoURL string, refMap map[string]string) (*sou
return obj, nil
}
// installPlatformPackageSource creates the platform PackageSource resource
// that references the Flux source resource (OCIRepository or GitRepository).
//
// The variant list is intentionally hardcoded here. These are platform-defined
// deployment profiles (not user-extensible), matching what was previously in
// the Helm template. Changes require a new operator build and release.
func installPlatformPackageSource(ctx context.Context, k8sClient client.Client, platformSourceName, sourceRefKind string) error {
logger := log.FromContext(ctx)
packageSourceName := "cozystack." + platformSourceName
ps := &cozyv1alpha1.PackageSource{
TypeMeta: metav1.TypeMeta{
APIVersion: cozyv1alpha1.GroupVersion.String(),
Kind: "PackageSource",
},
ObjectMeta: metav1.ObjectMeta{
Name: packageSourceName,
Annotations: map[string]string{
"operator.cozystack.io/skip-cozystack-values": "true",
},
},
Spec: cozyv1alpha1.PackageSourceSpec{
SourceRef: &cozyv1alpha1.PackageSourceRef{
Kind: sourceRefKind,
Name: platformSourceName,
Namespace: "cozy-system",
Path: "/",
},
},
}
variantData := []struct {
name string
valuesFiles []string
}{
{"default", []string{"values.yaml"}},
{"isp-full", []string{"values.yaml", "values-isp-full.yaml"}},
{"isp-hosted", []string{"values.yaml", "values-isp-hosted.yaml"}},
{"isp-full-generic", []string{"values.yaml", "values-isp-full-generic.yaml"}},
}
variants := make([]cozyv1alpha1.Variant, len(variantData))
for i, v := range variantData {
variants[i] = cozyv1alpha1.Variant{
Name: v.name,
Components: []cozyv1alpha1.Component{
{
Name: "platform",
Path: "core/platform",
Install: &cozyv1alpha1.ComponentInstall{
Namespace: "cozy-system",
ReleaseName: "cozystack-platform",
},
ValuesFiles: v.valuesFiles,
},
},
}
}
ps.Spec.Variants = variants
logger.Info("Applying platform PackageSource", "name", packageSourceName)
patchOptions := &client.PatchOptions{
FieldManager: "cozystack-operator",
Force: func() *bool { b := true; return &b }(),
}
if err := k8sClient.Patch(ctx, ps, client.Apply, patchOptions); err != nil {
return fmt.Errorf("failed to apply PackageSource %s: %w", packageSourceName, err)
}
logger.Info("Applied platform PackageSource", "name", packageSourceName)
return nil
}

View File

@@ -0,0 +1,574 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"testing"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func newTestScheme() *runtime.Scheme {
s := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(s)
return s
}
func TestInstallPlatformPackageSource_Creates(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
// Verify name
if ps.Name != "cozystack.cozystack-platform" {
t.Errorf("expected name %q, got %q", "cozystack.cozystack-platform", ps.Name)
}
// Verify annotation
if ps.Annotations["operator.cozystack.io/skip-cozystack-values"] != "true" {
t.Errorf("expected skip-cozystack-values annotation to be 'true', got %q", ps.Annotations["operator.cozystack.io/skip-cozystack-values"])
}
// Verify sourceRef
if ps.Spec.SourceRef == nil {
t.Fatal("expected SourceRef to be set")
}
if ps.Spec.SourceRef.Kind != "OCIRepository" {
t.Errorf("expected sourceRef.kind %q, got %q", "OCIRepository", ps.Spec.SourceRef.Kind)
}
if ps.Spec.SourceRef.Name != "cozystack-platform" {
t.Errorf("expected sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
}
if ps.Spec.SourceRef.Namespace != "cozy-system" {
t.Errorf("expected sourceRef.namespace %q, got %q", "cozy-system", ps.Spec.SourceRef.Namespace)
}
if ps.Spec.SourceRef.Path != "/" {
t.Errorf("expected sourceRef.path %q, got %q", "/", ps.Spec.SourceRef.Path)
}
// Verify variants
expectedVariants := []string{"default", "isp-full", "isp-hosted", "isp-full-generic"}
if len(ps.Spec.Variants) != len(expectedVariants) {
t.Fatalf("expected %d variants, got %d", len(expectedVariants), len(ps.Spec.Variants))
}
for i, name := range expectedVariants {
if ps.Spec.Variants[i].Name != name {
t.Errorf("expected variant[%d].name %q, got %q", i, name, ps.Spec.Variants[i].Name)
}
if len(ps.Spec.Variants[i].Components) != 1 {
t.Errorf("expected variant[%d] to have 1 component, got %d", i, len(ps.Spec.Variants[i].Components))
}
}
}
func TestInstallPlatformPackageSource_Updates(t *testing.T) {
s := newTestScheme()
existing := &cozyv1alpha1.PackageSource{
ObjectMeta: metav1.ObjectMeta{
Name: "cozystack.cozystack-platform",
ResourceVersion: "1",
Labels: map[string]string{
"custom-label": "should-be-preserved",
},
},
Spec: cozyv1alpha1.PackageSourceSpec{
SourceRef: &cozyv1alpha1.PackageSourceRef{
Kind: "OCIRepository",
Name: "old-name",
Namespace: "cozy-system",
},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(existing).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
// Verify sourceRef was updated
if ps.Spec.SourceRef.Name != "cozystack-platform" {
t.Errorf("expected updated sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
}
// Verify all 4 variants are present after update
if len(ps.Spec.Variants) != 4 {
t.Errorf("expected 4 variants after update, got %d", len(ps.Spec.Variants))
}
// Verify that labels set by other controllers are preserved (SSA does not overwrite unmanaged fields)
if ps.Labels["custom-label"] != "should-be-preserved" {
t.Errorf("expected custom-label to be preserved, got %q", ps.Labels["custom-label"])
}
}
func TestParsePlatformSourceURL(t *testing.T) {
tests := []struct {
name string
url string
wantType string
wantURL string
wantErr bool
}{
{
name: "OCI URL",
url: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
wantType: "oci",
wantURL: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
},
{
name: "HTTPS URL",
url: "https://github.com/cozystack/cozystack",
wantType: "git",
wantURL: "https://github.com/cozystack/cozystack",
},
{
name: "SSH URL",
url: "ssh://git@github.com/cozystack/cozystack",
wantType: "git",
wantURL: "ssh://git@github.com/cozystack/cozystack",
},
{
name: "empty URL",
url: "",
wantErr: true,
},
{
name: "unsupported scheme",
url: "ftp://example.com/repo",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sourceType, repoURL, err := parsePlatformSourceURL(tt.url)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error for URL %q, got nil", tt.url)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if sourceType != tt.wantType {
t.Errorf("expected type %q, got %q", tt.wantType, sourceType)
}
if repoURL != tt.wantURL {
t.Errorf("expected URL %q, got %q", tt.wantURL, repoURL)
}
})
}
}
func TestInstallPlatformPackageSource_VariantValuesFiles(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
expectedValuesFiles := map[string][]string{
"default": {"values.yaml"},
"isp-full": {"values.yaml", "values-isp-full.yaml"},
"isp-hosted": {"values.yaml", "values-isp-hosted.yaml"},
"isp-full-generic": {"values.yaml", "values-isp-full-generic.yaml"},
}
for _, v := range ps.Spec.Variants {
expected, ok := expectedValuesFiles[v.Name]
if !ok {
t.Errorf("unexpected variant %q", v.Name)
continue
}
if len(v.Components) != 1 {
t.Errorf("variant %q: expected 1 component, got %d", v.Name, len(v.Components))
continue
}
comp := v.Components[0]
if comp.Name != "platform" {
t.Errorf("variant %q: expected component name %q, got %q", v.Name, "platform", comp.Name)
}
if comp.Path != "core/platform" {
t.Errorf("variant %q: expected component path %q, got %q", v.Name, "core/platform", comp.Path)
}
if comp.Install == nil {
t.Errorf("variant %q: expected Install to be set", v.Name)
} else {
if comp.Install.Namespace != "cozy-system" {
t.Errorf("variant %q: expected install namespace %q, got %q", v.Name, "cozy-system", comp.Install.Namespace)
}
if comp.Install.ReleaseName != "cozystack-platform" {
t.Errorf("variant %q: expected install releaseName %q, got %q", v.Name, "cozystack-platform", comp.Install.ReleaseName)
}
}
if len(comp.ValuesFiles) != len(expected) {
t.Errorf("variant %q: expected %d valuesFiles, got %d", v.Name, len(expected), len(comp.ValuesFiles))
continue
}
for i, f := range expected {
if comp.ValuesFiles[i] != f {
t.Errorf("variant %q: expected valuesFiles[%d] %q, got %q", v.Name, i, f, comp.ValuesFiles[i])
}
}
}
}
func TestInstallPlatformPackageSource_CustomName(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "custom-source", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.custom-source"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
if ps.Name != "cozystack.custom-source" {
t.Errorf("expected name %q, got %q", "cozystack.custom-source", ps.Name)
}
if ps.Spec.SourceRef.Name != "custom-source" {
t.Errorf("expected sourceRef.name %q, got %q", "custom-source", ps.Spec.SourceRef.Name)
}
}
func TestInstallPlatformPackageSource_GitRepository(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "my-source", "GitRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.my-source"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
if ps.Spec.SourceRef.Kind != "GitRepository" {
t.Errorf("expected sourceRef.kind %q, got %q", "GitRepository", ps.Spec.SourceRef.Kind)
}
if ps.Spec.SourceRef.Name != "my-source" {
t.Errorf("expected sourceRef.name %q, got %q", "my-source", ps.Spec.SourceRef.Name)
}
}
func TestParseRefSpec(t *testing.T) {
tests := []struct {
name string
input string
want map[string]string
wantErr bool
}{
{
name: "empty string",
input: "",
want: map[string]string{},
},
{
name: "single key-value",
input: "tag=v1.0",
want: map[string]string{"tag": "v1.0"},
},
{
name: "multiple key-values",
input: "digest=sha256:abc123,tag=v1.0",
want: map[string]string{"digest": "sha256:abc123", "tag": "v1.0"},
},
{
name: "whitespace around pairs",
input: " tag=v1.0 , branch=main ",
want: map[string]string{"tag": "v1.0", "branch": "main"},
},
{
name: "equals sign in value",
input: "digest=sha256:abc=123",
want: map[string]string{"digest": "sha256:abc=123"},
},
{
name: "missing equals sign",
input: "tag",
wantErr: true,
},
{
name: "empty key",
input: "=value",
wantErr: true,
},
{
name: "empty value",
input: "tag=",
wantErr: true,
},
{
name: "trailing comma",
input: "tag=v1.0,",
want: map[string]string{"tag": "v1.0"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseRefSpec(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error for input %q, got nil", tt.input)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got) != len(tt.want) {
t.Fatalf("expected %d entries, got %d: %v", len(tt.want), len(got), got)
}
for k, v := range tt.want {
if got[k] != v {
t.Errorf("expected %q=%q, got %q=%q", k, v, k, got[k])
}
}
})
}
}
func TestValidateOCIRef(t *testing.T) {
tests := []struct {
name string
refMap map[string]string
wantErr bool
}{
{
name: "valid tag",
refMap: map[string]string{"tag": "v1.0"},
},
{
name: "valid digest",
refMap: map[string]string{"digest": "sha256:abc123def456"},
},
{
name: "valid semver",
refMap: map[string]string{"semver": ">=1.0.0"},
},
{
name: "multiple valid keys",
refMap: map[string]string{"tag": "v1.0", "digest": "sha256:abc"},
},
{
name: "empty map",
refMap: map[string]string{},
},
{
name: "invalid key",
refMap: map[string]string{"branch": "main"},
wantErr: true,
},
{
name: "invalid digest format",
refMap: map[string]string{"digest": "md5:abc"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateOCIRef(tt.refMap)
if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestValidateGitRef(t *testing.T) {
tests := []struct {
name string
refMap map[string]string
wantErr bool
}{
{
name: "valid branch",
refMap: map[string]string{"branch": "main"},
},
{
name: "valid commit",
refMap: map[string]string{"commit": "abc1234"},
},
{
name: "valid tag and branch",
refMap: map[string]string{"tag": "v1.0", "branch": "release"},
},
{
name: "empty map",
refMap: map[string]string{},
},
{
name: "invalid key",
refMap: map[string]string{"digest": "sha256:abc"},
wantErr: true,
},
{
name: "commit too short",
refMap: map[string]string{"commit": "abc"},
wantErr: true,
},
{
name: "commit not hex",
refMap: map[string]string{"commit": "zzzzzzz"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateGitRef(tt.refMap)
if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestGenerateOCIRepository(t *testing.T) {
refMap := map[string]string{"tag": "v1.0", "digest": "sha256:abc123"}
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", refMap)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Name != "my-repo" {
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
}
if obj.Namespace != "cozy-system" {
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
}
if obj.Spec.URL != "oci://registry.example.com/repo" {
t.Errorf("expected URL %q, got %q", "oci://registry.example.com/repo", obj.Spec.URL)
}
if obj.Spec.Reference == nil {
t.Fatal("expected Reference to be set")
}
if obj.Spec.Reference.Tag != "v1.0" {
t.Errorf("expected tag %q, got %q", "v1.0", obj.Spec.Reference.Tag)
}
if obj.Spec.Reference.Digest != "sha256:abc123" {
t.Errorf("expected digest %q, got %q", "sha256:abc123", obj.Spec.Reference.Digest)
}
}
func TestGenerateOCIRepository_NoRef(t *testing.T) {
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.Reference != nil {
t.Error("expected Reference to be nil for empty refMap")
}
}
func TestGenerateOCIRepository_InvalidRef(t *testing.T) {
_, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{"branch": "main"})
if err == nil {
t.Fatal("expected error for invalid OCI ref key, got nil")
}
}
func TestGenerateGitRepository(t *testing.T) {
refMap := map[string]string{"branch": "main", "commit": "abc1234def5678"}
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", refMap)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Name != "my-repo" {
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
}
if obj.Namespace != "cozy-system" {
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
}
if obj.Spec.URL != "https://github.com/user/repo" {
t.Errorf("expected URL %q, got %q", "https://github.com/user/repo", obj.Spec.URL)
}
if obj.Spec.Reference == nil {
t.Fatal("expected Reference to be set")
}
if obj.Spec.Reference.Branch != "main" {
t.Errorf("expected branch %q, got %q", "main", obj.Spec.Reference.Branch)
}
if obj.Spec.Reference.Commit != "abc1234def5678" {
t.Errorf("expected commit %q, got %q", "abc1234def5678", obj.Spec.Reference.Commit)
}
}
func TestGenerateGitRepository_NoRef(t *testing.T) {
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.Reference != nil {
t.Error("expected Reference to be nil for empty refMap")
}
}
func TestGenerateGitRepository_InvalidRef(t *testing.T) {
_, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{"digest": "sha256:abc"})
if err == nil {
t.Fatal("expected error for invalid Git ref key, got nil")
}
}

View File

@@ -86,14 +86,18 @@ EOF
yq -i ".clusters[0].cluster.server = \"https://localhost:${port}\"" "tenantkubeconfig-${test_name}"
# Set up port forwarding to the Kubernetes API server for a 200 second timeout
# Kill any stale port-forward on this port from a previous retry
pkill -f "port-forward.*${port}:" 2>/dev/null || true
sleep 1
# Set up port forwarding to the Kubernetes API server
bash -c 'timeout 500s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
# Verify the Kubernetes version matches what we expect (retry for up to 20 seconds)
timeout 20 sh -ec 'until kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' version 2>/dev/null | grep -Fq "Server Version: ${k8s_version}"; do sleep 5; done'
# Wait for the nodes to be ready (timeout after 2 minutes)
timeout 3m bash -c '
until [ "$(kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -eq 2 ]; do
# Wait for at least 2 nodes to join (timeout after 8 minutes)
timeout 8m bash -c '
until [ "$(kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -ge 2 ]; do
sleep 2
done
'

View File

@@ -8,7 +8,7 @@
}
@test "Install Cozystack" {
# Install cozy-installer chart (CRDs from crds/ are applied automatically)
# Install cozy-installer chart (operator installs CRDs on startup via --install-crds)
helm upgrade installer packages/core/installer \
--install \
--namespace cozy-system \
@@ -19,6 +19,14 @@
# Verify the operator deployment is available
kubectl wait deployment/cozystack-operator -n cozy-system --timeout=1m --for=condition=Available
# Wait for operator to install CRDs (happens at startup before reconcile loop).
# kubectl wait fails immediately if the CRD does not exist yet, so poll until it appears first.
timeout 120 sh -ec 'until kubectl wait crd/packages.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
timeout 120 sh -ec 'until kubectl wait crd/packagesources.cozystack.io --for=condition=Established --timeout=10s 2>/dev/null; do sleep 2; done'
# Wait for operator to create the platform PackageSource
timeout 120 sh -ec 'until kubectl get packagesource cozystack.cozystack-platform >/dev/null 2>&1; do sleep 2; done'
# Create platform Package with isp-full variant
kubectl apply -f - <<EOF
apiVersion: cozystack.io/v1alpha1

View File

@@ -53,6 +53,39 @@ KEYCLOAK_REDIRECTS=$(echo "$COZYSTACK_CM" | jq -r '.data["extra-keycloak-redirec
TELEMETRY_ENABLED=$(echo "$COZYSTACK_CM" | jq -r '.data["telemetry-enabled"] // "true"')
BUNDLE_NAME=$(echo "$COZYSTACK_CM" | jq -r '.data["bundle-name"] // "paas-full"')
# Certificate issuer configuration (old undocumented field: clusterissuer)
OLD_CLUSTER_ISSUER=$(echo "$COZYSTACK_CM" | jq -r '.data["clusterissuer"] // ""')
# Convert old clusterissuer value to new solver/issuerName fields
SOLVER=""
ISSUER_NAME=""
case "$OLD_CLUSTER_ISSUER" in
cloudflare)
SOLVER="dns01"
ISSUER_NAME="letsencrypt-prod"
;;
http01)
SOLVER="http01"
ISSUER_NAME="letsencrypt-prod"
;;
"")
# Field not set; omit from Package so chart defaults apply
;;
*)
# Unrecognised value — treat as custom ClusterIssuer name with no solver override
ISSUER_NAME="$OLD_CLUSTER_ISSUER"
;;
esac
# Build certificates YAML block (empty string when no override needed)
if [ -n "$SOLVER" ] || [ -n "$ISSUER_NAME" ]; then
CERTIFICATES_SECTION=" certificates:
solver: \"${SOLVER}\"
issuerName: \"${ISSUER_NAME}\""
else
CERTIFICATES_SECTION=""
fi
# Network configuration
POD_CIDR=$(echo "$COZYSTACK_CM" | jq -r '.data["ipv4-pod-cidr"] // "10.244.0.0/16"')
POD_GATEWAY=$(echo "$COZYSTACK_CM" | jq -r '.data["ipv4-pod-gateway"] // "10.244.0.1"')
@@ -110,6 +143,8 @@ echo " OIDC Enabled: $OIDC_ENABLED"
echo " Bundle Name: $BUNDLE_NAME"
echo " System Enabled: $SYSTEM_ENABLED"
echo " System Type: $SYSTEM_TYPE"
echo " Certificate Solver: ${SOLVER:-http01 (default)}"
echo " Issuer Name: ${ISSUER_NAME:-letsencrypt-prod (default)}"
echo ""
# Generate Package YAML
@@ -144,6 +179,7 @@ spec:
host: "$ROOT_HOST"
apiServerEndpoint: "$API_SERVER_ENDPOINT"
externalIPs: $EXTERNAL_IPS
${CERTIFICATES_SECTION}
authentication:
oidc:
enabled: $OIDC_ENABLED

View File

@@ -24,8 +24,7 @@ API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-ru
UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}"
CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4"
TMPDIR=$(mktemp -d)
OPERATOR_CRDDIR=packages/core/installer/crds
OPERATOR_EMBEDDIR=internal/crdinstall/manifests
OPERATOR_CRDDIR=internal/crdinstall/manifests
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions
COZY_RD_CRDDIR=packages/system/application-definition-crd/definition
BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions
@@ -74,9 +73,6 @@ $CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:arti
mv ${TMPDIR}/cozystack.io_packages.yaml ${OPERATOR_CRDDIR}/cozystack.io_packages.yaml
mv ${TMPDIR}/cozystack.io_packagesources.yaml ${OPERATOR_CRDDIR}/cozystack.io_packagesources.yaml
cp ${OPERATOR_CRDDIR}/cozystack.io_packages.yaml ${OPERATOR_EMBEDDIR}/cozystack.io_packages.yaml
cp ${OPERATOR_CRDDIR}/cozystack.io_packagesources.yaml ${OPERATOR_EMBEDDIR}/cozystack.io_packagesources.yaml
mv ${TMPDIR}/cozystack.io_applicationdefinitions.yaml \
${COZY_RD_CRDDIR}/cozystack.io_applicationdefinitions.yaml

View File

@@ -46,8 +46,11 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph
}
}
// Build schema with multilineString for string fields without enum
// Parse OpenAPI schema once for reuse
l := log.FromContext(ctx)
openAPIProps := parseOpenAPIProperties(crd.Spec.Application.OpenAPISchema)
// Build schema with multilineString for string fields without enum
schema, err := buildMultilineStringSchema(crd.Spec.Application.OpenAPISchema)
if err != nil {
// If schema parsing fails, log the error and use an empty schema
@@ -55,6 +58,9 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph
schema = map[string]any{}
}
// Override specific fields with API-backed dropdowns (listInput type)
applyListInputOverrides(schema, kind, openAPIProps)
spec := map[string]any{
"customizationId": customizationID,
"hidden": hidden,
@@ -176,6 +182,101 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
return schema, nil
}
// applyListInputOverrides injects listInput type overrides into the schema
// for fields that should be rendered as API-backed dropdowns in the dashboard.
// openAPIProps are the parsed top-level properties from the OpenAPI schema.
func applyListInputOverrides(schema map[string]any, kind string, openAPIProps map[string]any) {
switch kind {
case "VMInstance":
specProps := ensureSchemaPath(schema, "spec")
field := map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/instancetype.kubevirt.io/v1beta1/virtualmachineclusterinstancetypes",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
},
}
if prop, _ := openAPIProps["instanceType"].(map[string]any); prop != nil {
if def := prop["default"]; def != nil {
field["default"] = def
}
}
specProps["instanceType"] = field
// Override disks[].name to be an API-backed dropdown listing VMDisk resources
disksItemProps := ensureArrayItemProps(specProps, "disks")
disksItemProps["name"] = map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
},
}
}
}
// ensureArrayItemProps ensures that parentProps[fieldName].items.properties exists
// and returns the items properties map. Used for overriding fields inside array items.
func ensureArrayItemProps(parentProps map[string]any, fieldName string) map[string]any {
field, ok := parentProps[fieldName].(map[string]any)
if !ok {
field = map[string]any{}
parentProps[fieldName] = field
}
items, ok := field["items"].(map[string]any)
if !ok {
items = map[string]any{}
field["items"] = items
}
props, ok := items["properties"].(map[string]any)
if !ok {
props = map[string]any{}
items["properties"] = props
}
return props
}
// parseOpenAPIProperties parses the top-level properties from an OpenAPI schema JSON string.
func parseOpenAPIProperties(openAPISchema string) map[string]any {
if openAPISchema == "" {
return nil
}
var root map[string]any
if err := json.Unmarshal([]byte(openAPISchema), &root); err != nil {
return nil
}
props, _ := root["properties"].(map[string]any)
return props
}
// ensureSchemaPath ensures the nested properties structure exists in a schema
// and returns the innermost properties map.
// e.g. ensureSchemaPath(schema, "spec") returns schema["properties"]["spec"]["properties"]
func ensureSchemaPath(schema map[string]any, segments ...string) map[string]any {
current := schema
for _, seg := range segments {
props, ok := current["properties"].(map[string]any)
if !ok {
props = map[string]any{}
current["properties"] = props
}
child, ok := props[seg].(map[string]any)
if !ok {
child = map[string]any{}
props[seg] = child
}
current = child
}
props, ok := current["properties"].(map[string]any)
if !ok {
props = map[string]any{}
current["properties"] = props
}
return props
}
// processSpecProperties recursively processes spec properties and adds multilineString type
// for string fields without enum
func processSpecProperties(props map[string]any, schemaProps map[string]any) {

View File

@@ -169,3 +169,231 @@ func TestBuildMultilineStringSchemaInvalidJSON(t *testing.T) {
t.Errorf("Expected nil schema for invalid JSON, got %v", schema)
}
}
func TestApplyListInputOverrides_VMInstance(t *testing.T) {
openAPIProps := map[string]any{
"instanceType": map[string]any{"type": "string", "default": "u1.medium"},
}
schema := map[string]any{}
applyListInputOverrides(schema, "VMInstance", openAPIProps)
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
instanceType, ok := specProps["instanceType"].(map[string]any)
if !ok {
t.Fatal("instanceType not found in schema.properties.spec.properties")
}
if instanceType["type"] != "listInput" {
t.Errorf("expected type listInput, got %v", instanceType["type"])
}
if instanceType["default"] != "u1.medium" {
t.Errorf("expected default u1.medium, got %v", instanceType["default"])
}
customProps, ok := instanceType["customProps"].(map[string]any)
if !ok {
t.Fatal("customProps not found")
}
expectedURI := "/api/clusters/{cluster}/k8s/apis/instancetype.kubevirt.io/v1beta1/virtualmachineclusterinstancetypes"
if customProps["valueUri"] != expectedURI {
t.Errorf("expected valueUri %s, got %v", expectedURI, customProps["valueUri"])
}
// Check disks[].name is a listInput
disks, ok := specProps["disks"].(map[string]any)
if !ok {
t.Fatal("disks not found in schema.properties.spec.properties")
}
items, ok := disks["items"].(map[string]any)
if !ok {
t.Fatal("disks.items not found")
}
itemProps, ok := items["properties"].(map[string]any)
if !ok {
t.Fatal("disks.items.properties not found")
}
diskName, ok := itemProps["name"].(map[string]any)
if !ok {
t.Fatal("disks.items.properties.name not found")
}
if diskName["type"] != "listInput" {
t.Errorf("expected disks name type listInput, got %v", diskName["type"])
}
diskCustomProps, ok := diskName["customProps"].(map[string]any)
if !ok {
t.Fatal("disks name customProps not found")
}
expectedDiskURI := "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks"
if diskCustomProps["valueUri"] != expectedDiskURI {
t.Errorf("expected disks valueUri %s, got %v", expectedDiskURI, diskCustomProps["valueUri"])
}
}
func TestApplyListInputOverrides_UnknownKind(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, "SomeOtherKind", map[string]any{})
if len(schema) != 0 {
t.Errorf("expected empty schema for unknown kind, got %v", schema)
}
}
func TestApplyListInputOverrides_NoDefault(t *testing.T) {
openAPIProps := map[string]any{
"instanceType": map[string]any{"type": "string"},
}
schema := map[string]any{}
applyListInputOverrides(schema, "VMInstance", openAPIProps)
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
instanceType := specProps["instanceType"].(map[string]any)
if _, exists := instanceType["default"]; exists {
t.Errorf("expected no default key, got %v", instanceType["default"])
}
}
func TestApplyListInputOverrides_MergesWithExistingSchema(t *testing.T) {
openAPIProps := map[string]any{
"instanceType": map[string]any{"type": "string", "default": "u1.medium"},
}
// Simulate schema that already has spec.properties from buildMultilineStringSchema
schema := map[string]any{
"properties": map[string]any{
"spec": map[string]any{
"properties": map[string]any{
"otherField": map[string]any{"type": "multilineString"},
},
},
},
}
applyListInputOverrides(schema, "VMInstance", openAPIProps)
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
// instanceType should be added
if _, ok := specProps["instanceType"].(map[string]any); !ok {
t.Fatal("instanceType not found after override")
}
// otherField should be preserved
otherField, ok := specProps["otherField"].(map[string]any)
if !ok {
t.Fatal("otherField was lost after override")
}
if otherField["type"] != "multilineString" {
t.Errorf("otherField type changed, got %v", otherField["type"])
}
}
func TestParseOpenAPIProperties(t *testing.T) {
t.Run("extracts properties", func(t *testing.T) {
props := parseOpenAPIProperties(`{"type":"object","properties":{"instanceType":{"type":"string","default":"u1.medium"}}}`)
field, _ := props["instanceType"].(map[string]any)
if field["default"] != "u1.medium" {
t.Errorf("expected default u1.medium, got %v", field["default"])
}
})
t.Run("empty string", func(t *testing.T) {
if props := parseOpenAPIProperties(""); props != nil {
t.Errorf("expected nil, got %v", props)
}
})
t.Run("invalid JSON", func(t *testing.T) {
if props := parseOpenAPIProperties("{bad"); props != nil {
t.Errorf("expected nil, got %v", props)
}
})
t.Run("no properties key", func(t *testing.T) {
if props := parseOpenAPIProperties(`{"type":"object"}`); props != nil {
t.Errorf("expected nil, got %v", props)
}
})
}
func TestEnsureSchemaPath(t *testing.T) {
t.Run("creates path from empty schema", func(t *testing.T) {
schema := map[string]any{}
props := ensureSchemaPath(schema, "spec")
props["field"] = "value"
// Verify structure: schema.properties.spec.properties.field
got := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)["field"]
if got != "value" {
t.Errorf("expected value, got %v", got)
}
})
t.Run("preserves existing nested properties", func(t *testing.T) {
schema := map[string]any{
"properties": map[string]any{
"spec": map[string]any{
"properties": map[string]any{
"existing": "keep",
},
},
},
}
props := ensureSchemaPath(schema, "spec")
if props["existing"] != "keep" {
t.Errorf("existing property lost, got %v", props["existing"])
}
})
t.Run("multi-level path", func(t *testing.T) {
schema := map[string]any{}
props := ensureSchemaPath(schema, "spec", "nested")
props["deep"] = true
got := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)["nested"].(map[string]any)["properties"].(map[string]any)["deep"]
if got != true {
t.Errorf("expected true, got %v", got)
}
})
}
func TestEnsureArrayItemProps(t *testing.T) {
t.Run("creates from empty parent", func(t *testing.T) {
parent := map[string]any{}
props := ensureArrayItemProps(parent, "disks")
props["name"] = map[string]any{"type": "listInput"}
got := parent["disks"].(map[string]any)["items"].(map[string]any)["properties"].(map[string]any)["name"].(map[string]any)["type"]
if got != "listInput" {
t.Errorf("expected listInput, got %v", got)
}
})
t.Run("preserves existing item properties", func(t *testing.T) {
parent := map[string]any{
"disks": map[string]any{
"items": map[string]any{
"properties": map[string]any{
"bus": map[string]any{"type": "string"},
},
},
},
}
props := ensureArrayItemProps(parent, "disks")
props["name"] = map[string]any{"type": "listInput"}
if props["bus"].(map[string]any)["type"] != "string" {
t.Error("existing bus property was lost")
}
if props["name"].(map[string]any)["type"] != "listInput" {
t.Error("name property was not added")
}
})
}

View File

@@ -1,7 +1,8 @@
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
@@ -13,10 +14,10 @@ metadata:
nginx.ingress.kubernetes.io/proxy-send-timeout: "900"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
{{- if ne $issuerType "cloudflare" }}
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
spec:
ingressClassName: {{ $ingress }}
tls:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,171 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.4
name: packages.cozystack.io
spec:
group: cozystack.io
names:
kind: Package
listKind: PackageList
plural: packages
shortNames:
- pkg
- pkgs
singular: package
scope: Cluster
versions:
- additionalPrinterColumns:
- description: Selected variant
jsonPath: .spec.variant
name: Variant
type: string
- description: Ready status
jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
- description: Ready message
jsonPath: .status.conditions[?(@.type=='Ready')].message
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: Package is the Schema for the packages API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: PackageSpec defines the desired state of Package
properties:
components:
additionalProperties:
description: PackageComponent defines overrides for a specific component
properties:
enabled:
description: |-
Enabled indicates whether this component should be installed
If false, the component will be disabled even if it's defined in the PackageSource
type: boolean
values:
description: |-
Values contains Helm chart values as a JSON object
These values will be merged with the default values from the PackageSource
x-kubernetes-preserve-unknown-fields: true
type: object
description: |-
Components is a map of release name to component overrides
Allows overriding values and enabling/disabling specific components from the PackageSource
type: object
ignoreDependencies:
description: |-
IgnoreDependencies is a list of package source dependencies to ignore
Dependencies listed here will not be installed even if they are specified in the PackageSource
items:
type: string
type: array
variant:
description: |-
Variant is the name of the variant to use from the PackageSource
If not specified, defaults to "default"
type: string
type: object
status:
description: PackageStatus defines the observed state of Package
properties:
conditions:
description: Conditions represents the latest available observations
of a Package's state
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
dependencies:
additionalProperties:
description: DependencyStatus represents the readiness status of
a dependency
properties:
ready:
description: Ready indicates whether the dependency is ready
type: boolean
required:
- ready
type: object
description: |-
Dependencies tracks the readiness status of each dependency
Key is the dependency package name, value indicates if the dependency is ready
type: object
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -1,250 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.4
name: packagesources.cozystack.io
spec:
group: cozystack.io
names:
kind: PackageSource
listKind: PackageSourceList
plural: packagesources
shortNames:
- pks
singular: packagesource
scope: Cluster
versions:
- additionalPrinterColumns:
- description: Package variants (comma-separated)
jsonPath: .status.variants
name: Variants
type: string
- description: Ready status
jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
- description: Ready message
jsonPath: .status.conditions[?(@.type=='Ready')].message
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: PackageSource is the Schema for the packagesources API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: PackageSourceSpec defines the desired state of PackageSource
properties:
sourceRef:
description: SourceRef is the source reference for the package source
charts
properties:
kind:
description: Kind of the source reference
enum:
- GitRepository
- OCIRepository
type: string
name:
description: Name of the source reference
type: string
namespace:
description: Namespace of the source reference
type: string
path:
description: |-
Path is the base path where packages are located in the source.
For GitRepository, defaults to "packages" if not specified.
For OCIRepository, defaults to empty string (root) if not specified.
type: string
required:
- kind
- name
- namespace
type: object
variants:
description: |-
Variants is a list of package source variants
Each variant defines components, applications, dependencies, and libraries for a specific configuration
items:
description: Variant defines a single variant configuration
properties:
components:
description: Components is a list of Helm releases to be installed
as part of this variant
items:
description: Component defines a single Helm release component
within a package source
properties:
install:
description: Install defines installation parameters for
this component
properties:
dependsOn:
description: DependsOn is a list of component names
that must be installed before this component
items:
type: string
type: array
namespace:
description: Namespace is the Kubernetes namespace
where the release will be installed
type: string
privileged:
description: Privileged indicates whether this release
requires privileged access
type: boolean
releaseName:
description: |-
ReleaseName is the name of the HelmRelease resource that will be created
If not specified, defaults to the component Name field
type: string
type: object
libraries:
description: |-
Libraries is a list of library names that this component depends on
These libraries must be defined at the variant level
items:
type: string
type: array
name:
description: Name is the unique identifier for this component
within the package source
type: string
path:
description: Path is the path to the Helm chart directory
type: string
valuesFiles:
description: ValuesFiles is a list of values file names
to use
items:
type: string
type: array
required:
- name
- path
type: object
type: array
dependsOn:
description: |-
DependsOn is a list of package source dependencies
For example: "cozystack.networking"
items:
type: string
type: array
libraries:
description: Libraries is a list of Helm library charts used
by components in this variant
items:
description: Library defines a Helm library chart
properties:
name:
description: Name is the optional name for library placed
in charts
type: string
path:
description: Path is the path to the library chart directory
type: string
required:
- path
type: object
type: array
name:
description: Name is the unique identifier for this variant
type: string
required:
- name
type: object
type: array
type: object
status:
description: PackageSourceStatus defines the observed state of PackageSource
properties:
conditions:
description: Conditions represents the latest available observations
of a PackageSource's state
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
variants:
description: |-
Variants is a comma-separated list of package variant names
This field is populated by the controller based on spec.variants keys
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -1,3 +1,7 @@
{{- $validVariants := list "talos" "generic" "hosted" -}}
{{- if not (has .Values.cozystackOperator.variant $validVariants) -}}
{{- fail (printf "Invalid cozystackOperator.variant %q: must be one of talos, generic, hosted" .Values.cozystackOperator.variant) -}}
{{- end -}}
---
apiVersion: v1
kind: Namespace
@@ -53,10 +57,9 @@ spec:
args:
- --leader-elect=true
- --install-flux=true
# CRDs are also in crds/ for initial helm install, but Helm never updates
# them on upgrade and never deletes them on uninstall. The operator applies
# embedded CRDs via server-side apply on every startup, ensuring they stay
# up to date. To fully remove CRDs, delete them manually after helm uninstall.
# The operator applies embedded CRDs via server-side apply on every
# startup, ensuring they stay up to date.
# To fully remove CRDs, delete them manually after helm uninstall.
- --install-crds=true
- --metrics-bind-address=0
- --health-probe-bind-address=0

View File

@@ -1,57 +0,0 @@
{{- $validVariants := list "talos" "generic" "hosted" -}}
{{- if not (has .Values.cozystackOperator.variant $validVariants) -}}
{{- fail (printf "Invalid cozystackOperator.variant %q: must be one of talos, generic, hosted" .Values.cozystackOperator.variant) -}}
{{- end -}}
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.cozystack-platform
annotations:
operator.cozystack.io/skip-cozystack-values: "true"
spec:
sourceRef:
kind: OCIRepository
name: cozystack-platform
namespace: cozy-system
path: /
variants:
- name: default
components:
- install:
namespace: cozy-system
releaseName: cozystack-platform
name: platform
path: core/platform
valuesFiles:
- values.yaml
- name: isp-full
components:
- install:
namespace: cozy-system
releaseName: cozystack-platform
name: platform
path: core/platform
valuesFiles:
- values.yaml
- values-isp-full.yaml
- name: isp-hosted
components:
- install:
namespace: cozy-system
releaseName: cozystack-platform
name: platform
path: core/platform
valuesFiles:
- values.yaml
- values-isp-hosted.yaml
- name: isp-full-generic
components:
- install:
namespace: cozy-system
releaseName: cozystack-platform
name: platform
path: core/platform
valuesFiles:
- values.yaml
- values-isp-full-generic.yaml

View File

@@ -0,0 +1,92 @@
#!/bin/sh
# Migration 32 --> 33
# Convert publishing.certificates.issuerType to solver + issuerName in
# the cozystack-platform Package resource.
#
# Old field (pre-refactor schema):
# publishing.certificates.issuerType: "http01" | "cloudflare"
# New fields:
# publishing.certificates.solver: "http01" | "dns01"
# publishing.certificates.issuerName: "letsencrypt-prod" (or custom)
#
# Conversion table:
# cloudflare -> solver: dns01, issuerName: letsencrypt-prod
# http01 -> solver: http01, issuerName: letsencrypt-prod
# <custom> -> issuerName: <custom> (solver left at chart default)
# <absent> -> no-op
set -euo pipefail
PACKAGE_NAME="cozystack.cozystack-platform"
# Check if Package exists
if ! kubectl get package "$PACKAGE_NAME" >/dev/null 2>&1; then
echo "Package $PACKAGE_NAME not found, skipping migration"
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=33 --dry-run=client -o yaml | kubectl apply -f -
exit 0
fi
# Read current issuerType value
ISSUER_TYPE=$(kubectl get package "$PACKAGE_NAME" -o json | \
jq -r '.spec.components.platform.values.publishing.certificates.issuerType // ""')
if [ -z "$ISSUER_TYPE" ]; then
echo "No issuerType found in Package $PACKAGE_NAME, nothing to migrate"
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=33 --dry-run=client -o yaml | kubectl apply -f -
exit 0
fi
echo "Found issuerType: $ISSUER_TYPE"
# Convert old issuerType to new solver/issuerName
SOLVER=""
ISSUER_NAME=""
case "$ISSUER_TYPE" in
cloudflare)
SOLVER="dns01"
ISSUER_NAME="letsencrypt-prod"
;;
http01)
SOLVER="http01"
ISSUER_NAME="letsencrypt-prod"
;;
*)
# Unrecognised value — treat as custom ClusterIssuer name, no solver override
ISSUER_NAME="$ISSUER_TYPE"
;;
esac
echo "Converting to: solver=${SOLVER:-<chart default>}, issuerName=${ISSUER_NAME:-<chart default>}"
# Build the certificates patch:
# - null removes issuerType (JSON merge patch semantics)
# - solver and issuerName are included only when non-empty
CERTS_PATCH=$(jq -n --arg solver "$SOLVER" --arg issuerName "$ISSUER_NAME" '
{"issuerType": null}
+ (if $solver != "" then {"solver": $solver} else {} end)
+ (if $issuerName != "" then {"issuerName": $issuerName} else {} end)
')
PATCH_JSON=$(jq -n --argjson certs "$CERTS_PATCH" '{
"spec": {
"components": {
"platform": {
"values": {
"publishing": {
"certificates": $certs
}
}
}
}
}
}')
kubectl patch package "$PACKAGE_NAME" --type=merge --patch "$PATCH_JSON"
echo "Migration complete: issuerType=$ISSUER_TYPE -> solver=${SOLVER:-<unset>} issuerName=${ISSUER_NAME:-<unset>}"
# Stamp version
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=33 --dry-run=client -o yaml | kubectl apply -f -

View File

@@ -21,7 +21,8 @@ stringData:
_cluster:
root-host: {{ $rootHost | quote }}
bundle-name: {{ .Values.bundles.system.variant | quote }}
clusterissuer: {{ .Values.publishing.certificates.issuerType | quote }}
solver: {{ .Values.publishing.certificates.solver | quote }}
issuer-name: {{ .Values.publishing.certificates.issuerName | quote }}
oidc-enabled: {{ .Values.authentication.oidc.enabled | quote }}
oidc-insecure-skip-verify: {{ .Values.authentication.oidc.insecureSkipVerify | quote }}
extra-keycloak-redirect-uri-for-dashboard: {{ index .Values.authentication.oidc.keycloakExtraRedirectUri | quote }}

View File

@@ -6,7 +6,7 @@ sourceRef:
migrations:
enabled: false
image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0-beta.6@sha256:37c78dafcedbdad94acd9912550db0b4875897150666b8a06edfa894de99064e
targetVersion: 32
targetVersion: 33
# Bundle deployment configuration
bundles:
system:
@@ -46,7 +46,8 @@ publishing:
apiServerEndpoint: "" # example: "https://api.example.org"
externalIPs: []
certificates:
issuerType: http01 # "http01" or "cloudflare"
solver: http01 # "http01" or "dns01"
issuerName: letsencrypt-prod
# Authentication configuration
authentication:
oidc:

View File

@@ -1,4 +1,5 @@
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
apiVersion: networking.k8s.io/v1
@@ -8,10 +9,10 @@ metadata:
labels:
app: bootbox
annotations:
{{- if ne $issuerType "cloudflare" }}
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
{{- if .Values.whitelistHTTP }}
nginx.ingress.kubernetes.io/whitelist-source-range: "{{ join "," (.Values.whitelist | default "0.0.0.0/32") }}"
{{- end }}

View File

@@ -36,6 +36,8 @@
{{- if not (eq .Values.topology "Client") }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
@@ -134,8 +136,10 @@ spec:
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
cert-manager.io/cluster-issuer: letsencrypt-prod
{{- end }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
tls:
- hosts:
- {{ .Values.host | default (printf "s3.%s" $host) }}

View File

@@ -1,6 +1,7 @@
{{- $host := .Values._namespace.host }}
{{- $ingress := .Values._namespace.ingress }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
apiVersion: networking.k8s.io/v1
kind: Ingress
@@ -13,10 +14,10 @@ metadata:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "99999"
nginx.ingress.kubernetes.io/proxy-send-timeout: "99999"
{{- if ne $issuerType "cloudflare" }}
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
spec:
ingressClassName: {{ $ingress }}
tls:

View File

@@ -5,6 +5,9 @@
# update this file only when a new major or minor version is released
apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
releaseSeries:
- major: 0
minor: 16
contract: v1beta1
- major: 0
minor: 15
contract: v1beta1

View File

@@ -1,7 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: v0.15.1-cp
name: v0.16.0-cp
labels:
cp-components: cozy
annotations:

View File

@@ -4,7 +4,7 @@ metadata:
name: kamaji
spec:
# https://github.com/clastix/cluster-api-control-plane-provider-kamaji
version: v0.15.1-cp
version: v0.16.0-cp
fetchConfig:
selector:
matchLabels:

View File

@@ -1,4 +1,4 @@
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
@@ -10,7 +10,7 @@ spec:
name: letsencrypt-prod
server: https://acme-v02.api.letsencrypt.org/directory
solvers:
- {{- if eq $issuerType "cloudflare" }}
- {{- if eq $solver "dns01" }}
dns01:
cloudflare:
apiTokenSecretRef:
@@ -34,7 +34,7 @@ spec:
name: letsencrypt-stage
server: https://acme-staging-v02.api.letsencrypt.org/directory
solvers:
- {{- if eq $issuerType "cloudflare" }}
- {{- if eq $solver "dns01" }}
dns01:
cloudflare:
apiTokenSecretRef:

View File

@@ -181,7 +181,6 @@ rules:
- persistentvolumes
- endpoints
- events
- resourcequotas
verbs:
- delete
- apiGroups: ["kubevirt.io"]

View File

@@ -23,7 +23,6 @@ spec:
namespace: cozy-system
interval: 1m0s
timeout: 5m0s
values:
_cluster:
oidc-enabled: {{ .Values.oidcEnabled | quote }}
root-host: {{ .Values.rootHost | quote }}
valuesFrom:
- kind: Secret
name: cozystack-values

View File

@@ -0,0 +1,49 @@
diff --git a/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx b/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx
index d5e5230..9038dbb 100644
--- a/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx
+++ b/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx
@@ -259,14 +259,15 @@ export const FormListInput: FC<TFormListInputProps> = ({
<PersistedCheckbox formName={persistName || name} persistedControls={persistedControls} type="arr" />
</Flex>
</Flex>
- <ResetedFormItem
- key={arrKey !== undefined ? arrKey : Array.isArray(name) ? name.slice(-1)[0] : name}
- name={arrName || fixedName}
- rules={[getRequiredRule(forceNonRequired === false && !!required?.includes(getStringByName(name)), name)]}
- validateTrigger="onBlur"
- hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
- >
- <Flex gap={8} align="center">
+ <Flex gap={8} align="center">
+ <ResetedFormItem
+ key={arrKey !== undefined ? arrKey : Array.isArray(name) ? name.slice(-1)[0] : name}
+ name={arrName || fixedName}
+ rules={[getRequiredRule(forceNonRequired === false && !!required?.includes(getStringByName(name)), name)]}
+ validateTrigger="onBlur"
+ hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
+ style={{ flex: 1 }}
+ >
<Select
mode={customProps.mode}
placeholder="Select"
@@ -277,13 +278,13 @@ export const FormListInput: FC<TFormListInputProps> = ({
showSearch
style={{ width: '100%' }}
/>
- {relatedValueTooltip && (
- <Tooltip title={relatedValueTooltip}>
- <QuestionCircleOutlined />
- </Tooltip>
- )}
- </Flex>
- </ResetedFormItem>
+ </ResetedFormItem>
+ {relatedValueTooltip && (
+ <Tooltip title={relatedValueTooltip}>
+ <QuestionCircleOutlined />
+ </Tooltip>
+ )}
+ </Flex>
</HiddenContainer>
)
}

View File

@@ -1,4 +1,5 @@
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
{{- $host := index .Values._cluster "root-host" }}
{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
@@ -8,9 +9,8 @@ apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
{{- if eq $issuerType "cloudflare" }}
{{- else }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $exposeIngress }}
{{- end }}
nginx.ingress.kubernetes.io/rewrite-target: /

View File

@@ -0,0 +1,28 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
# Helm source files
README.md.gotmpl
.helmignore
# Build tools
Makefile

View File

@@ -0,0 +1,39 @@
apiVersion: v2
appVersion: latest
description: Kamaji is the Hosted Control Plane Manager for Kubernetes.
home: https://github.com/clastix/kamaji
icon: https://github.com/clastix/kamaji/raw/master/assets/logo-colored.png
maintainers:
- email: dario@tranchitella.eu
name: Dario Tranchitella
url: https://clastix.io
- email: me@bsctl.io
name: Adriano Pezzuto
url: https://clastix.io
name: kamaji-crds
sources:
- https://github.com/clastix/kamaji
type: application
version: 0.0.0+latest
annotations:
artifacthub.io/crds: |
- kind: TenantControlPlane
version: v1alpha1
name: tenantcontrolplanes.kamaji.clastix.io
displayName: TenantControlPlane
description: TenantControlPlane defines the desired state for a Control Plane backed by Kamaji.
- kind: DataStore
version: v1alpha1
name: datastores.kamaji.clastix.io
displayName: DataStore
description: DataStores is holding all the required details to communicate with a Datastore, such as etcd, MySQL, PostgreSQL, and NATS.
artifacthub.io/links: |
- name: CLASTIX
url: https://clastix.io
- name: support
url: https://clastix.io/support
artifacthub.io/changes: |
- kind: changed
description: Upgrading support to Kubernetes v1.35
- kind: added
description: Supporting multiple Datastore via etcd overrides

View File

@@ -0,0 +1,9 @@
docs: HELMDOCS_VERSION := v1.8.1
docs: docker
@docker run --rm -v "$$(pwd):/helm-docs" -u $$(id -u) jnorwood/helm-docs:$(HELMDOCS_VERSION)
docker:
@hash docker 2>/dev/null || {\
echo "You need docker" &&\
exit 1;\
}

View File

@@ -0,0 +1,2 @@
Kamaji Custom Resource Definitions have been installed properly:
you can proceed to upgrade your Kamaji operator instance.

View File

@@ -0,0 +1,66 @@
# kamaji-crds
![Version: 0.0.0+latest](https://img.shields.io/badge/Version-0.0.0+latest-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square)
Kamaji is the Hosted Control Plane Manager for Kubernetes.
## Maintainers
| Name | Email | Url |
| ---- | ------ | --- |
| Dario Tranchitella | <dario@tranchitella.eu> | <https://clastix.io> |
| Adriano Pezzuto | <me@bsctl.io> | <https://clastix.io> |
## Source Code
* <https://github.com/clastix/kamaji>
[Kamaji](https://github.com/clastix/kamaji) Custom Resource Definitions packaged as Helm Charts.
## How to use this chart
Add `clastix` Helm repository:
helm repo add clastix https://clastix.github.io/charts
Install the Chart with the release name `kamaji-crds`:
helm upgrade --install --namespace kamaji-system --create-namespace kamaji-crds clastix/kamaji-crds
Show the status:
helm status kamaji-crds -n kamaji-system
Upgrade the Chart
helm upgrade kamaji-crds -n kamaji-system clastix/kamaji-crds
Uninstall the Chart
helm uninstall kamaji-crds -n kamaji-system
## Customize the installation
There are two methods for specifying overrides of values during Chart installation: `--values` and `--set`.
The `--values` option is the preferred method because it allows you to keep your overrides in a YAML file, rather than specifying them all on the command line. Create a copy of the YAML file `values.yaml` and add your overrides to it.
Specify your overrides file when you install the Chart:
helm upgrade kamaji-crds --install --namespace kamaji-system --create-namespace clastix/kamaji-crds --values myvalues.yaml
The values in your overrides file `myvalues.yaml` will override their counterparts in the Chart's values.yaml file. Any values in `values.yaml` that werent overridden will keep their defaults.
If you only need to make minor customizations, you can specify them on the command line by using the `--set` option. For example:
helm upgrade kamaji-crds --install --namespace kamaji-system --create-namespace clastix/kamaji-crds --set kamajiCertificateName=kamaji
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| fullnameOverride | string | `""` | Overrides the full name of the resources created by the chart. |
| kamajiCertificateName | string | `"kamaji-serving-cert"` | The cert-manager Certificate resource name, holding the Certificate Authority for webhooks. |
| kamajiNamespace | string | `"kamaji-system"` | The namespace where Kamaji has been installed: required to inject the Certificate Authority for cert-manager. |
| kamajiService | string | `"kamaji-webhook-service"` | The Kamaji webhook Service name. |
| nameOverride | string | `""` | Overrides the name of the chart for resource naming purposes. |

View File

@@ -0,0 +1,54 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.maintainersSection" . }}
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
[Kamaji](https://github.com/clastix/kamaji) Custom Resource Definitions packaged as Helm Charts.
## How to use this chart
Add `clastix` Helm repository:
helm repo add clastix https://clastix.github.io/charts
Install the Chart with the release name `kamaji-crds`:
helm upgrade --install --namespace kamaji-system --create-namespace kamaji-crds clastix/kamaji-crds
Show the status:
helm status kamaji-crds -n kamaji-system
Upgrade the Chart
helm upgrade kamaji-crds -n kamaji-system clastix/kamaji-crds
Uninstall the Chart
helm uninstall kamaji-crds -n kamaji-system
## Customize the installation
There are two methods for specifying overrides of values during Chart installation: `--values` and `--set`.
The `--values` option is the preferred method because it allows you to keep your overrides in a YAML file, rather than specifying them all on the command line. Create a copy of the YAML file `values.yaml` and add your overrides to it.
Specify your overrides file when you install the Chart:
helm upgrade kamaji-crds --install --namespace kamaji-system --create-namespace clastix/kamaji-crds --values myvalues.yaml
The values in your overrides file `myvalues.yaml` will override their counterparts in the Chart's values.yaml file. Any values in `values.yaml` that werent overridden will keep their defaults.
If you only need to make minor customizations, you can specify them on the command line by using the `--set` option. For example:
helm upgrade kamaji-crds --install --namespace kamaji-system --create-namespace clastix/kamaji-crds --set kamajiCertificateName=kamaji
{{ template "chart.valuesSection" . }}

View File

@@ -0,0 +1,11 @@
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
name: kamaji-webhook-service
namespace: kamaji-system
path: /convert
conversionReviewVersions:
- v1

View File

@@ -0,0 +1,292 @@
group: kamaji.clastix.io
names:
kind: DataStore
listKind: DataStoreList
plural: datastores
singular: datastore
scope: Cluster
versions:
- additionalPrinterColumns:
- description: Kamaji data store driver
jsonPath: .spec.driver
name: Driver
type: string
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: DataStore is the Schema for the datastores API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: DataStoreSpec defines the desired state of DataStore.
properties:
basicAuth:
description: |-
In case of authentication enabled for the given data store, specifies the username and password pair.
This value is optional.
properties:
password:
properties:
content:
description: |-
Bare content of the file, base64 encoded.
It has precedence over the SecretReference value.
format: byte
type: string
secretReference:
properties:
keyPath:
description: |-
Name of the key for the given Secret reference where the content is stored.
This value is mandatory.
minLength: 1
type: string
name:
description: name is unique within a namespace to reference a secret resource.
type: string
namespace:
description: namespace defines the space within which the secret name must be unique.
type: string
required:
- keyPath
type: object
x-kubernetes-map-type: atomic
type: object
username:
properties:
content:
description: |-
Bare content of the file, base64 encoded.
It has precedence over the SecretReference value.
format: byte
type: string
secretReference:
properties:
keyPath:
description: |-
Name of the key for the given Secret reference where the content is stored.
This value is mandatory.
minLength: 1
type: string
name:
description: name is unique within a namespace to reference a secret resource.
type: string
namespace:
description: namespace defines the space within which the secret name must be unique.
type: string
required:
- keyPath
type: object
x-kubernetes-map-type: atomic
type: object
required:
- password
- username
type: object
driver:
description: The driver to use to connect to the shared datastore.
enum:
- etcd
- MySQL
- PostgreSQL
- NATS
type: string
x-kubernetes-validations:
- message: Datastore driver is immutable
rule: self == oldSelf
endpoints:
description: |-
List of the endpoints to connect to the shared datastore.
No need for protocol, just bare IP/FQDN and port.
items:
type: string
minItems: 1
type: array
tlsConfig:
description: |-
Defines the TLS/SSL configuration required to connect to the data store in a secure way.
This value is optional.
properties:
certificateAuthority:
description: |-
Retrieve the Certificate Authority certificate and private key, such as bare content of the file, or a SecretReference.
The key reference is required since etcd authentication is based on certificates, and Kamaji is responsible in creating this.
properties:
certificate:
properties:
content:
description: |-
Bare content of the file, base64 encoded.
It has precedence over the SecretReference value.
format: byte
type: string
secretReference:
properties:
keyPath:
description: |-
Name of the key for the given Secret reference where the content is stored.
This value is mandatory.
minLength: 1
type: string
name:
description: name is unique within a namespace to reference a secret resource.
type: string
namespace:
description: namespace defines the space within which the secret name must be unique.
type: string
required:
- keyPath
type: object
x-kubernetes-map-type: atomic
type: object
privateKey:
properties:
content:
description: |-
Bare content of the file, base64 encoded.
It has precedence over the SecretReference value.
format: byte
type: string
secretReference:
properties:
keyPath:
description: |-
Name of the key for the given Secret reference where the content is stored.
This value is mandatory.
minLength: 1
type: string
name:
description: name is unique within a namespace to reference a secret resource.
type: string
namespace:
description: namespace defines the space within which the secret name must be unique.
type: string
required:
- keyPath
type: object
x-kubernetes-map-type: atomic
type: object
required:
- certificate
type: object
clientCertificate:
description: Specifies the SSL/TLS key and private key pair used to connect to the data store.
properties:
certificate:
properties:
content:
description: |-
Bare content of the file, base64 encoded.
It has precedence over the SecretReference value.
format: byte
type: string
secretReference:
properties:
keyPath:
description: |-
Name of the key for the given Secret reference where the content is stored.
This value is mandatory.
minLength: 1
type: string
name:
description: name is unique within a namespace to reference a secret resource.
type: string
namespace:
description: namespace defines the space within which the secret name must be unique.
type: string
required:
- keyPath
type: object
x-kubernetes-map-type: atomic
type: object
privateKey:
properties:
content:
description: |-
Bare content of the file, base64 encoded.
It has precedence over the SecretReference value.
format: byte
type: string
secretReference:
properties:
keyPath:
description: |-
Name of the key for the given Secret reference where the content is stored.
This value is mandatory.
minLength: 1
type: string
name:
description: name is unique within a namespace to reference a secret resource.
type: string
namespace:
description: namespace defines the space within which the secret name must be unique.
type: string
required:
- keyPath
type: object
x-kubernetes-map-type: atomic
type: object
required:
- certificate
- privateKey
type: object
required:
- certificateAuthority
type: object
required:
- driver
- endpoints
type: object
x-kubernetes-validations:
- message: certificateAuthority privateKey must have secretReference or content when driver is etcd
rule: '(self.driver == "etcd") ? (self.tlsConfig != null && (has(self.tlsConfig.certificateAuthority.privateKey.secretReference) || has(self.tlsConfig.certificateAuthority.privateKey.content))) : true'
- message: clientCertificate must have secretReference or content when driver is etcd
rule: '(self.driver == "etcd") ? (self.tlsConfig != null && (has(self.tlsConfig.clientCertificate.certificate.secretReference) || has(self.tlsConfig.clientCertificate.certificate.content))) : true'
- message: clientCertificate privateKey must have secretReference or content when driver is etcd
rule: '(self.driver == "etcd") ? (self.tlsConfig != null && (has(self.tlsConfig.clientCertificate.privateKey.secretReference) || has(self.tlsConfig.clientCertificate.privateKey.content))) : true'
- message: When driver is not etcd and tlsConfig exists, clientCertificate must be null or contain valid content
rule: '(self.driver != "etcd" && has(self.tlsConfig) && has(self.tlsConfig.clientCertificate)) ? (((has(self.tlsConfig.clientCertificate.certificate.secretReference) || has(self.tlsConfig.clientCertificate.certificate.content)))) : true'
- message: When driver is not etcd and basicAuth exists, username must have secretReference or content
rule: '(self.driver != "etcd" && has(self.basicAuth)) ? ((has(self.basicAuth.username.secretReference) || has(self.basicAuth.username.content))) : true'
- message: When driver is not etcd and basicAuth exists, password must have secretReference or content
rule: '(self.driver != "etcd" && has(self.basicAuth)) ? ((has(self.basicAuth.password.secretReference) || has(self.basicAuth.password.content))) : true'
- message: When driver is not etcd, either tlsConfig or basicAuth must be provided
rule: '(self.driver != "etcd") ? (has(self.tlsConfig) || has(self.basicAuth)) : true'
status:
description: DataStoreStatus defines the observed state of DataStore.
properties:
observedGeneration:
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
format: int64
type: integer
usedBy:
description: List of the Tenant Control Planes, namespaced named, using this data store.
items:
type: string
type: array
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,218 @@
group: kamaji.clastix.io
names:
categories:
- kamaji
kind: KubeconfigGenerator
listKind: KubeconfigGeneratorList
plural: kubeconfiggenerators
shortNames:
- kc
singular: kubeconfiggenerator
scope: Cluster
versions:
- additionalPrinterColumns:
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: KubeconfigGenerator is the Schema for the kubeconfiggenerators API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
controlPlaneEndpointFrom:
default: admin.svc
description: |-
ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator.
The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`.
type: string
groups:
description: |-
Groups is resolved a set of strings used to assign the x509 organisations field.
It will be recognised by Kubernetes as user groups.
items:
description: |-
CompoundValue allows defining a static, or a dynamic value.
Options are mutually exclusive, just one should be picked up.
properties:
fromDefinition:
description: |-
FromDefinition is used to generate a dynamic value,
it uses the dot notation to access fields from the referenced TenantControlPlane object:
e.g.: metadata.name
type: string
stringValue:
description: StringValue is a static string value.
type: string
type: object
x-kubernetes-validations:
- message: Either stringValue or fromDefinition must be set, but not both.
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
type: array
namespaceSelector:
description: NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
tenantControlPlaneSelector:
description: TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
user:
description: User resolves to a string to identify the client, assigned to the x509 Common Name field.
properties:
fromDefinition:
description: |-
FromDefinition is used to generate a dynamic value,
it uses the dot notation to access fields from the referenced TenantControlPlane object:
e.g.: metadata.name
type: string
stringValue:
description: StringValue is a static string value.
type: string
type: object
x-kubernetes-validations:
- message: Either stringValue or fromDefinition must be set, but not both.
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
required:
- user
type: object
status:
description: KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator.
properties:
availableResources:
default: 0
description: |-
AvailableResources is the sum of successfully generated resources.
In case of a different value compared to Resources, check the field errors.
type: integer
errors:
description: Errors is the list of failed kubeconfig generations.
items:
properties:
message:
description: Message is the error message recorded upon the last generator run.
type: string
resource:
description: Resource is the Namespaced name of the errored resource.
type: string
required:
- message
- resource
type: object
type: array
observedGeneration:
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
format: int64
type: integer
resources:
default: 0
description: Resources is the sum of targeted TenantControlPlane objects.
type: integer
required:
- availableResources
- resources
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,49 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "kamaji-crds.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "kamaji.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "kamaji-crds.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create the cert-manager annotation to inject Certificate CA.
*/}}
{{- define "kamaji-crds.certManagerAnnotation" -}}
{{- printf "%s/%s" (required "A valid .Values.kamajiNamespace is required" .Values.kamajiNamespace) (required "A valid .Values.kamajiCertificateName is required" .Values.kamajiCertificateName) }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "kamaji-crds.labels" -}}
helm.sh/chart: {{ include "kamaji-crds.chart" . }}
app.kubernetes.io/name: {{ include "kamaji-crds.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: "crds"
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

View File

@@ -0,0 +1,10 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: {{ include "kamaji-crds.certManagerAnnotation" . }}
labels:
{{- include "kamaji-crds.labels" . | nindent 4 }}
name: datastores.kamaji.clastix.io
spec:
{{ tpl (.Files.Get "hack/kamaji.clastix.io_datastores_spec.yaml") . | nindent 2}}

View File

@@ -0,0 +1,10 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: {{ include "kamaji-crds.certManagerAnnotation" . }}
labels:
{{- include "kamaji-crds.labels" . | nindent 4 }}
name: kubeconfiggenerators.kamaji.clastix.io
spec:
{{ tpl (.Files.Get "hack/kamaji.clastix.io_kubeconfiggenerators_spec.yaml") . | nindent 2 }}

View File

@@ -0,0 +1,10 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: {{ include "kamaji-crds.certManagerAnnotation" . }}
labels:
{{- include "kamaji-crds.labels" . | nindent 4 }}
name: tenantcontrolplanes.kamaji.clastix.io
spec:
{{ tpl (.Files.Get "hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml") . | nindent 2 }}

View File

@@ -0,0 +1,15 @@
# Default values for kamaji-crds.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# -- Overrides the name of the chart for resource naming purposes.
nameOverride: ""
# -- Overrides the full name of the resources created by the chart.
fullnameOverride: ""
# -- The namespace where Kamaji has been installed: required to inject the Certificate Authority for cert-manager.
kamajiNamespace: kamaji-system
# -- The Kamaji webhook Service name.
kamajiService: kamaji-webhook-service
# -- The cert-manager Certificate resource name, holding the Certificate Authority for webhooks.
kamajiCertificateName: kamaji-serving-cert

View File

@@ -21,3 +21,8 @@
.idea/
*.tmproj
.vscode/
# Helm source files
README.md.gotmpl
.helmignore
# Build tools
Makefile

View File

@@ -1,6 +1,6 @@
dependencies:
- name: kamaji-etcd
repository: https://clastix.github.io/charts
version: 0.9.2
digest: sha256:ba76d3a30e5e20dbbbbcc36a0e7465d4b1adacc956061e7f6ea47b99fc8f08a6
generated: "2025-03-14T21:23:30.421915+09:00"
version: 0.11.0
digest: sha256:96b4115b8c02f771f809ec1bed3be3a3903e7e8315d6966aa54b0f73230ea421
generated: "2025-07-03T09:19:19.835421461+02:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: v0.0.0
appVersion: latest
description: Kamaji is the Hosted Control Plane Manager for Kubernetes.
home: https://github.com/clastix/kamaji
icon: https://github.com/clastix/kamaji/raw/master/assets/logo-colored.png
@@ -17,11 +17,11 @@ name: kamaji
sources:
- https://github.com/clastix/kamaji
type: application
version: 0.0.0
version: 0.0.0+latest
dependencies:
- name: kamaji-etcd
repository: https://clastix.github.io/charts
version: ">=0.9.2"
version: ">=0.11.0"
condition: kamaji-etcd.deploy
annotations:
catalog.cattle.io/certified: partner
@@ -46,4 +46,5 @@ annotations:
artifacthub.io/operator: "true"
artifacthub.io/operatorCapabilities: "full lifecycle"
artifacthub.io/changes: |
- Using dependency chart `kamaji-etcd` as a default DataStore.
- kind: added
description: Releasing latest chart at every push

View File

@@ -1,6 +1,6 @@
# kamaji
![Version: 0.0.0](https://img.shields.io/badge/Version-0.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.0.0](https://img.shields.io/badge/AppVersion-v0.0.0-informational?style=flat-square)
![Version: 0.0.0+latest](https://img.shields.io/badge/Version-0.0.0+latest-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square)
Kamaji is the Hosted Control Plane Manager for Kubernetes.
@@ -22,7 +22,7 @@ Kubernetes: `>=1.21.0-0`
| Repository | Name | Version |
|------------|------|---------|
| https://clastix.github.io/charts | kamaji-etcd | >=0.9.2 |
| https://clastix.github.io/charts | kamaji-etcd | >=0.11.0 |
[Kamaji](https://github.com/clastix/kamaji) requires a [multi-tenant `etcd`](https://github.com/clastix/kamaji-internal/blob/master/deploy/getting-started-with-kamaji.md#setup-internal-multi-tenant-etcd) cluster.
This Helm Chart starting from v0.1.1 provides the installation of an internal `etcd` in order to streamline the local test. If you'd like to use an externally managed etcd instance, you can specify the overrides and by setting the value `etcd.deploy=false`.
@@ -82,10 +82,25 @@ Here the values you can override:
| image.repository | string | `"clastix/kamaji"` | The container image of the Kamaji controller. |
| image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. |
| imagePullSecrets | list | `[]` | |
| kamaji-etcd.datastore.enabled | bool | `true` | |
| kamaji-etcd.datastore.name | string | `"default"` | |
| kamaji-etcd.deploy | bool | `true` | |
| kamaji-etcd.fullnameOverride | string | `"kamaji-etcd"` | |
| kamaji-etcd | object | `{"clusterDomain":"cluster.local","datastore":{"enabled":true,"name":"default"},"deploy":true,"fullnameOverride":"kamaji-etcd"}` | Subchart: See https://github.com/clastix/kamaji-etcd/blob/master/charts/kamaji-etcd/values.yaml |
| kubeconfigGenerator.affinity | object | `{}` | Kubernetes affinity rules to apply to Kubeconfig Generator controller pods |
| kubeconfigGenerator.enableLeaderElect | bool | `true` | Enables the leader election. |
| kubeconfigGenerator.enabled | bool | `false` | Toggle to deploy the Kubeconfig Generator Deployment. |
| kubeconfigGenerator.extraArgs | list | `[]` | A list of extra arguments to add to the Kubeconfig Generator controller default ones. |
| kubeconfigGenerator.fullnameOverride | string | `""` | |
| kubeconfigGenerator.healthProbeBindAddress | string | `":8081"` | The address the probe endpoint binds to. |
| kubeconfigGenerator.loggingDevel.enable | bool | `false` | Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) |
| kubeconfigGenerator.nodeSelector | object | `{}` | Kubernetes node selector rules to schedule Kubeconfig Generator controller |
| kubeconfigGenerator.podAnnotations | object | `{}` | The annotations to apply to the Kubeconfig Generator controller pods. |
| kubeconfigGenerator.podSecurityContext | object | `{"runAsNonRoot":true}` | The securityContext to apply to the Kubeconfig Generator controller pods. |
| kubeconfigGenerator.replicaCount | int | `2` | The number of the pod replicas for the Kubeconfig Generator controller. |
| kubeconfigGenerator.resources.limits.cpu | string | `"200m"` | |
| kubeconfigGenerator.resources.limits.memory | string | `"512Mi"` | |
| kubeconfigGenerator.resources.requests.cpu | string | `"200m"` | |
| kubeconfigGenerator.resources.requests.memory | string | `"512Mi"` | |
| kubeconfigGenerator.securityContext | object | `{"allowPrivilegeEscalation":false}` | The securityContext to apply to the Kubeconfig Generator controller container only. |
| kubeconfigGenerator.serviceAccountOverride | string | `""` | The name of the service account to use. If not set, the root Kamaji one will be used. |
| kubeconfigGenerator.tolerations | list | `[]` | Kubernetes node taints that the Kubeconfig Generator controller pods would tolerate |
| livenessProbe | object | `{"httpGet":{"path":"/healthz","port":"healthcheck"},"initialDelaySeconds":15,"periodSeconds":20}` | The livenessProbe for the controller container |
| loggingDevel.enable | bool | `false` | Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) (default false) |
| metricsBindAddress | string | `":8080"` | The address the metric endpoint binds to. (default ":8080") |

View File

@@ -1,12 +0,0 @@
# Kamaji
Kamaji deploys and operates Kubernetes at scale with a fraction of the operational burden.
Useful links:
- [Kamaji Github repository](https://github.com/clastix/kamaji)
- [Kamaji Documentation](https://kamaji.clastix.io)
## Requirements
* Kubernetes v1.22+
* Helm v3

View File

@@ -1,3 +1,25 @@
- apiGroups:
- ""
resources:
- configmaps
- secrets
- services
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
@@ -21,11 +43,19 @@
- list
- watch
- apiGroups:
- ""
- gateway.networking.k8s.io
resources:
- configmaps
- secrets
- services
- gateways
verbs:
- get
- list
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
- grpcroutes
- httproutes
- tlsroutes
verbs:
- create
- delete
@@ -51,6 +81,7 @@
- kamaji.clastix.io
resources:
- datastores/status
- kubeconfiggenerators/status
- tenantcontrolplanes/status
verbs:
- get
@@ -59,6 +90,18 @@
- apiGroups:
- kamaji.clastix.io
resources:
- kubeconfiggenerators
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- kamaji.clastix.io
resources:
- kubeconfiggenerators/finalizers
- tenantcontrolplanes/finalizers
verbs:
- update

View File

@@ -4,7 +4,7 @@ kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: kamaji-system/kamaji-serving-cert
controller-gen.kubebuilder.io/version: v0.16.1
controller-gen.kubebuilder.io/version: v0.20.0
name: datastores.kamaji.clastix.io
spec:
group: kamaji.clastix.io
@@ -284,6 +284,10 @@ spec:
status:
description: DataStoreStatus defines the observed state of DataStore.
properties:
observedGeneration:
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
format: int64
type: integer
usedBy:
description: List of the Tenant Control Planes, namespaced named, using this data store.
items:

View File

@@ -0,0 +1,226 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: kamaji-system/kamaji-serving-cert
controller-gen.kubebuilder.io/version: v0.20.0
name: kubeconfiggenerators.kamaji.clastix.io
spec:
group: kamaji.clastix.io
names:
categories:
- kamaji
kind: KubeconfigGenerator
listKind: KubeconfigGeneratorList
plural: kubeconfiggenerators
shortNames:
- kc
singular: kubeconfiggenerator
scope: Cluster
versions:
- additionalPrinterColumns:
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: KubeconfigGenerator is the Schema for the kubeconfiggenerators API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
controlPlaneEndpointFrom:
default: admin.svc
description: |-
ControlPlaneEndpointFrom is the key used to extract the Tenant Control Plane endpoint that must be used by the generator.
The targeted Secret is the `${TCP}-admin-kubeconfig` one, default to `admin.svc`.
type: string
groups:
description: |-
Groups is resolved a set of strings used to assign the x509 organisations field.
It will be recognised by Kubernetes as user groups.
items:
description: |-
CompoundValue allows defining a static, or a dynamic value.
Options are mutually exclusive, just one should be picked up.
properties:
fromDefinition:
description: |-
FromDefinition is used to generate a dynamic value,
it uses the dot notation to access fields from the referenced TenantControlPlane object:
e.g.: metadata.name
type: string
stringValue:
description: StringValue is a static string value.
type: string
type: object
x-kubernetes-validations:
- message: Either stringValue or fromDefinition must be set, but not both.
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
type: array
namespaceSelector:
description: NamespaceSelector is used to filter Namespaces from which the generator should extract TenantControlPlane objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
tenantControlPlaneSelector:
description: TenantControlPlaneSelector is used to filter the TenantControlPlane objects that should be address by the generator.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
user:
description: User resolves to a string to identify the client, assigned to the x509 Common Name field.
properties:
fromDefinition:
description: |-
FromDefinition is used to generate a dynamic value,
it uses the dot notation to access fields from the referenced TenantControlPlane object:
e.g.: metadata.name
type: string
stringValue:
description: StringValue is a static string value.
type: string
type: object
x-kubernetes-validations:
- message: Either stringValue or fromDefinition must be set, but not both.
rule: (has(self.stringValue) || has(self.fromDefinition)) && !(has(self.stringValue) && has(self.fromDefinition))
required:
- user
type: object
status:
description: KubeconfigGeneratorStatus defines the observed state of KubeconfigGenerator.
properties:
availableResources:
default: 0
description: |-
AvailableResources is the sum of successfully generated resources.
In case of a different value compared to Resources, check the field errors.
type: integer
errors:
description: Errors is the list of failed kubeconfig generations.
items:
properties:
message:
description: Message is the error message recorded upon the last generator run.
type: string
resource:
description: Resource is the Namespaced name of the errored resource.
type: string
required:
- message
- resource
type: object
type: array
observedGeneration:
description: ObservedGeneration represents the .metadata.generation that was last reconciled.
format: int64
type: integer
resources:
default: 0
description: Resources is the sum of targeted TenantControlPlane objects.
type: integer
required:
- availableResources
- resources
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -89,3 +89,15 @@ Create the name of the cert-manager Certificate
{{- define "kamaji.certificateName" -}}
{{- printf "%s-serving-cert" (include "kamaji.fullname" .) }}
{{- end }}
{{/*
Kubeconfig Generator Deployment name.
*/}}
{{- define "kamaji.kubeconfigGeneratorName" -}}
{{- if .Values.kubeconfigGenerator.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name "kubeconfig-generator" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,54 @@
{{- if .Values.kubeconfigGenerator.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
{{- include "kamaji.labels" . | nindent 4 }}
name: {{ include "kamaji.kubeconfigGeneratorName" . }}
namespace: {{ .Release.Namespace }}
spec:
replicas: {{ .Values.kubeconfigGenerator.replicaCount }}
selector:
matchLabels:
{{- include "kamaji.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.kubeconfigGenerator.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "kamaji.selectorLabels" . | nindent 8 }}
spec:
securityContext:
{{- toYaml .Values.kubeconfigGenerator.podSecurityContext | nindent 8 }}
serviceAccountName: {{ default .Values.kubeconfigGenerator.serviceAccountOverride (include "kamaji.serviceAccountName" .) }}
containers:
- args:
- kubeconfig-generator
- --health-probe-bind-address={{ .Values.kubeconfigGenerator.healthProbeBindAddress }}
- --leader-elect={{ .Values.kubeconfigGenerator.enableLeaderElect }}
{{- if .Values.kubeconfigGenerator.loggingDevel.enable }}- --zap-devel{{- end }}
{{- with .Values.kubeconfigGenerator.extraArgs }}
{{- toYaml . | nindent 10 }}
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
name: controller
resources:
{{- toYaml .Values.kubeconfigGenerator.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.kubeconfigGenerator.securityContext | nindent 12 }}
{{- with .Values.kubeconfigGenerator.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.kubeconfigGenerator.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.kubeconfigGenerator.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}

View File

@@ -98,9 +98,12 @@ loggingDevel:
# -- If specified, all the Kamaji instances with an unassigned DataStore will inherit this default value.
defaultDatastoreName: default
# -- Subchart: See https://github.com/clastix/kamaji-etcd/blob/master/charts/kamaji-etcd/values.yaml
kamaji-etcd:
deploy: true
fullnameOverride: kamaji-etcd
## -- Important, this must match your management cluster's clusterDomain, otherwise the init jobs will fail
clusterDomain: "cluster.local"
datastore:
enabled: true
name: default
@@ -108,4 +111,48 @@ kamaji-etcd:
# -- Disable the analytics traces collection
telemetry:
disabled: false
kubeconfigGenerator:
# -- Toggle to deploy the Kubeconfig Generator Deployment.
enabled: false
fullnameOverride: ""
# -- The number of the pod replicas for the Kubeconfig Generator controller.
replicaCount: 2
# -- The annotations to apply to the Kubeconfig Generator controller pods.
podAnnotations: {}
# -- The securityContext to apply to the Kubeconfig Generator controller pods.
podSecurityContext:
runAsNonRoot: true
# -- The name of the service account to use. If not set, the root Kamaji one will be used.
serviceAccountOverride: ""
# -- The address the probe endpoint binds to.
healthProbeBindAddress: ":8081"
# -- Enables the leader election.
enableLeaderElect: true
loggingDevel:
# -- Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error)
enable: false
# -- A list of extra arguments to add to the Kubeconfig Generator controller default ones.
extraArgs: []
resources:
limits:
cpu: 200m
memory: 512Mi
requests:
cpu: 200m
memory: 512Mi
# -- The securityContext to apply to the Kubeconfig Generator controller container only.
securityContext:
allowPrivilegeEscalation: false
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
# -- Kubernetes node selector rules to schedule Kubeconfig Generator controller
nodeSelector: {}
# -- Kubernetes node taints that the Kubeconfig Generator controller pods would tolerate
tolerations: []
# -- Kubernetes affinity rules to apply to Kubeconfig Generator controller pods
affinity: {}

View File

@@ -1,7 +1,7 @@
# Build the manager binary
FROM golang:1.24 as builder
FROM golang:1.26 AS builder
ARG VERSION=edge-25.4.1
ARG VERSION=edge-26.2.4
ARG TARGETOS
ARG TARGETARCH

View File

@@ -1,156 +0,0 @@
diff --git a/internal/resources/api_server_certificate.go b/internal/resources/api_server_certificate.go
index 436cdf9..4702b6c 100644
--- a/internal/resources/api_server_certificate.go
+++ b/internal/resources/api_server_certificate.go
@@ -108,6 +108,7 @@ func (r *APIServerCertificate) mutate(ctx context.Context, tenantControlPlane *k
}
r.resource.SetLabels(utilities.MergeMaps(
+ r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "x509",
diff --git a/internal/resources/api_server_kubelet_client_certificate.go b/internal/resources/api_server_kubelet_client_certificate.go
index 85b4d42..da18db4 100644
--- a/internal/resources/api_server_kubelet_client_certificate.go
+++ b/internal/resources/api_server_kubelet_client_certificate.go
@@ -95,6 +95,7 @@ func (r *APIServerKubeletClientCertificate) mutate(ctx context.Context, tenantCo
}
r.resource.SetLabels(utilities.MergeMaps(
+ r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "x509",
diff --git a/internal/resources/ca_certificate.go b/internal/resources/ca_certificate.go
index 5425b0b..625273f 100644
--- a/internal/resources/ca_certificate.go
+++ b/internal/resources/ca_certificate.go
@@ -137,7 +137,7 @@ func (r *CACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1
corev1.TLSPrivateKeyKey: ca.PrivateKey,
}
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
utilities.SetObjectChecksum(r.resource, r.resource.Data)
diff --git a/internal/resources/datastore/datastore_certificate.go b/internal/resources/datastore/datastore_certificate.go
index dea45ae..8492a5e 100644
--- a/internal/resources/datastore/datastore_certificate.go
+++ b/internal/resources/datastore/datastore_certificate.go
@@ -94,6 +94,7 @@ func (r *Certificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1al
r.resource.Data["ca.crt"] = ca
r.resource.SetLabels(utilities.MergeMaps(
+ r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "x509",
diff --git a/internal/resources/datastore/datastore_storage_config.go b/internal/resources/datastore/datastore_storage_config.go
index 7d03420..4ea9e64 100644
--- a/internal/resources/datastore/datastore_storage_config.go
+++ b/internal/resources/datastore/datastore_storage_config.go
@@ -181,7 +181,7 @@ func (r *Config) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.
utilities.SetObjectChecksum(r.resource, r.resource.Data)
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
return ctrl.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
}
diff --git a/internal/resources/front-proxy-client-certificate.go b/internal/resources/front-proxy-client-certificate.go
index f5ed67c..2dd4eda 100644
--- a/internal/resources/front-proxy-client-certificate.go
+++ b/internal/resources/front-proxy-client-certificate.go
@@ -95,6 +95,7 @@ func (r *FrontProxyClientCertificate) mutate(ctx context.Context, tenantControlP
}
r.resource.SetLabels(utilities.MergeMaps(
+ r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "x509",
diff --git a/internal/resources/front_proxy_ca_certificate.go b/internal/resources/front_proxy_ca_certificate.go
index d410720..ccadc70 100644
--- a/internal/resources/front_proxy_ca_certificate.go
+++ b/internal/resources/front_proxy_ca_certificate.go
@@ -114,7 +114,7 @@ func (r *FrontProxyCACertificate) mutate(ctx context.Context, tenantControlPlane
kubeadmconstants.FrontProxyCAKeyName: ca.PrivateKey,
}
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
utilities.SetObjectChecksum(r.resource, r.resource.Data)
diff --git a/internal/resources/k8s_ingress_resource.go b/internal/resources/k8s_ingress_resource.go
index f2e014f..e1aef59 100644
--- a/internal/resources/k8s_ingress_resource.go
+++ b/internal/resources/k8s_ingress_resource.go
@@ -147,7 +147,7 @@ func (r *KubernetesIngressResource) Define(_ context.Context, tenantControlPlane
func (r *KubernetesIngressResource) mutate(tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
return func() error {
- labels := utilities.MergeMaps(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Labels)
+ labels := utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Labels)
r.resource.SetLabels(labels)
annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Ingress.AdditionalMetadata.Annotations)
diff --git a/internal/resources/k8s_service_resource.go b/internal/resources/k8s_service_resource.go
index 7e7f11f..9c30145 100644
--- a/internal/resources/k8s_service_resource.go
+++ b/internal/resources/k8s_service_resource.go
@@ -76,7 +76,12 @@ func (r *KubernetesServiceResource) mutate(ctx context.Context, tenantControlPla
address, _ := tenantControlPlane.DeclaredControlPlaneAddress(ctx, r.Client)
return func() error {
- labels := utilities.MergeMaps(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Labels)
+ labels := utilities.MergeMaps(
+ r.resource.GetLabels(),
+ utilities.KamajiLabels(
+ tenantControlPlane.GetName(), r.GetName()),
+ tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Labels,
+ )
r.resource.SetLabels(labels)
annotations := utilities.MergeMaps(r.resource.GetAnnotations(), tenantControlPlane.Spec.ControlPlane.Service.AdditionalMetadata.Annotations)
diff --git a/internal/resources/kubeadm_config.go b/internal/resources/kubeadm_config.go
index ae4cfc0..98dc36d 100644
--- a/internal/resources/kubeadm_config.go
+++ b/internal/resources/kubeadm_config.go
@@ -89,7 +89,7 @@ func (r *KubeadmConfigResource) mutate(ctx context.Context, tenantControlPlane *
return err
}
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
params := kubeadm.Parameters{
TenantControlPlaneAddress: address,
diff --git a/internal/resources/kubeconfig.go b/internal/resources/kubeconfig.go
index a87da7f..bd77676 100644
--- a/internal/resources/kubeconfig.go
+++ b/internal/resources/kubeconfig.go
@@ -163,6 +163,7 @@ func (r *KubeconfigResource) mutate(ctx context.Context, tenantControlPlane *kam
}
r.resource.SetLabels(utilities.MergeMaps(
+ r.resource.GetLabels(),
utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()),
map[string]string{
constants.ControllerLabelResource: "kubeconfig",
diff --git a/internal/resources/sa_certificate.go b/internal/resources/sa_certificate.go
index b53c7b0..4001eca 100644
--- a/internal/resources/sa_certificate.go
+++ b/internal/resources/sa_certificate.go
@@ -113,7 +113,7 @@ func (r *SACertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1
kubeadmconstants.ServiceAccountPrivateKeyName: sa.PrivateKey,
}
- r.resource.SetLabels(utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName()))
+ r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
utilities.SetObjectChecksum(r.resource, r.resource.Data)

View File

@@ -1,23 +1,30 @@
diff --git a/cmd/manager/cmd.go b/cmd/manager/cmd.go
index 9a24d4e..a03a4e0 100644
--- a/cmd/manager/cmd.go
+++ b/cmd/manager/cmd.go
@@ -31,7 +31,6 @@ import (
@@ -4,7 +4,6 @@
package manager
import (
- "context"
"flag"
"fmt"
"io"
@@ -34,7 +33,6 @@
"github.com/clastix/kamaji/controllers/soot"
"github.com/clastix/kamaji/internal"
"github.com/clastix/kamaji/internal/builders/controlplane"
- datastoreutils "github.com/clastix/kamaji/internal/datastore/utils"
"github.com/clastix/kamaji/internal/utilities"
"github.com/clastix/kamaji/internal/webhook"
"github.com/clastix/kamaji/internal/webhook/handlers"
"github.com/clastix/kamaji/internal/webhook/routes"
@@ -80,10 +79,6 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
@@ -85,10 +83,6 @@
if webhookCABundle, err = os.ReadFile(webhookCAPath); err != nil {
return fmt.Errorf("unable to read webhook CA: %w", err)
}
- if err = datastoreutils.CheckExists(ctx, scheme, datastore); err != nil {
- return err
- }
-
if controllerReconcileTimeout.Seconds() == 0 {
return fmt.Errorf("the controller reconcile timeout must be greater than zero")
- if err = datastoreutils.CheckExists(context.Background(), scheme, datastore); err != nil {
- return err
}
if controllerReconcileTimeout.Seconds() == 0 {

View File

@@ -0,0 +1,49 @@
diff --git a/internal/kubeadm/uploadconfig.go b/internal/kubeadm/uploadconfig.go
index 89c9b54..1ee38cd 100644
--- a/internal/kubeadm/uploadconfig.go
+++ b/internal/kubeadm/uploadconfig.go
@@ -41,7 +41,7 @@ func UploadKubeletConfig(client kubernetes.Interface, config *Configuration, pat
TenantControlPlaneCgroupDriver: config.Parameters.TenantControlPlaneCGroupDriver,
}
- content, err := getKubeletConfigmapContent(kubeletConfiguration, patches)
+ content, err := getKubeletConfigmapContent(kubeletConfiguration, patches, config.Parameters.TenantControlPlaneVersion)
if err != nil {
return nil, err
}
@@ -72,7 +72,13 @@ func UploadKubeletConfig(client kubernetes.Interface, config *Configuration, pat
return nil, nil
}
-func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration, patch jsonpatchv5.Patch) ([]byte, error) {
+// minVerKubeletNewDefaults is the minimum Kubernetes version that supports the
+// CrashLoopBackOff and ImagePullCredentialsVerificationPolicy kubelet
+// configuration fields (gated by KubeletCrashLoopBackOffMax and
+// KubeletEnsureSecretPulledImages feature gates respectively).
+var minVerKubeletNewDefaults = semver.MustParse("1.35.0")
+
+func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration, patch jsonpatchv5.Patch, version string) ([]byte, error) {
var kc kubelettypes.KubeletConfiguration
kubeletv1beta1.SetDefaults_KubeletConfiguration(&kc)
@@ -94,6 +100,20 @@ func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration, patch
// determine the resolvConf location, as reported in clastix/kamaji#581.
kc.ResolverConfig = nil
+ // Clear fields set by SetDefaults_KubeletConfiguration from Kubernetes >= 1.35.
+ // Older kubelets reject these fields because the corresponding feature gates
+ // (KubeletCrashLoopBackOffMax, KubeletEnsureSecretPulledImages) are not enabled.
+ // See: https://github.com/clastix/kamaji/issues/1062
+ parsedVer, parseErr := semver.ParseTolerant(version)
+ if parseErr != nil {
+ return nil, fmt.Errorf("failed to parse kubernetes version %q for kubelet config: %w", version, parseErr)
+ }
+
+ if parsedVer.LT(minVerKubeletNewDefaults) {
+ kc.CrashLoopBackOff = kubelettypes.CrashLoopBackOffConfig{}
+ kc.ImagePullCredentialsVerificationPolicy = ""
+ }
+
if len(patch) > 0 {
kubeletConfig, patchErr := utilities.EncodeToJSON(&kc)
if patchErr != nil {

View File

@@ -1,5 +1,6 @@
{{- $host := index .Values._cluster "root-host" }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
apiVersion: networking.k8s.io/v1
@@ -8,10 +9,10 @@ metadata:
name: keycloak-ingress
{{- with .Values.ingress.annotations }}
annotations:
{{- if ne $issuerType "cloudflare" }}
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $exposeIngress }}
{{- end }}
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:

File diff suppressed because one or more lines are too long

View File

@@ -124,7 +124,7 @@ spec:
filesystemOverhead:
description: FilesystemOverhead describes the space reserved for
overhead when using Filesystem volumes. A value is between 0
and 1, if not defined it is 0.055 (5.5% overhead)
and 1, if not defined it is 0.06 (6% overhead)
properties:
global:
description: Global is how much space of a Filesystem volume
@@ -2656,7 +2656,7 @@ spec:
filesystemOverhead:
description: FilesystemOverhead describes the space reserved for
overhead when using Filesystem volumes. A value is between 0
and 1, if not defined it is 0.055 (5.5% overhead)
and 1, if not defined it is 0.06 (6% overhead)
properties:
global:
description: Global is how much space of a Filesystem volume
@@ -5164,6 +5164,14 @@ rules:
- get
- update
- delete
- apiGroups:
- admissionregistration.k8s.io
resourceNames:
- cdi-api-dataimportcron-mutate
resources:
- mutatingwebhookconfigurations
verbs:
- delete
- apiGroups:
- apiregistration.k8s.io
resources:
@@ -5175,6 +5183,17 @@ rules:
- create
- update
- delete
- apiGroups:
- populator.storage.k8s.io
resources:
- volumepopulators
verbs:
- get
- list
- watch
- create
- update
- delete
- apiGroups:
- authorization.k8s.io
resources:
@@ -5285,6 +5304,9 @@ rules:
verbs:
- create
- patch
- get
- list
- watch
- apiGroups:
- ""
resources:
@@ -5325,6 +5347,14 @@ rules:
- watch
- create
- delete
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
@@ -5358,6 +5388,7 @@ rules:
- get
- apiGroups:
- cdi.kubevirt.io
- forklift.cdi.kubevirt.io
resources:
- '*'
verbs:
@@ -5418,14 +5449,11 @@ rules:
verbs:
- update
- apiGroups:
- forklift.cdi.kubevirt.io
- authorization.k8s.io
resources:
- ovirtvolumepopulators
- openstackvolumepopulators
- subjectaccessreviews
verbs:
- get
- list
- watch
- create
- apiGroups:
- ""
resources:
@@ -5670,6 +5698,7 @@ metadata:
labels:
cdi.kubevirt.io: cdi-operator
name: cdi-operator
np.kubevirt.io/allow-access-cluster-services: "true"
operator.cdi.kubevirt.io: ""
prometheus.cdi.kubevirt.io: "true"
name: cdi-operator
@@ -5688,6 +5717,7 @@ spec:
labels:
cdi.kubevirt.io: cdi-operator
name: cdi-operator
np.kubevirt.io/allow-access-cluster-services: "true"
operator.cdi.kubevirt.io: ""
prometheus.cdi.kubevirt.io: "true"
spec:
@@ -5708,33 +5738,50 @@ spec:
- name: DEPLOY_CLUSTER_RESOURCES
value: "true"
- name: OPERATOR_VERSION
value: v1.62.0
value: v1.64.0
- name: CONTROLLER_IMAGE
value: quay.io/kubevirt/cdi-controller:v1.62.0
value: quay.io/kubevirt/cdi-controller:v1.64.0
- name: IMPORTER_IMAGE
value: quay.io/kubevirt/cdi-importer:v1.62.0
value: quay.io/kubevirt/cdi-importer:v1.64.0
- name: CLONER_IMAGE
value: quay.io/kubevirt/cdi-cloner:v1.62.0
value: quay.io/kubevirt/cdi-cloner:v1.64.0
- name: OVIRT_POPULATOR_IMAGE
value: quay.io/kubevirt/cdi-importer:v1.62.0
value: quay.io/kubevirt/cdi-importer:v1.64.0
- name: APISERVER_IMAGE
value: quay.io/kubevirt/cdi-apiserver:v1.62.0
value: quay.io/kubevirt/cdi-apiserver:v1.64.0
- name: UPLOAD_SERVER_IMAGE
value: quay.io/kubevirt/cdi-uploadserver:v1.62.0
value: quay.io/kubevirt/cdi-uploadserver:v1.64.0
- name: UPLOAD_PROXY_IMAGE
value: quay.io/kubevirt/cdi-uploadproxy:v1.62.0
value: quay.io/kubevirt/cdi-uploadproxy:v1.64.0
- name: VERBOSITY
value: "1"
- name: PULL_POLICY
value: IfNotPresent
- name: MONITORING_NAMESPACE
image: quay.io/kubevirt/cdi-operator:v1.62.0
image: quay.io/kubevirt/cdi-operator:v1.64.0
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /healthz
port: 8444
scheme: HTTP
initialDelaySeconds: 5
timeoutSeconds: 10
name: cdi-operator
ports:
- containerPort: 8080
- containerPort: 8443
name: metrics
protocol: TCP
- containerPort: 8444
name: health
protocol: TCP
readinessProbe:
httpGet:
path: /readyz
port: 8444
scheme: HTTP
initialDelaySeconds: 5
timeoutSeconds: 10
resources:
requests:
cpu: 100m
@@ -5748,13 +5795,19 @@ spec:
seccompProfile:
type: RuntimeDefault
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
terminationMessagePolicy: FallbackToLogsOnError
nodeSelector:
kubernetes.io/os: linux
securityContext:
runAsNonRoot: true
serviceAccountName: cdi-operator
tolerations:
- key: CriticalAddonsOnly
operator: Exists
---
- key: CriticalAddonsOnly
operator: Exists
- effect: NoSchedule
key: node-role.kubernetes.io/control-plane
operator: Exists
- effect: NoSchedule
key: node-role.kubernetes.io/master
operator: Exists
---

View File

@@ -6,7 +6,8 @@ include ../../../hack/package.mk
update:
rm -rf templates
mkdir templates
export RELEASE=$$(curl https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt) && \
# v1.7.0 blocked by https://github.com/kubevirt/kubevirt/issues/16386
export RELEASE=v1.6.3 && \
wget https://github.com/kubevirt/kubevirt/releases/download/$${RELEASE}/kubevirt-operator.yaml -O templates/kubevirt-operator.yaml && \
sed -i 's/namespace: kubevirt/namespace: $(NAMESPACE)/g' templates/kubevirt-operator.yaml
awk -i inplace -v RS="---" '!/kind: Namespace/{printf "%s", $$0 RS}' templates/kubevirt-operator.yaml

View File

@@ -288,6 +288,10 @@ spec:
developerConfiguration:
description: DeveloperConfiguration holds developer options
properties:
clusterProfiler:
description: Enable the ability to pprof profile KubeVirt
control plane
type: boolean
cpuAllocationRatio:
description: |-
For each requested virtual CPU, CPUAllocationRatio defines how much physical CPU to request per VMI
@@ -337,6 +341,8 @@ spec:
type: integer
virtOperator:
type: integer
virtSynchronizationController:
type: integer
type: object
memoryOvercommit:
description: |-
@@ -2132,6 +2138,10 @@ spec:
When ServiceMonitorNamespace is set, then we'll install the service monitor object in that namespace
otherwise we will use the monitoring namespace.
type: string
synchronizationPort:
description: Specify the port to listen on for VMI status synchronization
traffic. Default is 9185
type: string
uninstallStrategy:
description: |-
Specifies if kubevirt can be deleted if workloads are still present.
@@ -3263,6 +3273,11 @@ spec:
description: KubeVirtPhase is a label for the phase of a KubeVirt
deployment at the current time.
type: string
synchronizationAddresses:
items:
type: string
type: array
x-kubernetes-list-type: atomic
targetDeploymentConfig:
type: string
targetDeploymentID:
@@ -3552,6 +3567,10 @@ spec:
developerConfiguration:
description: DeveloperConfiguration holds developer options
properties:
clusterProfiler:
description: Enable the ability to pprof profile KubeVirt
control plane
type: boolean
cpuAllocationRatio:
description: |-
For each requested virtual CPU, CPUAllocationRatio defines how much physical CPU to request per VMI
@@ -3601,6 +3620,8 @@ spec:
type: integer
virtOperator:
type: integer
virtSynchronizationController:
type: integer
type: object
memoryOvercommit:
description: |-
@@ -5396,6 +5417,10 @@ spec:
When ServiceMonitorNamespace is set, then we'll install the service monitor object in that namespace
otherwise we will use the monitoring namespace.
type: string
synchronizationPort:
description: Specify the port to listen on for VMI status synchronization
traffic. Default is 9185
type: string
uninstallStrategy:
description: |-
Specifies if kubevirt can be deleted if workloads are still present.
@@ -6527,6 +6552,11 @@ spec:
description: KubeVirtPhase is a label for the phase of a KubeVirt
deployment at the current time.
type: string
synchronizationAddresses:
items:
type: string
type: array
x-kubernetes-list-type: atomic
targetDeploymentConfig:
type: string
targetDeploymentID:
@@ -6602,6 +6632,8 @@ rules:
- kubevirt-virt-api-certs
- kubevirt-controller-certs
- kubevirt-exportproxy-certs
- kubevirt-synchronization-controller-certs
- kubevirt-synchronization-controller-server-certs
resources:
- secrets
verbs:
@@ -6713,6 +6745,28 @@ rules:
- get
- list
- watch
- apiGroups:
- ""
resourceNames:
- kubevirt-ca
resources:
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- delete
- update
- create
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
@@ -6936,6 +6990,7 @@ rules:
- persistentvolumeclaims
verbs:
- get
- list
- apiGroups:
- kubevirt.io
resources:
@@ -7331,6 +7386,15 @@ rules:
- create
- get
- delete
- apiGroups:
- resource.k8s.io
resources:
- resourceslices
- resourceclaims
verbs:
- list
- watch
- get
- apiGroups:
- kubevirt.io
resources:
@@ -7402,6 +7466,50 @@ rules:
verbs:
- list
- watch
- apiGroups:
- kubevirt.io
resources:
- virtualmachineinstances
verbs:
- get
- list
- watch
- update
- patch
- apiGroups:
- kubevirt.io
resources:
- virtualmachineinstancemigrations
verbs:
- get
- list
- watch
- patch
- delete
- apiGroups:
- kubevirt.io
resources:
- kubevirts
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- update
- create
- patch
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- apiGroups:
- kubevirt.io
resources:
@@ -7430,6 +7538,8 @@ rules:
- virtualmachineinstances/sev/fetchcertchain
- virtualmachineinstances/sev/querylaunchmeasurement
- virtualmachineinstances/usbredir
- virtualmachines/objectgraph
- virtualmachineinstances/objectgraph
verbs:
- get
- apiGroups:
@@ -7586,6 +7696,8 @@ rules:
- virtualmachineinstances/sev/fetchcertchain
- virtualmachineinstances/sev/querylaunchmeasurement
- virtualmachineinstances/usbredir
- virtualmachines/objectgraph
- virtualmachineinstances/objectgraph
verbs:
- get
- apiGroups:
@@ -7746,6 +7858,8 @@ rules:
- virtualmachineinstances/userlist
- virtualmachineinstances/sev/fetchcertchain
- virtualmachineinstances/sev/querylaunchmeasurement
- virtualmachines/objectgraph
- virtualmachineinstances/objectgraph
verbs:
- get
- apiGroups:
@@ -7922,15 +8036,22 @@ spec:
- virt-operator
env:
- name: VIRT_OPERATOR_IMAGE
value: quay.io/kubevirt/virt-operator:v1.5.2
value: quay.io/kubevirt/virt-operator:v1.6.3
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.annotations['olm.targetNamespaces']
- name: KUBEVIRT_VERSION
value: v1.5.2
image: quay.io/kubevirt/virt-operator:v1.5.2
value: v1.6.3
image: quay.io/kubevirt/virt-operator:v1.6.3
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /metrics
port: 8443
scheme: HTTPS
initialDelaySeconds: 5
timeoutSeconds: 10
name: virt-operator
ports:
- containerPort: 8443
@@ -7957,6 +8078,7 @@ spec:
- ALL
seccompProfile:
type: RuntimeDefault
terminationMessagePolicy: FallbackToLogsOnError
volumeMounts:
- mountPath: /etc/virt-operator/certificates
name: kubevirt-operator-certs
@@ -7981,4 +8103,4 @@ spec:
secretName: kubevirt-operator-certs
- emptyDir: {}
name: profile-data
---
---

View File

@@ -22,6 +22,8 @@ spec:
- GPU
- VMExport
evictionStrategy: LiveMigrate
virtualMachineOptions:
disableSerialConsoleLog: {}
vmRolloutStrategy: LiveUpdate
workloadUpdateStrategy:
workloadUpdateMethods:

View File

@@ -0,0 +1,155 @@
diff --git a/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java b/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java
index 49138a8fd..2f768ca0d 100644
--- a/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java
+++ b/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java
@@ -83,6 +83,8 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
+import java.nio.file.Files;
+import java.nio.file.Paths;
@Singleton
public class DeviceHandlerImpl implements DeviceHandler
@@ -1646,7 +1648,10 @@ public class DeviceHandlerImpl implements DeviceHandler
private void updateDiscGran(VlmProviderObject<Resource> vlmData) throws DatabaseException, StorageException
{
String devicePath = vlmData.getDevicePath();
- if (devicePath != null && vlmData.exists())
+ // Check if device path physically exists before calling lsblk
+ // This is important for DRBD devices which might be temporarily unavailable during adjust
+ // (drbdadm adjust brings devices down/up, and kernel might not have created the device node yet)
+ if (devicePath != null && vlmData.exists() && Files.exists(Paths.get(devicePath)))
{
if (vlmData.getDiscGran() == VlmProviderObject.UNINITIALIZED_SIZE)
{
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
index 01967a31f..78b8195a4 100644
--- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
@@ -592,7 +592,29 @@ public class DrbdLayer implements DeviceLayer
// The .res file might not have been generated in the prepare method since it was
// missing information from the child-layers. Now that we have processed them, we
// need to make sure the .res file exists in all circumstances.
- regenerateResFile(drbdRscData);
+ // However, if the underlying devices are not accessible (e.g., LUKS device is closed
+ // during resource deletion), we skip regenerating the res file to avoid errors
+ boolean canRegenerateResFile = true;
+ if (!skipDisk && !drbdRscData.getAbsResource().isDrbdDiskless(workerCtx))
+ {
+ AbsRscLayerObject<Resource> dataChild = drbdRscData.getChildBySuffix(RscLayerSuffixes.SUFFIX_DATA);
+ if (dataChild != null)
+ {
+ for (DrbdVlmData<Resource> drbdVlmData : drbdRscData.getVlmLayerObjects().values())
+ {
+ VlmProviderObject<Resource> childVlm = dataChild.getVlmProviderObject(drbdVlmData.getVlmNr());
+ if (childVlm == null || !childVlm.exists() || childVlm.getDevicePath() == null)
+ {
+ canRegenerateResFile = false;
+ break;
+ }
+ }
+ }
+ }
+ if (canRegenerateResFile)
+ {
+ regenerateResFile(drbdRscData);
+ }
// createMetaData needs rendered resFile
for (DrbdVlmData<Resource> drbdVlmData : createMetaData)
@@ -766,19 +788,72 @@ public class DrbdLayer implements DeviceLayer
if (drbdRscData.isAdjustRequired())
{
- try
+ // Check if underlying devices are accessible before adjusting
+ // This is important for encrypted resources (LUKS) where the device
+ // might be closed during deletion
+ boolean canAdjust = true;
+
+ // IMPORTANT: Check child volumes only when disk access is actually needed.
+ // For network reconnect (StandAlone -> Connected), disk access is not required.
+ boolean needsDiskAccess = false;
+
+ // Check if there are pending operations that require disk access
+ for (DrbdVlmData<Resource> drbdVlmData : drbdRscData.getVlmLayerObjects().values())
{
- drbdUtils.adjust(
- drbdRscData,
- false,
- skipDisk,
- false
- );
+ Volume vlm = (Volume) drbdVlmData.getVolume();
+ StateFlags<Volume.Flags> vlmFlags = vlm.getFlags();
+
+ // Disk access is needed if:
+ // - creating a new volume
+ // - resizing
+ // - checking/creating metadata
+ if (!drbdVlmData.exists() ||
+ drbdVlmData.checkMetaData() ||
+ vlmFlags.isSomeSet(workerCtx, Volume.Flags.RESIZE, Volume.Flags.DRBD_RESIZE))
+ {
+ needsDiskAccess = true;
+ break;
+ }
+ }
+
+ // Check child volumes only if disk access is actually needed
+ if (needsDiskAccess && !skipDisk && !drbdRscData.getAbsResource().isDrbdDiskless(workerCtx))
+ {
+ AbsRscLayerObject<Resource> dataChild = drbdRscData.getChildBySuffix(RscLayerSuffixes.SUFFIX_DATA);
+ if (dataChild != null)
+ {
+ for (DrbdVlmData<Resource> drbdVlmData : drbdRscData.getVlmLayerObjects().values())
+ {
+ VlmProviderObject<Resource> childVlm = dataChild.getVlmProviderObject(drbdVlmData.getVlmNr());
+ if (childVlm == null || !childVlm.exists() || childVlm.getDevicePath() == null)
+ {
+ canAdjust = false;
+ break;
+ }
+ }
+ }
}
- catch (ExtCmdFailedException extCmdExc)
+
+ if (canAdjust)
+ {
+ try
+ {
+ drbdUtils.adjust(
+ drbdRscData,
+ false,
+ skipDisk,
+ false
+ );
+ }
+ catch (ExtCmdFailedException extCmdExc)
+ {
+ restoreBackupResFile(drbdRscData);
+ throw extCmdExc;
+ }
+ }
+ else
{
- restoreBackupResFile(drbdRscData);
- throw extCmdExc;
+ drbdRscData.setAdjustRequired(false);
}
}
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java
index cdca0b6d2..89c8be9da 100644
--- a/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java
@@ -383,6 +383,7 @@ public class LuksLayer implements DeviceLayer
vlmData.setSizeState(Size.AS_EXPECTED);
vlmData.setOpened(true);
+ vlmData.setExists(true);
vlmData.setFailed(false);
}
}

View File

@@ -280,8 +280,8 @@ vmagent:
cluster: cozystack
remoteWrite:
urls:
- http://vminsert-shortterm.{{ .Values.global.target }}.svc:8480/insert/0/prometheus
- http://vminsert-longterm.{{ .Values.global.target }}.svc:8480/insert/0/prometheus
- http://vminsert-shortterm.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}:8480/insert/0/prometheus
- http://vminsert-longterm.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}:8480/insert/0/prometheus
extraArgs: {}
fluent-bit:
@@ -344,7 +344,7 @@ fluent-bit:
[OUTPUT]
Name http
Match kube.*
Host vlogs-generic.{{ .Values.global.target }}.svc
Host vlogs-generic.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}
port 9428
compress gzip
uri /insert/jsonline?_stream_fields=log_source,stream,kubernetes_pod_name,kubernetes_container_name,kubernetes_namespace_name&_msg_field=log&_time_field=date
@@ -355,7 +355,7 @@ fluent-bit:
[OUTPUT]
Name http
Match events.*
Host vlogs-generic.{{ .Values.global.target }}.svc
Host vlogs-generic.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}
port 9428
compress gzip
uri /insert/jsonline?_stream_fields=log_source,reason,meatdata_namespace,metadata_name&_msg_field=message&_time_field=date
@@ -366,7 +366,7 @@ fluent-bit:
[OUTPUT]
Name http
Match audit.*
Host vlogs-generic.{{ .Values.global.target }}.svc
Host vlogs-generic.{{ .Values.global.target }}.svc.{{ (index .Values._cluster "cluster-domain") | default "cluster.local" }}
port 9428
compress gzip
uri /insert/jsonline?_stream_fields=log_source,stage,user_username,verb,requestUri&_msg_field=requestURI&_time_field=date

View File

@@ -1,4 +1,5 @@
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
@@ -171,10 +172,10 @@ metadata:
labels:
app: alerta
annotations:
{{- if ne $issuerType "cloudflare" }}
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
spec:
ingressClassName: {{ $ingress }}
tls:

View File

@@ -1,4 +1,5 @@
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
---
@@ -72,10 +73,10 @@ spec:
ingress:
metadata:
annotations:
{{- if ne $issuerType "cloudflare" }}
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: "{{ $ingress }}"
{{- end }}
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
spec:
ingressClassName: "{{ $ingress }}"
rules: