Compare commits
18 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f309823d2 | ||
|
|
c0004e5c5a | ||
|
|
09edf16b85 | ||
|
|
5c68447763 | ||
|
|
e1f66927cb | ||
|
|
79c23d895a | ||
|
|
82fb7990d5 | ||
|
|
5eb52f00b0 | ||
|
|
fe577a26c6 | ||
|
|
f6da822cd0 | ||
|
|
3c2aeb133e | ||
|
|
c131acc8c6 | ||
|
|
cbc1606c41 | ||
|
|
f0f3b18add | ||
|
|
f384be5475 | ||
|
|
ed89553023 | ||
|
|
41bcb0becb | ||
|
|
5329dd180b |
42 changed files with 874 additions and 235 deletions
1
Makefile
1
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/matchbox:v1.3.0@sha256:85b8e04bf6f0690612dd63e80475df269f4a436d16680f8a40f2860cf16e2f74
|
||||
ghcr.io/cozystack/cozystack/matchbox:v1.3.1@sha256:f61425c515e7c8b49d634cef4f050faeeaa8c455ffbbe77eee669a04dc1ed305
|
||||
|
|
|
|||
|
|
@ -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 .
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 }}
|
||||
|
|
@ -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
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: etcd-deployed-version
|
||||
data:
|
||||
version: {{ .Chart.Version }}
|
||||
34
packages/extra/etcd/tests/no-post-upgrade-hook_test.yaml
Normal file
34
packages/extra/etcd/tests/no-post-upgrade-hook_test.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.3.0@sha256:5bcccbdb13979a16cee535eb5fbcdf0d87973689010a10b7b25e55c5be3edaa6
|
||||
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.3.1@sha256:3895676b15cdf186b03f231dcfdfe40812ecd76ab00c3b4e2952daccbacd57f3
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:fb65e734bbdbdaef2238769cc2ecfb54dbddaf1e0952ad438a9b3e26b9dbb4b5
|
||||
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:7473f0acd04d2e14393b4fc3638fe9e0d3dc15a5778b05dac9dfe1a1957d2647
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{{- $brandingConfig := .Values._cluster.branding | default dict }}
|
||||
|
||||
{{- $tenantText := "v1.3.0" }}
|
||||
{{- $tenantText := "v1.3.1" }}
|
||||
{{- $footerText := "Cozystack" }}
|
||||
{{- $titleText := "Cozystack Dashboard" }}
|
||||
{{- $logoText := "" }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/grafana-dashboards:v1.3.0@sha256:2c9aa0b48e2bf6167db198f4d15882bfe51700108edf2e9f6d0942940a2c1204
|
||||
ghcr.io/cozystack/cozystack/grafana-dashboards:v1.3.1@sha256:2c9aa0b48e2bf6167db198f4d15882bfe51700108edf2e9f6d0942940a2c1204
|
||||
|
|
|
|||
|
|
@ -3,14 +3,37 @@ 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:
|
||||
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
|
||||
- --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.3.1@sha256:8c727e4eb9500f7dd1c76b6daf7bb5e6cfee1984dfb79e0a8fc4c4b467937f86
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)" \
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -24,6 +26,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 +126,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 Forbidden to follow standard K8s RBAC behavior
|
||||
return nil, apierrors.NewForbidden(r.gvr.GroupResource(), name, fmt.Errorf("access denied"))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -143,11 +156,33 @@ 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{}
|
||||
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
|
||||
}
|
||||
|
|
@ -189,6 +224,30 @@ 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 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.ErrorS(err, "Failed to check access for namespace in watch", "namespace", ns.Name)
|
||||
continue
|
||||
}
|
||||
if !hasAccess {
|
||||
// User doesn't have access, skip this event
|
||||
continue
|
||||
}
|
||||
|
||||
out := &corev1alpha1.TenantNamespace{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: corev1alpha1.SchemeGroupVersion.String(),
|
||||
|
|
@ -309,6 +368,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,
|
||||
|
|
@ -347,22 +426,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":
|
||||
if u.GetName() == fmt.Sprintf("system:serviceaccount:%s:%s", subj.Namespace, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -373,6 +439,60 @@ 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")
|
||||
}
|
||||
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
|
||||
}
|
||||
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]
|
||||
if matchesSubject(subj, rbs.Items[i].Namespace, username, groups) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Boiler-plate
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,10 +3,20 @@
|
|||
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"
|
||||
|
||||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
)
|
||||
|
||||
func TestMakeListSortsAlphabetically(t *testing.T) {
|
||||
|
|
@ -38,3 +48,466 @@ 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 Forbidden to follow standard K8s RBAC behavior
|
||||
if !apierrors.IsForbidden(err) {
|
||||
t.Errorf("expected Forbidden 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue