From 5329dd180ba0a73607630fac42af846a0a47113b Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Thu, 23 Apr 2026 10:41:41 +0500 Subject: [PATCH 01/11] fix(backups): move velero-configmap Role to velero chart The backupstrategy-controller chart declared a Role/RoleBinding in the cozy-velero namespace for ResourceModifier ConfigMap management. Because velero is an optional package, that namespace does not exist in bundles without velero, so Helm install aborted with "namespaces \"cozy-velero\" not found" and blocked the default install of backupstrategy-controller. Move the Role and RoleBinding into the velero chart so they are created only when velero is actually installed. The RoleBinding subject points to the backupstrategy-controller ServiceAccount in its fixed namespace (cozy-backup-controller). Assisted-By: Claude Signed-off-by: Myasnikov Daniil --- .../templates/rbac-bind.yaml | 14 ---------- .../templates/rbac.yaml | 15 +++-------- .../backupstrategy-controller-rbac.yaml | 26 +++++++++++++++++++ 3 files changed, 29 insertions(+), 26 deletions(-) create mode 100644 packages/system/velero/templates/backupstrategy-controller-rbac.yaml diff --git a/packages/system/backupstrategy-controller/templates/rbac-bind.yaml b/packages/system/backupstrategy-controller/templates/rbac-bind.yaml index 4ecacce0..03578cc2 100644 --- a/packages/system/backupstrategy-controller/templates/rbac-bind.yaml +++ b/packages/system/backupstrategy-controller/templates/rbac-bind.yaml @@ -10,17 +10,3 @@ subjects: - kind: ServiceAccount name: backupstrategy-controller namespace: {{ .Release.Namespace }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: backups.cozystack.io:strategy-controller:velero-configmaps - namespace: cozy-velero -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: backups.cozystack.io:strategy-controller:velero-configmaps -subjects: -- kind: ServiceAccount - name: backupstrategy-controller - namespace: {{ .Release.Namespace }} diff --git a/packages/system/backupstrategy-controller/templates/rbac.yaml b/packages/system/backupstrategy-controller/templates/rbac.yaml index bbdb74f8..634ea88b 100644 --- a/packages/system/backupstrategy-controller/templates/rbac.yaml +++ b/packages/system/backupstrategy-controller/templates/rbac.yaml @@ -27,7 +27,9 @@ rules: resources: ["pods"] verbs: ["get", "list", "watch"] # ConfigMaps: controller-runtime cache requires cluster-scoped list/watch; -# create/update/delete is scoped to cozy-velero via the Role below. +# create/update/delete is scoped to cozy-velero via a Role shipped by the +# velero chart (packages/system/velero/templates/backupstrategy-controller-rbac.yaml) +# so it is only created when velero is installed. - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "watch"] @@ -78,14 +80,3 @@ rules: - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["get", "list", "watch", "create", "update", "patch"] ---- -# To create ResourceModifiers in ConfigMaps for Restore in Velero install namespace. -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: backups.cozystack.io:strategy-controller:velero-configmaps - namespace: cozy-velero -rules: -- apiGroups: [""] - resources: ["configmaps"] - verbs: ["create", "get", "list", "delete", "deletecollection", "patch", "update"] diff --git a/packages/system/velero/templates/backupstrategy-controller-rbac.yaml b/packages/system/velero/templates/backupstrategy-controller-rbac.yaml new file mode 100644 index 00000000..43c51cd7 --- /dev/null +++ b/packages/system/velero/templates/backupstrategy-controller-rbac.yaml @@ -0,0 +1,26 @@ +# Grants the backupstrategy-controller permission to manage ResourceModifier +# ConfigMaps in the Velero install namespace. Lives here (not in the +# backupstrategy-controller chart) so that the Role is only created when +# velero is actually installed — otherwise the chart would try to create it +# in a namespace that does not exist. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: backups.cozystack.io:strategy-controller:velero-configmaps +rules: +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["create", "get", "list", "delete", "deletecollection", "patch", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: backups.cozystack.io:strategy-controller:velero-configmaps +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: backups.cozystack.io:strategy-controller:velero-configmaps +subjects: +- kind: ServiceAccount + name: backupstrategy-controller + namespace: cozy-backup-controller From ed89553023791c9eefc46b9270061b816ca7fdce Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Sun, 19 Apr 2026 19:01:53 +0300 Subject: [PATCH 02/11] fix(kamaji): increase memory limits and add startup probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Increase memory limit from 500Mi to 512Mi - Increase memory request from 100Mi to 256Mi - Add startup probe with 60s timeout (12 attempts × 5s) - Increase readiness/liveness initialDelaySeconds from 5/15 to 30s This fixes OOMKilled crashes observed in production where kamaji controller was being killed due to insufficient memory during startup. Signed-off-by: IvanHunters (cherry picked from commit e148343fd9bf9440d726bf01ebb07374119927d0) --- packages/system/kamaji/values.yaml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/system/kamaji/values.yaml b/packages/system/kamaji/values.yaml index 4de74113..6ba649f0 100644 --- a/packages/system/kamaji/values.yaml +++ b/packages/system/kamaji/values.yaml @@ -8,9 +8,32 @@ kamaji: resources: limits: cpu: 200m - memory: 500Mi + memory: 512Mi requests: cpu: 100m - memory: 100Mi + memory: 256Mi + startupProbe: + httpGet: + path: /healthz + port: healthcheck + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 12 + livenessProbe: + httpGet: + path: /healthz + port: healthcheck + initialDelaySeconds: 30 + periodSeconds: 20 + timeoutSeconds: 1 + readinessProbe: + httpGet: + path: /readyz + port: healthcheck + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 1 extraArgs: - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.3.0@sha256:8c9af083b60600c0800eb56a2cda75f26007b7272a1cf019140de003bfce1a4d From f384be5475a34e8a772780dfea1cd996796f6b35 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Mon, 27 Apr 2026 11:32:17 +0200 Subject: [PATCH 03/11] feat(linstor): bump linstor-csi to v1.10.6 with Protocol-C dual-attach fix Bump linstor-csi to v1.10.6 and add an out-of-tree patch that overrides DrbdOptions/Net/protocol to C on the resource-definition whenever a volume is attached to a second node with allow-two-primaries (the live-migration flow), and reverts it on detach. Without the patch, any KubeVirt live migration of a VM whose volume sits on a Protocol-A/B resource (for example a "replicated-async" StorageClass) ends up in a permanent evacuation loop: drbdadm adjust on the satellite rejects allow-two-primaries with "Protocol C required" (errno 139), the migration fails, and KubeVirt retries until manual intervention. The override is set at the resource-definition level so it covers every connection, including diskless TieBreaker peers, and is tagged with an Aux marker so Detach reverts only the override we installed. The 001 relocate-after-clone patch was regenerated against v1.10.6 (context shift only, no logic change). Verified end-to-end on dev5: live migration of a KubeVirt VM whose PVC sits on a Protocol-A resource-group now succeeds in a single Attach. Patch is upstreamed as draft PR piraeusdatastore/linstor-csi#435. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil (cherry picked from commit 073fb1630d67ace6655deb262d1734fab98e8ce4) --- packages/system/linstor/Makefile | 2 +- .../linstor/images/linstor-csi/Dockerfile | 2 +- .../001-relocate-after-clone-restore.diff | 62 +------- ...2-protocol-c-override-for-dual-attach.diff | 132 ++++++++++++++++++ 4 files changed, 140 insertions(+), 58 deletions(-) create mode 100644 packages/system/linstor/images/linstor-csi/patches/002-protocol-c-override-for-dual-attach.diff diff --git a/packages/system/linstor/Makefile b/packages/system/linstor/Makefile index 2ad4def9..8d566678 100644 --- a/packages/system/linstor/Makefile +++ b/packages/system/linstor/Makefile @@ -5,7 +5,7 @@ include ../../../hack/common-envs.mk include ../../../hack/package.mk LINSTOR_VERSION ?= 1.33.2 -LINSTOR_CSI_VERSION ?= v1.10.5 +LINSTOR_CSI_VERSION ?= v1.10.6 image: image-piraeus-server image-linstor-csi diff --git a/packages/system/linstor/images/linstor-csi/Dockerfile b/packages/system/linstor/images/linstor-csi/Dockerfile index 6cfd44aa..df5d179d 100644 --- a/packages/system/linstor/images/linstor-csi/Dockerfile +++ b/packages/system/linstor/images/linstor-csi/Dockerfile @@ -1,6 +1,6 @@ FROM golang:1.25 AS builder -ARG VERSION=v1.10.5 +ARG VERSION=v1.10.6 ARG LINSTOR_WAIT_UNTIL_VERSION=v0.3.1 ARG TARGETARCH ARG TARGETOS diff --git a/packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff b/packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff index ff0aec0c..0e272670 100644 --- a/packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff +++ b/packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff @@ -1,25 +1,7 @@ diff --git a/pkg/client/linstor.go b/pkg/client/linstor.go -index f544493..98e7fde 100644 +index ac4651d..46d565d 100644 --- a/pkg/client/linstor.go +++ b/pkg/client/linstor.go -@@ -181,7 +181,7 @@ func LogLevel(s string) func(*Linstor) error { - func (s *Linstor) ListAllWithStatus(ctx context.Context) ([]volume.VolumeStatus, error) { - var vols []volume.VolumeStatus - -- resourcesByName := make(map[string][]lapi.Resource) -+ resourcesByName := make(map[string][]lapi.ResourceWithVolumes) - - resDefs, err := s.client.ResourceDefinitions.GetAll(ctx, lapi.RDGetAllRequest{WithVolumeDefinitions: true}) - if err != nil { -@@ -194,7 +194,7 @@ func (s *Linstor) ListAllWithStatus(ctx context.Context) ([]volume.VolumeStatus, - } - - for i := range allResources { -- resourcesByName[allResources[i].Name] = append(resourcesByName[allResources[i].Name], allResources[i].Resource) -+ resourcesByName[allResources[i].Name] = append(resourcesByName[allResources[i].Name], allResources[i]) - } - - for _, rd := range resDefs { @@ -462,6 +462,14 @@ func (s *Linstor) Clone(ctx context.Context, vol, src *volume.Info, params *volu return err } @@ -35,7 +17,7 @@ index f544493..98e7fde 100644 logger.Debug("reconcile extra properties") err = s.client.ResourceDefinitions.Modify(ctx, vol.ID, lapi.GenericPropsModify{OverrideProps: vol.Properties}) -@@ -1280,6 +1288,14 @@ func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *v +@@ -1297,6 +1305,14 @@ func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *v return err } @@ -50,7 +32,7 @@ index f544493..98e7fde 100644 logger.Debug("reconcile extra properties") err = s.client.ResourceDefinitions.Modify(ctx, vol.ID, lapi.GenericPropsModify{OverrideProps: vol.Properties}) -@@ -1470,9 +1486,8 @@ func (s *Linstor) reconcileSnapshotResources(ctx context.Context, snapshot *volu +@@ -1487,9 +1503,8 @@ func (s *Linstor) reconcileSnapshotResources(ctx context.Context, snapshot *volu return fmt.Errorf("snapshot '%s' not deployed on any node", snap.Name) } @@ -62,7 +44,7 @@ index f544493..98e7fde 100644 for _, snapNode := range snap.Nodes { if err := s.NodeAvailable(ctx, snapNode); err != nil { logger.WithField("selected node candidate", snapNode).WithError(err).Debug("node is not available") -@@ -1480,13 +1495,23 @@ func (s *Linstor) reconcileSnapshotResources(ctx context.Context, snapshot *volu +@@ -1497,13 +1512,23 @@ func (s *Linstor) reconcileSnapshotResources(ctx context.Context, snapshot *volu } if slices.Contains(preferredNodes, snapNode) { @@ -92,7 +74,7 @@ index f544493..98e7fde 100644 } if selectedNode == "" { -@@ -1679,6 +1704,114 @@ func (s *Linstor) reconcileResourcePlacement(ctx context.Context, vol *volume.In +@@ -1696,6 +1721,114 @@ func (s *Linstor) reconcileResourcePlacement(ctx context.Context, vol *volume.In return nil } @@ -207,40 +189,8 @@ index f544493..98e7fde 100644 // FindSnapsByID searches the snapshot in the backend func (s *Linstor) FindSnapsByID(ctx context.Context, id string) ([]*volume.Snapshot, error) { snapshotId, err := volume.ParseSnapshotId(id) -@@ -2173,7 +2306,7 @@ func (s *Linstor) Status(ctx context.Context, volId string) ([]string, *csi.Volu - "volume": volId, - }).Debug("getting assignments") - -- ress, err := s.client.Resources.GetAll(ctx, volId) -+ ress, err := s.client.Resources.GetResourceView(ctx, &lapi.ListOpts{Resource: []string{volId}}) - if err != nil { - return nil, nil, fmt.Errorf("failed to list resources for '%s': %w", volId, err) - } -@@ -2261,13 +2394,20 @@ func GetSnapshotRemoteAndReadiness(snap *lapi.Snapshot) (string, bool, error) { - return "", slices.Contains(snap.Flags, lapiconsts.FlagSuccessful), nil - } - --func NodesAndConditionFromResources(ress []lapi.Resource) ([]string, *csi.VolumeCondition) { -+func NodesAndConditionFromResources(ress []lapi.ResourceWithVolumes) ([]string, *csi.VolumeCondition) { - var allNodes, abnormalNodes []string - - for i := range ress { - res := &ress[i] - -- allNodes = append(allNodes, res.NodeName) -+ // A resource is a CSI publish target if any of its volumes were created -+ // by ControllerPublishVolume, identified by the temporary-diskless-attach property. -+ if slices.ContainsFunc(res.Volumes, func(v lapi.Volume) bool { -+ createdFor, ok := v.Props[linstor.PropertyCreatedFor] -+ return ok && createdFor == linstor.CreatedForTemporaryDisklessAttach -+ }) { -+ allNodes = append(allNodes, res.NodeName) -+ } - - if res.State == nil { - abnormalNodes = append(abnormalNodes, res.NodeName) diff --git a/pkg/volume/parameter.go b/pkg/volume/parameter.go -index 39acd95..aed18ab 100644 +index 39acd95..54d1dfb 100644 --- a/pkg/volume/parameter.go +++ b/pkg/volume/parameter.go @@ -50,6 +50,7 @@ const ( diff --git a/packages/system/linstor/images/linstor-csi/patches/002-protocol-c-override-for-dual-attach.diff b/packages/system/linstor/images/linstor-csi/patches/002-protocol-c-override-for-dual-attach.diff new file mode 100644 index 00000000..279c636b --- /dev/null +++ b/packages/system/linstor/images/linstor-csi/patches/002-protocol-c-override-for-dual-attach.diff @@ -0,0 +1,132 @@ +diff --git a/pkg/client/linstor.go b/pkg/client/linstor.go +index ac4651d..9d75c79 100644 +--- a/pkg/client/linstor.go ++++ b/pkg/client/linstor.go +@@ -653,11 +653,35 @@ func (s *Linstor) Attach(ctx context.Context, volId, node string, rwxBlock bool) + } + + if otherResInUse > 0 && rwxBlock { +- rdPropsModify := lapi.GenericPropsModify{OverrideProps: map[string]string{ ++ rdProps := map[string]string{ + linstor.PropertyAllowTwoPrimaries: "yes", +- }} ++ } + +- err = s.client.ResourceDefinitions.Modify(ctx, volId, rdPropsModify) ++ // DRBD requires Protocol C whenever allow-two-primaries is enabled. ++ // If the resource is configured with Protocol A or B (typically ++ // through its resource-group), drbdadm adjust on the satellites ++ // fails with "Protocol C required" (errno 139) and live migration ++ // cannot proceed. Override Protocol to C on the resource-definition ++ // itself so it applies to every connection (including diskless ++ // peers, e.g. a TieBreaker). The marker lets Detach revert exactly ++ // the override we installed without disturbing operator-set props. ++ proto, protoErr := s.getEffectiveDrbdProtocol(ctx, volId) ++ if protoErr != nil { ++ s.log.WithError(protoErr).WithField("volume", volId). ++ Warn("failed to determine effective DRBD protocol; skipping Protocol=C override for dual-attach") ++ } else if proto == "A" || proto == "B" { ++ s.log.WithFields(logrus.Fields{ ++ "volume": volId, ++ "protocol": proto, ++ }).Info("installing Protocol=C override on resource-definition for dual-attach") ++ ++ rdProps[linstor.PropertyDrbdNetProtocol] = "C" ++ rdProps[linstor.PropertyCsiProtocolOverride] = "yes" ++ } ++ ++ err = s.client.ResourceDefinitions.Modify(ctx, volId, lapi.GenericPropsModify{ ++ OverrideProps: rdProps, ++ }) + if err != nil { + return "", err + } +@@ -774,11 +798,26 @@ func (s *Linstor) Detach(ctx context.Context, volId, node string) error { + } + + if resInUse == 1 { +- rdPropsModify := lapi.GenericPropsModify{DeleteProps: []string{ +- linstor.PropertyAllowTwoPrimaries, +- }} ++ deleteProps := []string{linstor.PropertyAllowTwoPrimaries} ++ ++ // Drop the Protocol=C override only if Attach installed it (gated by ++ // our marker), so an operator-set Protocol property on the ++ // resource-definition is preserved. ++ rd, getErr := s.client.ResourceDefinitions.Get(ctx, volId) ++ if getErr != nil { ++ log.WithError(getErr).Warn("failed to fetch resource-definition props; skipping Protocol=C override removal") ++ } else if rd.Props[linstor.PropertyCsiProtocolOverride] != "" { ++ log.Info("removing Protocol=C override from resource-definition") + +- err = s.client.ResourceDefinitions.Modify(ctx, volId, rdPropsModify) ++ deleteProps = append(deleteProps, ++ linstor.PropertyDrbdNetProtocol, ++ linstor.PropertyCsiProtocolOverride, ++ ) ++ } ++ ++ err = s.client.ResourceDefinitions.Modify(ctx, volId, lapi.GenericPropsModify{ ++ DeleteProps: deleteProps, ++ }) + if err != nil { + return nil404(err) + } +@@ -805,6 +844,33 @@ func (s *Linstor) Detach(ctx context.Context, volId, node string) error { + return nil404(s.client.Resources.Delete(ctx, volId, node)) + } + ++// getEffectiveDrbdProtocol returns the DRBD network protocol ("A", "B" or "C") ++// that LINSTOR will hand to drbdadm for the given resource. Resource-definition ++// properties take precedence; otherwise the value is inherited from the ++// resource-group. An empty string is returned when neither level sets the ++// property (LINSTOR's compiled-in default is "C"). ++func (s *Linstor) getEffectiveDrbdProtocol(ctx context.Context, volId string) (string, error) { ++ rd, err := s.client.ResourceDefinitions.Get(ctx, volId) ++ if err != nil { ++ return "", err ++ } ++ ++ if v, ok := rd.Props[linstor.PropertyDrbdNetProtocol]; ok { ++ return v, nil ++ } ++ ++ if rd.ResourceGroupName == "" { ++ return "", nil ++ } ++ ++ rg, err := s.client.ResourceGroups.Get(ctx, rd.ResourceGroupName) ++ if err != nil { ++ return "", err ++ } ++ ++ return rg.Props[linstor.PropertyDrbdNetProtocol], nil ++} ++ + // CapacityBytes returns the amount of free space in the storage pool specified by the params and topology. + func (s *Linstor) CapacityBytes(ctx context.Context, storagePools []string, overProvision *float64, segments map[string]string) (int64, error) { + log := s.log.WithField("storage-pools", storagePools).WithField("segments", segments) +diff --git a/pkg/linstor/const.go b/pkg/linstor/const.go +index 9a5f79c..c8bc9c3 100644 +--- a/pkg/linstor/const.go ++++ b/pkg/linstor/const.go +@@ -44,6 +44,19 @@ const ( + // PropertyAllowTwoPrimaries is DRBD option to allow second primary. Mainly used for live-migration. + PropertyAllowTwoPrimaries = lc.NamespcDrbdNetOptions + "/allow-two-primaries" + ++ // PropertyDrbdNetProtocol is the DRBD network replication protocol option ++ // (A = async, B = semi-sync, C = sync). DRBD requires Protocol C whenever ++ // allow-two-primaries is enabled, otherwise drbdadm adjust fails with ++ // "Protocol C required" (errno 139). ++ PropertyDrbdNetProtocol = lc.NamespcDrbdNetOptions + "/protocol" ++ ++ // PropertyCsiProtocolOverride marks a resource-connection where this driver ++ // has installed a temporary Protocol=C override to make a Protocol-A/B ++ // resource compatible with allow-two-primaries during live migration. The ++ // marker lets us safely remove only the overrides we set, without touching ++ // connection-level overrides installed by the operator. ++ PropertyCsiProtocolOverride = lc.NamespcAuxiliary + "/csi-protocol-override" ++ + // CreatedForTemporaryDisklessAttach marks a resource as temporary, i.e. it should be removed after it is no longer + // needed. + CreatedForTemporaryDisklessAttach = "temporary-diskless-attach" From cbc1606c41d7b497468c37f3140a23be3b70227a Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Thu, 23 Apr 2026 13:34:21 +0500 Subject: [PATCH 04/11] fix(etcd): remove destructive post-upgrade cert-regeneration hook The etcd chart shipped a `post-upgrade` Helm hook that `kubectl delete`d the etcd TLS chain (`etcd-{ca,peer-ca,client,peer,server}-tls`) and then deleted etcd pods on every chart upgrade, gated by a semver compare of an `etcd-deployed-version` ConfigMap against `2.6.1`. The hook was added as a one-shot migration for the chart `2.6.0 -> 2.6.1` transition. Since commit f871fbdb ("Remove versions_map logic") all chart versions are stamped as `0.0.0+`, which per semver is always `< 2.6.1`. The gate therefore always resolves to "update certs", firing the destructive hook on every etcd upgrade. On clusters running Kamaji-managed tenant control planes this wipes the etcd CA, cert-manager re-issues it, and tenant kube-apiservers hit `x509: certificate signed by unknown authority` against `etcd..svc:2379` until each tenant DataStore is manually re-reconciled. Commit 47d81f70 ("Disabled private key rotation in CA certs") already fixed the underlying `rotationPolicy: Always` issue the migration was papering over, so the hook has no remaining use. Remove the hook Job, its RBAC, the version ConfigMap it read, and add a helm-unittest suite under `packages/extra/etcd/tests/` that guards against re-introducing the hook or the version ConfigMap. Assisted-By: Claude Signed-off-by: Myasnikov Daniil (cherry picked from commit c1508940bda27b2979639981fd25461c9d4824b3) --- packages/extra/etcd/Makefile | 3 ++ packages/extra/etcd/templates/hook/job.yaml | 39 ------------------- packages/extra/etcd/templates/hook/role.yaml | 26 ------------- .../etcd/templates/hook/rolebinding.yaml | 15 ------- .../etcd/templates/hook/serviceaccount.yaml | 7 ---- packages/extra/etcd/templates/version.yaml | 6 --- .../etcd/tests/no-post-upgrade-hook_test.yaml | 34 ++++++++++++++++ 7 files changed, 37 insertions(+), 93 deletions(-) delete mode 100644 packages/extra/etcd/templates/hook/job.yaml delete mode 100644 packages/extra/etcd/templates/hook/role.yaml delete mode 100644 packages/extra/etcd/templates/hook/rolebinding.yaml delete mode 100644 packages/extra/etcd/templates/hook/serviceaccount.yaml delete mode 100644 packages/extra/etcd/templates/version.yaml create mode 100644 packages/extra/etcd/tests/no-post-upgrade-hook_test.yaml diff --git a/packages/extra/etcd/Makefile b/packages/extra/etcd/Makefile index f37d6e1e..2b3ed61e 100644 --- a/packages/extra/etcd/Makefile +++ b/packages/extra/etcd/Makefile @@ -5,3 +5,6 @@ include ../../../hack/package.mk generate: cozyvalues-gen -v values.yaml -s values.schema.json -r README.md ../../../hack/update-crd.sh + +test: + helm unittest . diff --git a/packages/extra/etcd/templates/hook/job.yaml b/packages/extra/etcd/templates/hook/job.yaml deleted file mode 100644 index 3bf2f84b..00000000 --- a/packages/extra/etcd/templates/hook/job.yaml +++ /dev/null @@ -1,39 +0,0 @@ -{{- $shouldUpdateCerts := true }} -{{- $configMap := lookup "v1" "ConfigMap" .Release.Namespace "etcd-deployed-version" }} -{{- if $configMap }} - {{- $deployedVersion := index $configMap "data" "version" }} - {{- if $deployedVersion | semverCompare ">= 2.6.1" }} - {{- $shouldUpdateCerts = false }} - {{- end }} -{{- end }} - -{{- if $shouldUpdateCerts }} ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: etcd-hook - annotations: - helm.sh/hook: post-upgrade - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -spec: - template: - metadata: - labels: - policy.cozystack.io/allow-to-apiserver: "true" - spec: - serviceAccountName: etcd-hook - containers: - - name: kubectl - image: docker.io/alpine/k8s:1.33.4 - command: - - sh - args: - - -exc - - |- - kubectl --namespace={{ .Release.Namespace }} delete secrets etcd-ca-tls etcd-peer-ca-tls - sleep 10 - kubectl --namespace={{ .Release.Namespace }} delete secrets etcd-client-tls etcd-peer-tls etcd-server-tls - kubectl --namespace={{ .Release.Namespace }} delete pods --selector=app.kubernetes.io/instance=etcd,app.kubernetes.io/managed-by=etcd-operator,app.kubernetes.io/name=etcd,cozystack.io/service=etcd - restartPolicy: Never -{{- end }} diff --git a/packages/extra/etcd/templates/hook/role.yaml b/packages/extra/etcd/templates/hook/role.yaml deleted file mode 100644 index 327eeadb..00000000 --- a/packages/extra/etcd/templates/hook/role.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - annotations: - helm.sh/hook: post-upgrade - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded - name: etcd-hook -rules: -- apiGroups: - - "" - resources: - - secrets - - pods - verbs: - - get - - list - - watch - - delete -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch diff --git a/packages/extra/etcd/templates/hook/rolebinding.yaml b/packages/extra/etcd/templates/hook/rolebinding.yaml deleted file mode 100644 index 0ee0ffd1..00000000 --- a/packages/extra/etcd/templates/hook/rolebinding.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: etcd-hook - annotations: - helm.sh/hook: post-upgrade - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: etcd-hook -subjects: - - kind: ServiceAccount - name: etcd-hook - namespace: {{ .Release.Namespace | quote }} diff --git a/packages/extra/etcd/templates/hook/serviceaccount.yaml b/packages/extra/etcd/templates/hook/serviceaccount.yaml deleted file mode 100644 index 552fb5fc..00000000 --- a/packages/extra/etcd/templates/hook/serviceaccount.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: etcd-hook - annotations: - helm.sh/hook: post-upgrade - helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded diff --git a/packages/extra/etcd/templates/version.yaml b/packages/extra/etcd/templates/version.yaml deleted file mode 100644 index cc9375bb..00000000 --- a/packages/extra/etcd/templates/version.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: etcd-deployed-version -data: - version: {{ .Chart.Version }} diff --git a/packages/extra/etcd/tests/no-post-upgrade-hook_test.yaml b/packages/extra/etcd/tests/no-post-upgrade-hook_test.yaml new file mode 100644 index 00000000..0e4f5aa3 --- /dev/null +++ b/packages/extra/etcd/tests/no-post-upgrade-hook_test.yaml @@ -0,0 +1,34 @@ +suite: etcd chart does not ship a destructive post-upgrade cert-regeneration hook + +release: + name: etcd + namespace: tenant-root + +templates: + - templates/check-release-name.yaml + - templates/dashboard-resourcemap.yaml + - templates/datastore.yaml + - templates/etcd-defrag.yaml + - templates/hook/job.yaml + - templates/podscrape.yaml + - templates/prometheus-rules.yaml + - templates/version.yaml + +tests: + - it: renders no Job named etcd-hook + documentSelector: + path: metadata.name + value: etcd-hook + skipEmptyTemplates: true + asserts: + - hasDocuments: + count: 0 + + - it: renders no ConfigMap named etcd-deployed-version + documentSelector: + path: metadata.name + value: etcd-deployed-version + skipEmptyTemplates: true + asserts: + - hasDocuments: + count: 0 From f6da822cd0fd3064702489b78e2d6261b22e3ee1 Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Mon, 27 Apr 2026 17:18:45 +0500 Subject: [PATCH 05/11] build(linstor): include linstor-gui in root image build target The linstor-gui package was added in #2382 with its own per-package Makefile and Dockerfile, but the root Makefile's `build:` target was not updated to invoke it. As a result `ghcr.io/cozystack/cozystack/ linstor-gui` has never been published (registry returns NAME_UNKNOWN) and the chart's `image.tag` was never digest-pinned. Any cluster deploying the chart hits ImagePullBackOff. Wire the package into the root build alongside the other system images. The next CI build will publish the image and the per-package Makefile will rewrite values.yaml `image.repository`/`image.tag` to a digest-pinned reference automatically. Assisted-By: Claude Signed-off-by: Myasnikov Daniil (cherry picked from commit 7443e223453032d0a907311f3b5865889b713299) --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 0e94c5e6..5041ac64 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ build: build-deps make -C packages/system/lineage-controller-webhook image make -C packages/system/cilium image make -C packages/system/linstor image + make -C packages/system/linstor-gui image make -C packages/system/kubeovn-webhook image make -C packages/system/kubeovn-plunger image make -C packages/system/dashboard image From fe577a26c60126b42d9ed8464649ffdb3fe67088 Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Mon, 27 Apr 2026 17:31:07 +0500 Subject: [PATCH 06/11] build(linstor): use shared BUILDX_ARGS for linstor-gui image build The per-package Makefile added in #2382 hardcoded buildx flags (--provenance, --builder, --platform=linux/amd64,linux/arm64, --push, --load, --label) instead of using the shared $(BUILDX_ARGS) macro from hack/common-envs.mk. This broke CI: the runner's default docker driver does not support multi-platform builds, and the hardcoded multi-arch platform list crashed `make build` with "Multi-platform build is not supported for the docker driver." Replace the hardcoded flags with $(BUILDX_ARGS) to match every other package (e.g. linstor, dashboard, cilium). $(BUILDX_ARGS) injects --push, --load, --label, --provenance=false, and only sets --builder or --platform when the operator explicitly exports BUILDER/PLATFORM. Assisted-By: Claude Signed-off-by: Myasnikov Daniil (cherry picked from commit a9a66bf066360c340203508fd6072977ff4eb8ea) --- packages/system/linstor-gui/Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/system/linstor-gui/Makefile b/packages/system/linstor-gui/Makefile index e202f4cc..b408491d 100644 --- a/packages/system/linstor-gui/Makefile +++ b/packages/system/linstor-gui/Makefile @@ -12,18 +12,13 @@ image: image-linstor-gui image-linstor-gui: docker buildx build images/linstor-gui \ - --provenance false \ - --builder=$(BUILDER) \ - --platform=linux/amd64,linux/arm64 \ --build-arg LINSTOR_GUI_VERSION=$(LINSTOR_GUI_VERSION) \ --tag $(REGISTRY)/linstor-gui:$(call settag,$(LINSTOR_GUI_VERSION)) \ --tag $(REGISTRY)/linstor-gui:$(call settag,$(LINSTOR_GUI_VERSION)-$(TAG)) \ --cache-from type=registry,ref=$(REGISTRY)/linstor-gui:latest \ --cache-to type=inline \ --metadata-file images/linstor-gui.json \ - --push=$(PUSH) \ - --label "org.opencontainers.image.source=https://github.com/cozystack/cozystack" \ - --load=$(LOAD) + $(BUILDX_ARGS) REPOSITORY="$(REGISTRY)/linstor-gui" \ yq -i '.image.repository = strenv(REPOSITORY)' values.yaml TAG="$(call settag,$(LINSTOR_GUI_VERSION))@$$(yq e '."containerimage.digest"' images/linstor-gui.json -o json -r)" \ From 82fb7990d5d92038b08472fe72a3d4f765ce09b9 Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Thu, 23 Apr 2026 18:13:10 +0300 Subject: [PATCH 07/11] fix(api): prevent IDOR in TenantNamespace Get and Watch handlers Fixed two IDOR vulnerabilities allowing authenticated users to access metadata of any tenant namespace without proper authorization. Changes: - Added hasAccessToNamespace() for efficient single-namespace access checks - Get() now verifies access before returning namespace metadata - Watch() filters events per-namespace with proper authorization - Returns NotFound (not Forbidden) to prevent tenant enumeration Performance optimization: - hasAccessToNamespace() lists RoleBindings only in target namespace instead of listing all cluster RoleBindings (order of magnitude faster) - Watch handler logs authorization errors for security audit Additional fixes: - Handle ServiceAccount subjects with empty namespace correctly - Add klog error logging for failed authorization checks Signed-off-by: IvanHunters (cherry picked from commit 64a3edff01533a0483db31686a6385dffa81ee55) --- pkg/registry/core/tenantnamespace/rest.go | 89 ++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/pkg/registry/core/tenantnamespace/rest.go b/pkg/registry/core/tenantnamespace/rest.go index f1ed3fab..7724c0e1 100644 --- a/pkg/registry/core/tenantnamespace/rest.go +++ b/pkg/registry/core/tenantnamespace/rest.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" @@ -123,8 +124,18 @@ func (r *REST) Get( return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) } + // Check if user has access to this namespace + hasAccess, err := r.hasAccessToNamespace(ctx, name) + if err != nil { + return nil, err + } + if !hasAccess { + // Return NotFound instead of Forbidden to prevent enumeration + return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) + } + ns := &corev1.Namespace{} - err := r.c.Get(ctx, types.NamespacedName{Namespace: "", Name: name}, ns, &client.GetOptions{Raw: opts}) + err = r.c.Get(ctx, types.NamespacedName{Namespace: "", Name: name}, ns, &client.GetOptions{Raw: opts}) if err != nil { return nil, err } @@ -189,6 +200,17 @@ func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch continue } + // Check if user has access to this namespace + hasAccess, err := r.hasAccessToNamespace(ctx, ns.Name) + if err != nil { + klog.Errorf("Failed to check access for namespace %s in watch: %v", ns.Name, err) + continue + } + if !hasAccess { + // User doesn't have access, skip this event + continue + } + out := &corev1alpha1.TenantNamespace{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1alpha1.SchemeGroupVersion.String(), @@ -359,7 +381,11 @@ func (r *REST) filterAccessible( break subjectLoop } case "ServiceAccount": - if u.GetName() == fmt.Sprintf("system:serviceaccount:%s:%s", subj.Namespace, subj.Name) { + saNamespace := subj.Namespace + if saNamespace == "" { + saNamespace = rbs.Items[i].Namespace + } + if u.GetName() == fmt.Sprintf("system:serviceaccount:%s:%s", saNamespace, subj.Name) { allowedNameSet[rbs.Items[i].Namespace] = struct{}{} break subjectLoop } @@ -373,6 +399,65 @@ func (r *REST) filterAccessible( return allowed, nil } +// hasAccessToNamespace checks if the user has access to a single namespace. +// This is optimized for Get/Watch operations where we check one namespace at a time. +// It lists RoleBindings only in the target namespace instead of all cluster RoleBindings. +func (r *REST) hasAccessToNamespace( + ctx context.Context, + namespace string, +) (bool, error) { + u, ok := request.UserFrom(ctx) + if !ok { + return false, fmt.Errorf("user missing in context") + } + + // Check privileged groups + groups := make(map[string]struct{}) + for _, group := range u.GetGroups() { + groups[group] = struct{}{} + } + if _, ok := groups["system:masters"]; ok { + return true, nil + } + if _, ok := groups["cozystack-cluster-admin"]; ok { + return true, nil + } + + // List RoleBindings only in the target namespace + rbs := &rbacv1.RoleBindingList{} + err := r.c.List(ctx, rbs, client.InNamespace(namespace)) + if err != nil { + return false, fmt.Errorf("failed to list rolebindings in %s: %w", namespace, err) + } + + // Check if user is in any RoleBinding subjects + for i := range rbs.Items { + for j := range rbs.Items[i].Subjects { + subj := rbs.Items[i].Subjects[j] + switch subj.Kind { + case "Group": + if _, ok := groups[subj.Name]; ok { + return true, nil + } + case "User": + if subj.Name == u.GetName() { + return true, nil + } + case "ServiceAccount": + saNamespace := subj.Namespace + if saNamespace == "" { + saNamespace = rbs.Items[i].Namespace + } + if u.GetName() == fmt.Sprintf("system:serviceaccount:%s:%s", saNamespace, subj.Name) { + return true, nil + } + } + } + } + + return false, nil +} + // ----------------------------------------------------------------------------- // Boiler-plate // ----------------------------------------------------------------------------- From 79c23d895a843f7259c9eadcecb986243ded0391 Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Thu, 23 Apr 2026 18:17:47 +0300 Subject: [PATCH 08/11] test(api): add security tests for TenantNamespace IDOR fix Added comprehensive unit tests for authorization logic: - hasAccessToNamespace() tests: - User subject access (positive and negative cases) - Group subject access - ServiceAccount subject access - ServiceAccount with empty namespace (defaults to RoleBinding ns) - Privileged groups (system:masters, cozystack-cluster-admin) - Get() handler tests: - Returns namespace when user has access - Returns NotFound when user lacks access (not Forbidden) - Returns NotFound for non-tenant namespaces All tests verify that authorization correctly enforces RoleBinding-based access control and prevents IDOR vulnerabilities. Signed-off-by: IvanHunters (cherry picked from commit 5b2501db91c82d8f9ce1d7187325927f7504847c) --- .../core/tenantnamespace/rest_test.go | 461 ++++++++++++++++++ 1 file changed, 461 insertions(+) diff --git a/pkg/registry/core/tenantnamespace/rest_test.go b/pkg/registry/core/tenantnamespace/rest_test.go index 7f2979bc..eb678949 100644 --- a/pkg/registry/core/tenantnamespace/rest_test.go +++ b/pkg/registry/core/tenantnamespace/rest_test.go @@ -3,10 +3,18 @@ package tenantnamespace import ( + "context" "testing" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestMakeListSortsAlphabetically(t *testing.T) { @@ -38,3 +46,456 @@ func TestMakeListSortsAlphabetically(t *testing.T) { } } } + +// Security tests for IDOR fix + +func TestHasAccessToNamespace_WithUserAccess(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "tenant-test"}, + } + + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "tenant-test", + }, + Subjects: []rbacv1.Subject{ + {Kind: "User", Name: "test-user", APIGroup: "rbac.authorization.k8s.io"}, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-role", + }, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, rb). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "test-user", + Groups: []string{"system:authenticated"}, + } + ctx := request.WithUser(context.Background(), u) + + hasAccess, err := r.hasAccessToNamespace(ctx, "tenant-test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !hasAccess { + t.Error("expected user to have access, but got false") + } +} + +func TestHasAccessToNamespace_WithoutAccess(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "tenant-test"}, + } + + // RoleBinding for different user + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "tenant-test", + }, + Subjects: []rbacv1.Subject{ + {Kind: "User", Name: "other-user", APIGroup: "rbac.authorization.k8s.io"}, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-role", + }, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, rb). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "test-user", + Groups: []string{"system:authenticated"}, + } + ctx := request.WithUser(context.Background(), u) + + hasAccess, err := r.hasAccessToNamespace(ctx, "tenant-test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if hasAccess { + t.Error("expected user to NOT have access, but got true") + } +} + +func TestHasAccessToNamespace_WithGroupAccess(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "tenant-test"}, + } + + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "tenant-test", + }, + Subjects: []rbacv1.Subject{ + {Kind: "Group", Name: "test-group", APIGroup: "rbac.authorization.k8s.io"}, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-role", + }, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, rb). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "test-user", + Groups: []string{"system:authenticated", "test-group"}, + } + ctx := request.WithUser(context.Background(), u) + + hasAccess, err := r.hasAccessToNamespace(ctx, "tenant-test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !hasAccess { + t.Error("expected user to have access via group, but got false") + } +} + +func TestHasAccessToNamespace_SystemMasters(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "tenant-test"}, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "admin", + Groups: []string{"system:masters"}, + } + ctx := request.WithUser(context.Background(), u) + + hasAccess, err := r.hasAccessToNamespace(ctx, "tenant-test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !hasAccess { + t.Error("expected system:masters to have access, but got false") + } +} + +func TestHasAccessToNamespace_CozyAdminGroup(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "tenant-test"}, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "cozy-admin", + Groups: []string{"cozystack-cluster-admin"}, + } + ctx := request.WithUser(context.Background(), u) + + hasAccess, err := r.hasAccessToNamespace(ctx, "tenant-test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !hasAccess { + t.Error("expected cozystack-cluster-admin to have access, but got false") + } +} + +func TestHasAccessToNamespace_ServiceAccount(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "tenant-test"}, + } + + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "tenant-test", + }, + Subjects: []rbacv1.Subject{ + {Kind: "ServiceAccount", Name: "test-sa", Namespace: "tenant-test"}, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-role", + }, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, rb). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "system:serviceaccount:tenant-test:test-sa", + Groups: []string{"system:authenticated"}, + } + ctx := request.WithUser(context.Background(), u) + + hasAccess, err := r.hasAccessToNamespace(ctx, "tenant-test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !hasAccess { + t.Error("expected service account to have access, but got false") + } +} + +func TestHasAccessToNamespace_ServiceAccountEmptyNamespace(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "tenant-test"}, + } + + // ServiceAccount subject with empty namespace should default to RoleBinding namespace + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "tenant-test", + }, + Subjects: []rbacv1.Subject{ + {Kind: "ServiceAccount", Name: "test-sa", Namespace: ""}, // Empty namespace + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-role", + }, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, rb). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "system:serviceaccount:tenant-test:test-sa", + Groups: []string{"system:authenticated"}, + } + ctx := request.WithUser(context.Background(), u) + + hasAccess, err := r.hasAccessToNamespace(ctx, "tenant-test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !hasAccess { + t.Error("expected service account with empty namespace to have access, but got false") + } +} + +func TestGet_WithAccess(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "tenant-test"}, + } + + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "tenant-test", + }, + Subjects: []rbacv1.Subject{ + {Kind: "User", Name: "test-user", APIGroup: "rbac.authorization.k8s.io"}, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-role", + }, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, rb). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "test-user", + Groups: []string{"system:authenticated"}, + } + ctx := request.WithUser(context.Background(), u) + + obj, err := r.Get(ctx, "tenant-test", &metav1.GetOptions{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if obj == nil { + t.Fatal("expected object, got nil") + } +} + +func TestGet_WithoutAccess(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "tenant-test"}, + } + + // RoleBinding for different user + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "tenant-test", + }, + Subjects: []rbacv1.Subject{ + {Kind: "User", Name: "other-user", APIGroup: "rbac.authorization.k8s.io"}, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-role", + }, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns, rb). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "test-user", + Groups: []string{"system:authenticated"}, + } + ctx := request.WithUser(context.Background(), u) + + obj, err := r.Get(ctx, "tenant-test", &metav1.GetOptions{}) + if err == nil { + t.Fatal("expected error, got nil") + } + if obj != nil { + t.Errorf("expected nil object, got %v", obj) + } + + // Verify it returns NotFound (not Forbidden) to prevent enumeration + if !apierrors.IsNotFound(err) { + t.Errorf("expected NotFound error, got %v", err) + } +} + +func TestGet_NonTenantNamespace(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = rbacv1.AddToScheme(scheme) + + client := fake.NewClientBuilder(). + WithScheme(scheme). + Build() + + r := &REST{ + c: client, + gvr: schema.GroupVersionResource{Group: "core.cozystack.io", Version: "v1alpha1", Resource: "tenantnamespaces"}, + } + + u := &user.DefaultInfo{ + Name: "test-user", + Groups: []string{"system:masters"}, + } + ctx := request.WithUser(context.Background(), u) + + obj, err := r.Get(ctx, "default", &metav1.GetOptions{}) + if err == nil { + t.Fatal("expected error for non-tenant namespace, got nil") + } + if obj != nil { + t.Errorf("expected nil object, got %v", obj) + } + if !apierrors.IsNotFound(err) { + t.Errorf("expected NotFound error, got %v", err) + } +} From e1f66927cb37774a7f592f1dbf4cdcc5c4fd27ab Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Tue, 28 Apr 2026 12:26:57 +0300 Subject: [PATCH 09/11] fix(api): address review feedback on TenantNamespace RBAC Address review feedback from sircthulhu and CodeRabbit: 1. Return Forbidden instead of NotFound for unauthorized access - Get() now returns 403 Forbidden to follow standard K8s RBAC behavior - Previously returned 404 NotFound for security-by-obscurity - Updated test expectations to match new behavior 2. Propagate field and label selectors in Watch handler - Pass opts.FieldSelector and opts.LabelSelector to upstream Watch - Add defensive filtering before authorization to prevent RBAC bypass - Fixes potential issue with resourceNames restrictions 3. Refactor subject-matching logic to eliminate duplication - Extract matchesSubject() helper for Group/User/ServiceAccount checks - Remove duplicated code from filterAccessible and hasAccessToNamespace - Consolidates ServiceAccount namespace fallback logic All tests pass successfully. Signed-off-by: IvanHunters (cherry picked from commit 7257b6aed4bb2b6499168eab467a400196f2e625) --- pkg/registry/core/tenantnamespace/rest.go | 94 +++++++++++-------- .../core/tenantnamespace/rest_test.go | 6 +- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/pkg/registry/core/tenantnamespace/rest.go b/pkg/registry/core/tenantnamespace/rest.go index 7724c0e1..223a6925 100644 --- a/pkg/registry/core/tenantnamespace/rest.go +++ b/pkg/registry/core/tenantnamespace/rest.go @@ -17,6 +17,8 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metainternal "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -130,8 +132,8 @@ func (r *REST) Get( return nil, err } if !hasAccess { - // Return NotFound instead of Forbidden to prevent enumeration - return nil, apierrors.NewNotFound(r.gvr.GroupResource(), name) + // Return Forbidden to follow standard K8s RBAC behavior + return nil, apierrors.NewForbidden(r.gvr.GroupResource(), name, fmt.Errorf("access denied")) } ns := &corev1.Namespace{} @@ -155,10 +157,20 @@ func (r *REST) Get( func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch.Interface, error) { nsList := &corev1.NamespaceList{} - nsWatch, err := r.w.Watch(ctx, nsList, &client.ListOptions{Raw: &metav1.ListOptions{ + + // Build upstream watch options with field and label selectors + rawOpts := &metav1.ListOptions{ Watch: true, ResourceVersion: opts.ResourceVersion, - }}) + } + if opts.FieldSelector != nil { + rawOpts.FieldSelector = opts.FieldSelector.String() + } + if opts.LabelSelector != nil { + rawOpts.LabelSelector = opts.LabelSelector.String() + } + + nsWatch, err := r.w.Watch(ctx, nsList, &client.ListOptions{Raw: rawOpts}) if err != nil { return nil, err } @@ -200,6 +212,18 @@ func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch continue } + // Apply defensive filtering for field and label selectors + if opts.FieldSelector != nil { + if !opts.FieldSelector.Matches(fields.Set{"metadata.name": ns.Name}) { + continue + } + } + if opts.LabelSelector != nil { + if !opts.LabelSelector.Matches(labels.Set(ns.Labels)) { + continue + } + } + // Check if user has access to this namespace hasAccess, err := r.hasAccessToNamespace(ctx, ns.Name) if err != nil { @@ -331,6 +355,26 @@ func (r *REST) makeList(src *corev1.NamespaceList, allowed []string) *corev1alph return out } +// matchesSubject checks if a RoleBinding subject matches the user's identity. +// It handles Group, User, and ServiceAccount subjects with proper namespace fallback. +func matchesSubject(subj rbacv1.Subject, bindingNamespace, username string, groups map[string]struct{}) bool { + switch subj.Kind { + case "Group": + _, ok := groups[subj.Name] + return ok + case "User": + return subj.Name == username + case "ServiceAccount": + saNamespace := subj.Namespace + if saNamespace == "" { + saNamespace = bindingNamespace + } + return username == fmt.Sprintf("system:serviceaccount:%s:%s", saNamespace, subj.Name) + default: + return false + } +} + func (r *REST) filterAccessible( ctx context.Context, names []string, @@ -369,26 +413,9 @@ func (r *REST) filterAccessible( subjectLoop: for j := range rbs.Items[i].Subjects { subj := rbs.Items[i].Subjects[j] - switch subj.Kind { - case "Group": - if _, ok = groups[subj.Name]; ok { - allowedNameSet[rbs.Items[i].Namespace] = struct{}{} - break subjectLoop - } - case "User": - if subj.Name == u.GetName() { - allowedNameSet[rbs.Items[i].Namespace] = struct{}{} - break subjectLoop - } - case "ServiceAccount": - saNamespace := subj.Namespace - if saNamespace == "" { - saNamespace = rbs.Items[i].Namespace - } - if u.GetName() == fmt.Sprintf("system:serviceaccount:%s:%s", saNamespace, subj.Name) { - allowedNameSet[rbs.Items[i].Namespace] = struct{}{} - break subjectLoop - } + if matchesSubject(subj, rbs.Items[i].Namespace, u.GetName(), groups) { + allowedNameSet[rbs.Items[i].Namespace] = struct{}{} + break subjectLoop } } } @@ -434,23 +461,8 @@ func (r *REST) hasAccessToNamespace( for i := range rbs.Items { for j := range rbs.Items[i].Subjects { subj := rbs.Items[i].Subjects[j] - switch subj.Kind { - case "Group": - if _, ok := groups[subj.Name]; ok { - return true, nil - } - case "User": - if subj.Name == u.GetName() { - return true, nil - } - case "ServiceAccount": - saNamespace := subj.Namespace - if saNamespace == "" { - saNamespace = rbs.Items[i].Namespace - } - if u.GetName() == fmt.Sprintf("system:serviceaccount:%s:%s", saNamespace, subj.Name) { - return true, nil - } + if matchesSubject(subj, rbs.Items[i].Namespace, u.GetName(), groups) { + return true, nil } } } diff --git a/pkg/registry/core/tenantnamespace/rest_test.go b/pkg/registry/core/tenantnamespace/rest_test.go index eb678949..e1fb1e85 100644 --- a/pkg/registry/core/tenantnamespace/rest_test.go +++ b/pkg/registry/core/tenantnamespace/rest_test.go @@ -462,9 +462,9 @@ func TestGet_WithoutAccess(t *testing.T) { t.Errorf("expected nil object, got %v", obj) } - // Verify it returns NotFound (not Forbidden) to prevent enumeration - if !apierrors.IsNotFound(err) { - t.Errorf("expected NotFound error, got %v", err) + // Verify it returns Forbidden to follow standard K8s RBAC behavior + if !apierrors.IsForbidden(err) { + t.Errorf("expected Forbidden error, got %v", err) } } From 5c68447763ae20f5532c6010fe6fdefe82e66db0 Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Tue, 28 Apr 2026 16:33:37 +0500 Subject: [PATCH 10/11] fix(api): address review feedback on TenantNamespace watch path - Hoist user identity extraction out of the Watch goroutine; reuse a cached username and groups map across events instead of re-fetching them per event. Watch now returns Unauthorized up front when no user is present in the context, rather than failing silently per event. - Switch the per-event access-check error log to structured klog.ErrorS to comply with the project Go style guide. - Strengthen TestGet_WithAccess to assert the concrete *TenantNamespace type plus Name, Kind, and APIVersion, so type or metadata regressions fail fast. Assisted-By: Claude Signed-off-by: Myasnikov Daniil (cherry picked from commit 61ed7ad89c0bff66236e6884e6b6cd6038383718) --- pkg/registry/core/tenantnamespace/rest.go | 35 +++++++++++++++---- .../core/tenantnamespace/rest_test.go | 16 +++++++-- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pkg/registry/core/tenantnamespace/rest.go b/pkg/registry/core/tenantnamespace/rest.go index 223a6925..4ee97eb0 100644 --- a/pkg/registry/core/tenantnamespace/rest.go +++ b/pkg/registry/core/tenantnamespace/rest.go @@ -156,6 +156,18 @@ func (r *REST) Get( // ----------------------------------------------------------------------------- func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch.Interface, error) { + // Extract user identity once for the lifetime of the watch — it does not + // change between events and rebuilding it per event is wasteful. + u, ok := request.UserFrom(ctx) + if !ok { + return nil, apierrors.NewUnauthorized("user missing in context") + } + username := u.GetName() + groups := make(map[string]struct{}) + for _, group := range u.GetGroups() { + groups[group] = struct{}{} + } + nsList := &corev1.NamespaceList{} // Build upstream watch options with field and label selectors @@ -224,10 +236,11 @@ func (r *REST) Watch(ctx context.Context, opts *metainternal.ListOptions) (watch } } - // Check if user has access to this namespace - hasAccess, err := r.hasAccessToNamespace(ctx, ns.Name) + // Check if user has access to this namespace using the cached + // identity — avoids re-extracting user/groups on every event. + hasAccess, err := r.hasAccessToNamespaceForUser(ctx, ns.Name, username, groups) if err != nil { - klog.Errorf("Failed to check access for namespace %s in watch: %v", ns.Name, err) + klog.ErrorS(err, "Failed to check access for namespace in watch", "namespace", ns.Name) continue } if !hasAccess { @@ -437,12 +450,22 @@ func (r *REST) hasAccessToNamespace( if !ok { return false, fmt.Errorf("user missing in context") } - - // Check privileged groups groups := make(map[string]struct{}) for _, group := range u.GetGroups() { groups[group] = struct{}{} } + return r.hasAccessToNamespaceForUser(ctx, namespace, u.GetName(), groups) +} + +// hasAccessToNamespaceForUser is the inner check that does not re-extract user +// identity from context. Use this in hot paths (e.g. the Watch loop) where the +// caller has already cached the user name and groups. +func (r *REST) hasAccessToNamespaceForUser( + ctx context.Context, + namespace, username string, + groups map[string]struct{}, +) (bool, error) { + // Check privileged groups if _, ok := groups["system:masters"]; ok { return true, nil } @@ -461,7 +484,7 @@ func (r *REST) hasAccessToNamespace( for i := range rbs.Items { for j := range rbs.Items[i].Subjects { subj := rbs.Items[i].Subjects[j] - if matchesSubject(subj, rbs.Items[i].Namespace, u.GetName(), groups) { + if matchesSubject(subj, rbs.Items[i].Namespace, username, groups) { return true, nil } } diff --git a/pkg/registry/core/tenantnamespace/rest_test.go b/pkg/registry/core/tenantnamespace/rest_test.go index e1fb1e85..52d2a075 100644 --- a/pkg/registry/core/tenantnamespace/rest_test.go +++ b/pkg/registry/core/tenantnamespace/rest_test.go @@ -15,6 +15,8 @@ import ( "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/endpoints/request" "sigs.k8s.io/controller-runtime/pkg/client/fake" + + corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" ) func TestMakeListSortsAlphabetically(t *testing.T) { @@ -408,8 +410,18 @@ func TestGet_WithAccess(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if obj == nil { - t.Fatal("expected object, got nil") + tn, ok := obj.(*corev1alpha1.TenantNamespace) + if !ok { + t.Fatalf("expected *TenantNamespace, got %T", obj) + } + if tn.Name != "tenant-test" { + t.Errorf("expected name %q, got %q", "tenant-test", tn.Name) + } + if tn.Kind != "TenantNamespace" { + t.Errorf("expected Kind=TenantNamespace, got %q", tn.Kind) + } + if tn.APIVersion != corev1alpha1.SchemeGroupVersion.String() { + t.Errorf("expected APIVersion=%q, got %q", corev1alpha1.SchemeGroupVersion.String(), tn.APIVersion) } } From c0004e5c5ad07c1db8ce5ef49ce6ca80daa4ce22 Mon Sep 17 00:00:00 2001 From: "cozystack-ci[bot]" <274107086+cozystack-ci[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 04:26:30 +0000 Subject: [PATCH 11/11] Prepare release v1.3.1 Signed-off-by: cozystack-ci[bot] <274107086+cozystack-ci[bot]@users.noreply.github.com> --- packages/apps/kubernetes/images/kubevirt-csi-driver.tag | 2 +- packages/core/installer/values.yaml | 4 ++-- packages/core/platform/values.yaml | 2 +- packages/core/testing/values.yaml | 2 +- packages/extra/bootbox/images/matchbox.tag | 2 +- packages/extra/seaweedfs/images/objectstorage-sidecar.tag | 2 +- packages/system/backup-controller/values.yaml | 2 +- packages/system/backupstrategy-controller/values.yaml | 2 +- packages/system/bucket/images/s3manager.tag | 2 +- packages/system/cozystack-api/values.yaml | 2 +- packages/system/cozystack-controller/values.yaml | 2 +- packages/system/dashboard/templates/configmap.yaml | 2 +- packages/system/dashboard/values.yaml | 6 +++--- .../system/grafana-operator/images/grafana-dashboards.tag | 2 +- packages/system/kamaji/values.yaml | 4 ++-- packages/system/kubeovn-plunger/values.yaml | 2 +- packages/system/kubeovn-webhook/values.yaml | 2 +- packages/system/kubevirt-csi-node/values.yaml | 2 +- packages/system/lineage-controller-webhook/values.yaml | 2 +- packages/system/linstor-gui/values.yaml | 4 +--- packages/system/linstor/values.yaml | 2 +- packages/system/multus/templates/multus-daemonset-thick.yml | 4 ++-- packages/system/objectstorage-controller/values.yaml | 2 +- packages/system/seaweedfs/values.yaml | 2 +- 24 files changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag index 00aa2caa..60d536ce 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:72154a97054e16cdf3dea6129d962b8d7e86b55cf9386095e8ac2ce7c8b69172 +ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:0e70f4391b344c71a9567e1ed22ca24910116a619cb681381059b8c39f382219 diff --git a/packages/core/installer/values.yaml b/packages/core/installer/values.yaml index eef691ea..7e66e5a1 100644 --- a/packages/core/installer/values.yaml +++ b/packages/core/installer/values.yaml @@ -1,9 +1,9 @@ cozystackOperator: # Deployment variant: talos, generic, hosted variant: talos - image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.3.0@sha256:62574f12486bb40c901cf5ed484cca264405ce5810196d86555cbb27cce1ba48 + image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.3.1@sha256:6cf2c5eb432193b5fa8ab202cbc56c8cd7971436639ceefa5be99aede74b3061 platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/cozystack-packages' - platformSourceRef: 'digest=sha256:a0b9ef938446b3132d3d22ad2262beb1027c48c9037b6c2346fdc2f19acd3036' + platformSourceRef: 'digest=sha256:7b0d95638195317f0d84f33e0cbd77542229c9df42de41ee22e977987043631f' # Generic variant configuration (only used when cozystackOperator.variant=generic) cozystack: # Kubernetes API server host (IP only, no protocol/port) diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index f33926db..44ad39c5 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -5,7 +5,7 @@ sourceRef: path: / migrations: enabled: false - image: ghcr.io/cozystack/cozystack/platform-migrations:v1.3.0@sha256:555e4b76421361805a84bc9088b01b23a9c4a9430bd8ebd2db82ef9677d7008c + image: ghcr.io/cozystack/cozystack/platform-migrations:v1.3.1@sha256:555e4b76421361805a84bc9088b01b23a9c4a9430bd8ebd2db82ef9677d7008c targetVersion: 39 # Bundle deployment configuration bundles: diff --git a/packages/core/testing/values.yaml b/packages/core/testing/values.yaml index 9d4bdd10..4ab67a33 100644 --- a/packages/core/testing/values.yaml +++ b/packages/core/testing/values.yaml @@ -1,2 +1,2 @@ e2e: - image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.3.0@sha256:0367a03b981df2a3ea13f411d4cb7869c2bf2c89c07d3d5c8971b9a28921ccef + image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.3.1@sha256:0367a03b981df2a3ea13f411d4cb7869c2bf2c89c07d3d5c8971b9a28921ccef diff --git a/packages/extra/bootbox/images/matchbox.tag b/packages/extra/bootbox/images/matchbox.tag index f51c7675..213476b5 100644 --- a/packages/extra/bootbox/images/matchbox.tag +++ b/packages/extra/bootbox/images/matchbox.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/matchbox:v1.3.0@sha256:85b8e04bf6f0690612dd63e80475df269f4a436d16680f8a40f2860cf16e2f74 +ghcr.io/cozystack/cozystack/matchbox:v1.3.1@sha256:f61425c515e7c8b49d634cef4f050faeeaa8c455ffbbe77eee669a04dc1ed305 diff --git a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag index 9b413da5..45cc1142 100644 --- a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag +++ b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.3.0@sha256:5bcccbdb13979a16cee535eb5fbcdf0d87973689010a10b7b25e55c5be3edaa6 +ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.3.1@sha256:3895676b15cdf186b03f231dcfdfe40812ecd76ab00c3b4e2952daccbacd57f3 diff --git a/packages/system/backup-controller/values.yaml b/packages/system/backup-controller/values.yaml index 34e033ce..58ad3361 100644 --- a/packages/system/backup-controller/values.yaml +++ b/packages/system/backup-controller/values.yaml @@ -1,5 +1,5 @@ backupController: - image: "ghcr.io/cozystack/cozystack/backup-controller:v1.3.0@sha256:e1a083dc92f26dfef004f47c1cd20a6357174aad835004f58e751c494b76649a" + image: "ghcr.io/cozystack/cozystack/backup-controller:v1.3.1@sha256:b0ed6db5cf738b774de62a2af51c9800c23dd6e977e95728d3b0098d18c8927e" replicas: 2 debug: false metrics: diff --git a/packages/system/backupstrategy-controller/values.yaml b/packages/system/backupstrategy-controller/values.yaml index d6453124..d77c8eb3 100644 --- a/packages/system/backupstrategy-controller/values.yaml +++ b/packages/system/backupstrategy-controller/values.yaml @@ -1,5 +1,5 @@ backupStrategyController: - image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.3.0@sha256:be0a9ec1f4307064b16388a24628aee46e06252738338add80b99ea1e04e62bf" + image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.3.1@sha256:8b0500a68c4dd027b6499d5309ccebddc56bb29f070722538cc306827e71da93" replicas: 2 debug: false metrics: diff --git a/packages/system/bucket/images/s3manager.tag b/packages/system/bucket/images/s3manager.tag index 889db0af..ee160919 100644 --- a/packages/system/bucket/images/s3manager.tag +++ b/packages/system/bucket/images/s3manager.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:fb65e734bbdbdaef2238769cc2ecfb54dbddaf1e0952ad438a9b3e26b9dbb4b5 +ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:7473f0acd04d2e14393b4fc3638fe9e0d3dc15a5778b05dac9dfe1a1957d2647 diff --git a/packages/system/cozystack-api/values.yaml b/packages/system/cozystack-api/values.yaml index dddabf54..5b0bc4f0 100644 --- a/packages/system/cozystack-api/values.yaml +++ b/packages/system/cozystack-api/values.yaml @@ -1,3 +1,3 @@ cozystackAPI: - image: ghcr.io/cozystack/cozystack/cozystack-api:v1.3.0@sha256:5fa8648821cf1e9e08cf7c2899c4b1c4226bb74c6773327141456c7d3f2b9b7e + image: ghcr.io/cozystack/cozystack/cozystack-api:v1.3.1@sha256:9494995677a4f6f6afcf4c222d1372efea33a69515fc5de4069ff5583ec4638f replicas: 2 diff --git a/packages/system/cozystack-controller/values.yaml b/packages/system/cozystack-controller/values.yaml index 4d0d0fde..fc853c94 100644 --- a/packages/system/cozystack-controller/values.yaml +++ b/packages/system/cozystack-controller/values.yaml @@ -1,4 +1,4 @@ cozystackController: - image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.3.0@sha256:d03d19b78c4c98f970ac549a68b01ef6bd1ad755f5e0dcb9e08503511cdf2fdc + image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.3.1@sha256:4b13d34b01e3e778feb2098053a3cccfe3260806af0b595fd7a6e27c0ff96faf debug: false disableTelemetry: false diff --git a/packages/system/dashboard/templates/configmap.yaml b/packages/system/dashboard/templates/configmap.yaml index a2b7684c..be255d1a 100644 --- a/packages/system/dashboard/templates/configmap.yaml +++ b/packages/system/dashboard/templates/configmap.yaml @@ -1,6 +1,6 @@ {{- $brandingConfig := .Values._cluster.branding | default dict }} -{{- $tenantText := "v1.3.0" }} +{{- $tenantText := "v1.3.1" }} {{- $footerText := "Cozystack" }} {{- $titleText := "Cozystack Dashboard" }} {{- $logoText := "" }} diff --git a/packages/system/dashboard/values.yaml b/packages/system/dashboard/values.yaml index a5188825..50f4fe5f 100644 --- a/packages/system/dashboard/values.yaml +++ b/packages/system/dashboard/values.yaml @@ -1,6 +1,6 @@ openapiUI: - image: ghcr.io/cozystack/cozystack/openapi-ui:v1.3.0@sha256:0fa79c373a62840a617ff1ca1b0e31931c13a6cf7b0bb0ff0dc191f047a465a3 + image: ghcr.io/cozystack/cozystack/openapi-ui:v1.3.1@sha256:33de92b549ddf1e56a62d79c46aeb077fbdc67ec16cf2b556be1714f5616788f openapiUIK8sBff: - image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.3.0@sha256:1b3ea6d4c7dbbe6a8def3b2807fffdfab2ac4afc39d7a846e57dd491fa168f92 + image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.3.1@sha256:c938fee904acd948800d4dc5e121c4c5cd64cb4a3160fb8d2f9dbff0e5168740 tokenProxy: - image: ghcr.io/cozystack/cozystack/token-proxy:v1.3.0@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc + image: ghcr.io/cozystack/cozystack/token-proxy:v1.3.1@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc diff --git a/packages/system/grafana-operator/images/grafana-dashboards.tag b/packages/system/grafana-operator/images/grafana-dashboards.tag index cde83a70..d73eda56 100644 --- a/packages/system/grafana-operator/images/grafana-dashboards.tag +++ b/packages/system/grafana-operator/images/grafana-dashboards.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/grafana-dashboards:v1.3.0@sha256:2c9aa0b48e2bf6167db198f4d15882bfe51700108edf2e9f6d0942940a2c1204 +ghcr.io/cozystack/cozystack/grafana-dashboards:v1.3.1@sha256:2c9aa0b48e2bf6167db198f4d15882bfe51700108edf2e9f6d0942940a2c1204 diff --git a/packages/system/kamaji/values.yaml b/packages/system/kamaji/values.yaml index 6ba649f0..758b834a 100644 --- a/packages/system/kamaji/values.yaml +++ b/packages/system/kamaji/values.yaml @@ -3,7 +3,7 @@ kamaji: deploy: false image: pullPolicy: IfNotPresent - tag: v1.3.0@sha256:8c9af083b60600c0800eb56a2cda75f26007b7272a1cf019140de003bfce1a4d + tag: v1.3.1@sha256:8c727e4eb9500f7dd1c76b6daf7bb5e6cfee1984dfb79e0a8fc4c4b467937f86 repository: ghcr.io/cozystack/cozystack/kamaji resources: limits: @@ -36,4 +36,4 @@ kamaji: periodSeconds: 10 timeoutSeconds: 1 extraArgs: - - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.3.0@sha256:8c9af083b60600c0800eb56a2cda75f26007b7272a1cf019140de003bfce1a4d + - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.3.1@sha256:8c727e4eb9500f7dd1c76b6daf7bb5e6cfee1984dfb79e0a8fc4c4b467937f86 diff --git a/packages/system/kubeovn-plunger/values.yaml b/packages/system/kubeovn-plunger/values.yaml index 02620593..06d79ad7 100644 --- a/packages/system/kubeovn-plunger/values.yaml +++ b/packages/system/kubeovn-plunger/values.yaml @@ -1,4 +1,4 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.3.0@sha256:b75a0facb99c3b0fe8090414b3425f5c4858fff632d1de122ce9944c4daa89c0 +image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.3.1@sha256:7587822422b571e575bf1809a7537ec637f95ba67c92ffac0a14509793651f89 ovnCentralName: ovn-central diff --git a/packages/system/kubeovn-webhook/values.yaml b/packages/system/kubeovn-webhook/values.yaml index 0c6ce81a..7ac5cfa2 100644 --- a/packages/system/kubeovn-webhook/values.yaml +++ b/packages/system/kubeovn-webhook/values.yaml @@ -1,3 +1,3 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.3.0@sha256:e18f9fd679e38f65362a8d0042f25468272f6d081136ad47027168d8e7e07a4a +image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.3.1@sha256:e6334c29d3aaf0dea766c88e3e05b53ad623d1bb497b3c836e6f76adade45b29 diff --git a/packages/system/kubevirt-csi-node/values.yaml b/packages/system/kubevirt-csi-node/values.yaml index 56e28543..8e38b7e2 100644 --- a/packages/system/kubevirt-csi-node/values.yaml +++ b/packages/system/kubevirt-csi-node/values.yaml @@ -1,3 +1,3 @@ storageClass: replicated csiDriver: - image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:72154a97054e16cdf3dea6129d962b8d7e86b55cf9386095e8ac2ce7c8b69172 + image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:0e70f4391b344c71a9567e1ed22ca24910116a619cb681381059b8c39f382219 diff --git a/packages/system/lineage-controller-webhook/values.yaml b/packages/system/lineage-controller-webhook/values.yaml index c5671548..2bdc2344 100644 --- a/packages/system/lineage-controller-webhook/values.yaml +++ b/packages/system/lineage-controller-webhook/values.yaml @@ -1,5 +1,5 @@ lineageControllerWebhook: - image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.3.0@sha256:e8984709686a5eaf19b89da378d7b8c688ea5607e0783a88d9c9e4ccfca96fb0 + image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.3.1@sha256:b73c3debd7b414e6c18b4816789030ef16c097076d3474f73b6d9fe1d7dadf98 debug: false localK8sAPIEndpoint: enabled: true diff --git a/packages/system/linstor-gui/values.yaml b/packages/system/linstor-gui/values.yaml index e3c49aea..e22e59b5 100644 --- a/packages/system/linstor-gui/values.yaml +++ b/packages/system/linstor-gui/values.yaml @@ -3,12 +3,10 @@ ## @param image.tag LINSTOR GUI container image tag (digest recommended) image: repository: ghcr.io/cozystack/cozystack/linstor-gui - tag: 2.3.0 - + tag: 2.3.0@sha256:554a47730231f43449fde91e9f8b9c01a94ec637b46aaa55d814b0cbb8d92680 ## @section Deployment ## @param replicas Number of linstor-gui replicas replicas: 1 - ## @section LINSTOR controller connection ## @param linstor.endpoint In-cluster URL of the LINSTOR controller REST API (HTTPS, mTLS) ## @param linstor.clientSecret Kubernetes Secret with `tls.crt`, `tls.key`, `ca.crt` used as the mTLS client certificate against the LINSTOR controller. Created by the `linstor` package. diff --git a/packages/system/linstor/values.yaml b/packages/system/linstor/values.yaml index 62dd2cac..8211739a 100644 --- a/packages/system/linstor/values.yaml +++ b/packages/system/linstor/values.yaml @@ -13,4 +13,4 @@ linstor: linstorCSI: image: repository: ghcr.io/cozystack/cozystack/linstor-csi - tag: v1.10.5@sha256:b8f59b5659fb1791cb764d3f37df4cf29920aadcc10637231ba7d857233f377d + tag: v1.10.6@sha256:aef5c6a8351a86daba781390c123f39f947da7ade59075b59f89fa8e3edeb75a diff --git a/packages/system/multus/templates/multus-daemonset-thick.yml b/packages/system/multus/templates/multus-daemonset-thick.yml index cad330ea..11a37f37 100644 --- a/packages/system/multus/templates/multus-daemonset-thick.yml +++ b/packages/system/multus/templates/multus-daemonset-thick.yml @@ -155,7 +155,7 @@ spec: serviceAccountName: multus containers: - name: kube-multus - image: ghcr.io/cozystack/cozystack/multus-cni:v1.3.0@sha256:6735ffc12e5e660951f5a42943b38c308f33774a55836e94c191001405b58ec0 + image: ghcr.io/cozystack/cozystack/multus-cni:v1.3.1@sha256:6735ffc12e5e660951f5a42943b38c308f33774a55836e94c191001405b58ec0 command: [ "/usr/src/multus-cni/bin/multus-daemon" ] resources: requests: @@ -201,7 +201,7 @@ spec: fieldPath: spec.nodeName initContainers: - name: install-multus-binary - image: ghcr.io/cozystack/cozystack/multus-cni:v1.3.0@sha256:6735ffc12e5e660951f5a42943b38c308f33774a55836e94c191001405b58ec0 + image: ghcr.io/cozystack/cozystack/multus-cni:v1.3.1@sha256:6735ffc12e5e660951f5a42943b38c308f33774a55836e94c191001405b58ec0 command: - "/usr/src/multus-cni/bin/install_multus" - "-d" diff --git a/packages/system/objectstorage-controller/values.yaml b/packages/system/objectstorage-controller/values.yaml index a3859194..3ca855cf 100644 --- a/packages/system/objectstorage-controller/values.yaml +++ b/packages/system/objectstorage-controller/values.yaml @@ -1,3 +1,3 @@ objectstorage: controller: - image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.3.0@sha256:7a9e4bf9c3f95f364756396815bb51b6c0f58f85db653460574d94b96cd3c7d5" + image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.3.1@sha256:7a9e4bf9c3f95f364756396815bb51b6c0f58f85db653460574d94b96cd3c7d5" diff --git a/packages/system/seaweedfs/values.yaml b/packages/system/seaweedfs/values.yaml index 3542a77f..00c3eaf4 100644 --- a/packages/system/seaweedfs/values.yaml +++ b/packages/system/seaweedfs/values.yaml @@ -177,7 +177,7 @@ seaweedfs: bucketClassName: "seaweedfs" region: "" sidecar: - image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.3.0@sha256:5bcccbdb13979a16cee535eb5fbcdf0d87973689010a10b7b25e55c5be3edaa6" + image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.3.1@sha256:3895676b15cdf186b03f231dcfdfe40812ecd76ab00c3b4e2952daccbacd57f3" certificates: commonName: "SeaweedFS CA" ipAddresses: []