From d25112f448f386e598c4991c9a7990ca90935bca Mon Sep 17 00:00:00 2001 From: Christian Gottinger Date: Fri, 22 May 2026 14:09:02 +0200 Subject: [PATCH 1/2] make cns admin role configurable with default cluster-admin --- Controllers/Sync.cs | 11 ++++++++++- Models/K8sRoleBinding.cs | 11 ++++------- Services/K8sResourceFactory.cs | 9 +++++---- appsettings.json | 5 ++++- .../cops-controller/templates/03-deployment.yaml | 2 ++ deployment/cops-controller/values.yaml | 4 +++- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Controllers/Sync.cs b/Controllers/Sync.cs index 4edbf68..c920ed0 100644 --- a/Controllers/Sync.cs +++ b/Controllers/Sync.cs @@ -1,4 +1,5 @@ using ConplementAG.CopsController.Services; +using Microsoft.Extensions.Configuration; using Newtonsoft.Json.Linq; using Serilog; using System; @@ -11,6 +12,13 @@ namespace ConplementAG.CopsController.Controllers [ApiController] public class SyncController : ControllerBase { + private readonly IConfiguration _configuration; + + public SyncController(IConfiguration configuration) + { + _configuration = configuration; + } + [HttpGet] public ActionResult Get() { @@ -25,7 +33,8 @@ public IActionResult Post([FromBody]JObject value) Log.Verbose("REQUEST===============================" + value.ToString(Newtonsoft.Json.Formatting.Indented)); var copsResource = CopsResourceFactory.Create(value); - var k8sResources = K8sResourceFactory.Create(copsResource); + var namespaceAdminRole = _configuration.GetValue("CopsController:NamespaceAdminRole", "cluster-admin"); + var k8sResources = K8sResourceFactory.Create(copsResource, namespaceAdminRole); JObject response = JObject.FromObject( new diff --git a/Models/K8sRoleBinding.cs b/Models/K8sRoleBinding.cs index 7fbbdda..f798711 100644 --- a/Models/K8sRoleBinding.cs +++ b/Models/K8sRoleBinding.cs @@ -22,8 +22,9 @@ public class K8sRoleBinding [JsonProperty("roleRef")] public K8sRoleRef RoleRef { get; set; } - public static K8sRoleBinding NamespaceFullAccess(string namespacename, - ICollection users, ICollection serviceAccounts) + public static K8sRoleBinding NamespaceFullAccess(string namespacename, + ICollection users, ICollection serviceAccounts, + string roleName = "cluster-admin") { if (string.IsNullOrEmpty(namespacename)) { @@ -52,11 +53,7 @@ public static K8sRoleBinding NamespaceFullAccess(string namespacename, Kind = "RoleBinding", ApiVersion = "rbac.authorization.k8s.io/v1", Metadata = new K8sMetadata { Name = $"copsnamespace-user", Namespace = namespacename }, - // The in-built clusterrole cluster-admin allows access to all resources (wildcard), - // so that we can use that clusterrole instead of writing our own which is far more brittle. - // Since we scope the cluster-admin to a namespace using RoleBinding (in contrast to ClusterRoleBinding) - // this is a good approach. - RoleRef = new K8sRoleRef("ClusterRole", "cluster-admin", "rbac.authorization.k8s.io") + RoleRef = new K8sRoleRef("ClusterRole", roleName, "rbac.authorization.k8s.io") }; var subjects = users.ToList() diff --git a/Services/K8sResourceFactory.cs b/Services/K8sResourceFactory.cs index 514368d..150125f 100644 --- a/Services/K8sResourceFactory.cs +++ b/Services/K8sResourceFactory.cs @@ -7,22 +7,23 @@ namespace ConplementAG.CopsController.Services { public class K8sResourceFactory { - public static IList Create(CopsResource resource) + public static IList Create(CopsResource resource, string namespaceAdminRole = "cluster-admin") { var source = Convert.ChangeType(resource, resource.GetType()); var method = typeof(K8sResourceFactory).GetMethod("Create", BindingFlags.NonPublic | BindingFlags.Static); - return (IList)method.Invoke(null, new[] { source }); + return (IList)method.Invoke(null, new[] { source, namespaceAdminRole }); } // Method used by reflection call - private static IList Create(CopsNamespace copsNamespace) + private static IList Create(CopsNamespace copsNamespace, string namespaceAdminRole) { return new List { new K8sNamespace(copsNamespace.Metadata.Name, copsNamespace.Spec.Project?.Name, copsNamespace.Spec.Project?.CostCenter), K8sRoleBinding.NamespaceFullAccess(copsNamespace.Metadata.Name, copsNamespace.Spec.NamespaceAdminUsers, - copsNamespace.Spec.NamespaceAdminServiceAccounts ?? new List().ToArray()), + copsNamespace.Spec.NamespaceAdminServiceAccounts ?? new List().ToArray(), + namespaceAdminRole), K8sClusterRoleBinding.CopsNamespaceEditBinding(copsNamespace.Metadata.Name, copsNamespace.Spec.NamespaceAdminUsers, copsNamespace.Spec.NamespaceAdminServiceAccounts ?? new List().ToArray()), K8sClusterRole.CopsNamespaceEdit(copsNamespace.Metadata.Name), diff --git a/appsettings.json b/appsettings.json index 4bd7500..74e66c3 100644 --- a/appsettings.json +++ b/appsettings.json @@ -4,5 +4,8 @@ "Default": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "CopsController": { + "NamespaceAdminRole": "cluster-admin" + } } diff --git a/deployment/cops-controller/templates/03-deployment.yaml b/deployment/cops-controller/templates/03-deployment.yaml index a792e7f..2e50820 100644 --- a/deployment/cops-controller/templates/03-deployment.yaml +++ b/deployment/cops-controller/templates/03-deployment.yaml @@ -31,4 +31,6 @@ spec: env: - name: Serilog__MinimumLevel value: Error + - name: CopsController__NamespaceAdminRole + value: {{ .Values.copsController.namespaceAdminRole }} \ No newline at end of file diff --git a/deployment/cops-controller/values.yaml b/deployment/cops-controller/values.yaml index 8189d96..46bee21 100644 --- a/deployment/cops-controller/values.yaml +++ b/deployment/cops-controller/values.yaml @@ -1,3 +1,5 @@ image: repository: conplementag/cops-controller - tag: 1.10.1 # x-release-please-version \ No newline at end of file + tag: 1.10.1 # x-release-please-version +copsController: + namespaceAdminRole: cluster-admin \ No newline at end of file From 3f90245618f7f814b2cbac01a68fc47453b36c5e Mon Sep 17 00:00:00 2001 From: Christian Gottinger Date: Fri, 22 May 2026 15:54:00 +0200 Subject: [PATCH 2/2] fix tests --- deployment/crds/copsnamespace.crd.yaml | 2 +- run_tests.sh | 15 +++++--- tests/1-empire-cns.yaml | 7 ++-- tests/2-updated-cns.yaml | 7 ++-- tests/invalid-definitions/1.yaml | 2 +- tests/invalid-definitions/2.yaml | 2 +- tests/invalid-definitions/3.yaml | 2 +- tests/invalid-definitions/4.yaml | 2 +- tests/invalid-definitions/5.yaml | 2 +- tests/invalid-definitions/6.yaml | 2 +- tests/invalid-definitions/7.yaml | 2 +- tests/tests.sh | 47 +++++++++++++++++--------- tests/valid-definitions/1.yaml | 7 ++-- tests/valid-definitions/2.yaml | 7 ++-- tests/valid-definitions/3.yaml | 7 ++-- 15 files changed, 74 insertions(+), 39 deletions(-) diff --git a/deployment/crds/copsnamespace.crd.yaml b/deployment/crds/copsnamespace.crd.yaml index d2524fe..06baeac 100644 --- a/deployment/crds/copsnamespace.crd.yaml +++ b/deployment/crds/copsnamespace.crd.yaml @@ -103,7 +103,7 @@ spec: type: object required: - name - - costcenter + - costCenter properties: name: type: string diff --git a/run_tests.sh b/run_tests.sh index 90e3a82..0c74d6a 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,11 +6,12 @@ programname=$0 UNIVERSAL_TEST_IDENTIFIED="cops-controller-component-tests" function usage { - echo "usage: $programname [--install-helm-and-cops-controller] [-r repository] [-t tag]" + echo "usage: $programname [--install-helm-and-cops-controller] [-r repository] [-t tag] [-a namespaceAdminRole]" echo " MAKE SURE YOU SPECIFY THE ARGUMENTS IN THE EXACT ORDER AS BELOW, THIS SCRIPT DOES NOT SUPPORT OUT OF ORDER ARGUMENTS!" - echo " --install-helm-and-cops-controller (optional) use to install global helm in the cluster and to deploy the cops controller specified by the -r and -t arguments. Make sure you have Helm > 2.16 if you use this option and you are running on k8s > 1.16" - echo " -r repository (optional) cops controller image repository. Should be accessible from the cluster (e.g. remote registry or local one shared with the cluster)." - echo " -t tag (optional) cops controller image tag." + echo " --install-helm-and-cops-controller (optional) install metacontroller and the cops controller specified by the -r and -t arguments. Requires Helm 3 and kubectl." + echo " -r repository (optional) cops controller image repository. Should be accessible from the cluster (e.g. remote registry or local one shared with the cluster)." + echo " -t tag (optional) cops controller image tag." + echo " -a namespaceAdminRole (optional) ClusterRole to bind for namespace admins. Defaults to devops-namespace-admin ." echo " " echo "Prerequisites: " echo " To run the tests, you need a running k8s cluster and docker engine" @@ -83,6 +84,7 @@ function cleanup { installController="no" repository="" tag="" +namespaceAdminRole="devops-namespace-admin" if [ -n "$1" ]; then # if any argument specified # all parameters mandatory now, in correct order @@ -92,10 +94,13 @@ if [ -n "$1" ]; then # if any argument specified installController="yes" repository=$3 tag=$5 + if [ "$6" == "-a" ] && [ -n "$7" ]; then + namespaceAdminRole=$7 + fi fi fi # register the cleanup function for all signal types (emulating finally block) trap cleanup EXIT ERR INT TERM -. ./tests/tests.sh "$installController" "$repository" "$tag" \ No newline at end of file +. ./tests/tests.sh "$installController" "$repository" "$tag" "$namespaceAdminRole" \ No newline at end of file diff --git a/tests/1-empire-cns.yaml b/tests/1-empire-cns.yaml index 4ad482e..2cf9af1 100644 --- a/tests/1-empire-cns.yaml +++ b/tests/1-empire-cns.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: {{NAMESPACE}} @@ -10,4 +10,7 @@ spec: - Second.User@conplement.de namespaceAdminServiceAccounts: - serviceAccount: {{SERVICE_ACCOUNT}} - namespace: {{SERVICE_ACCOUNT_NAMESPACE}} \ No newline at end of file + namespace: {{SERVICE_ACCOUNT_NAMESPACE}} + project: + name: empire + costCenter: "66" \ No newline at end of file diff --git a/tests/2-updated-cns.yaml b/tests/2-updated-cns.yaml index 2825b66..3c24ff2 100644 --- a/tests/2-updated-cns.yaml +++ b/tests/2-updated-cns.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: {{NAMESPACE}} @@ -13,4 +13,7 @@ spec: - serviceAccount: {{SERVICE_ACCOUNT}} namespace: {{SERVICE_ACCOUNT_NAMESPACE}} - serviceAccount: {{ADDITIONAL_SERVICE_ACCOUNT}} - namespace: {{ADDITIONAL_SERVICE_ACCOUNT_NAMESPACE}} \ No newline at end of file + namespace: {{ADDITIONAL_SERVICE_ACCOUNT_NAMESPACE}} + project: + name: empire + costCenter: "66" \ No newline at end of file diff --git a/tests/invalid-definitions/1.yaml b/tests/invalid-definitions/1.yaml index 2ddf23f..d602fe4 100644 --- a/tests/invalid-definitions/1.yaml +++ b/tests/invalid-definitions/1.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-invalid-ns-1-without-spec diff --git a/tests/invalid-definitions/2.yaml b/tests/invalid-definitions/2.yaml index aa61d9a..ea907e3 100644 --- a/tests/invalid-definitions/2.yaml +++ b/tests/invalid-definitions/2.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-invalid-ns-2-without-users diff --git a/tests/invalid-definitions/3.yaml b/tests/invalid-definitions/3.yaml index adcfbfe..267b463 100644 --- a/tests/invalid-definitions/3.yaml +++ b/tests/invalid-definitions/3.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-invalid-ns-3-only-with-sas diff --git a/tests/invalid-definitions/4.yaml b/tests/invalid-definitions/4.yaml index 088eddc..52c865d 100644 --- a/tests/invalid-definitions/4.yaml +++ b/tests/invalid-definitions/4.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-invalid-ns-4-with-invalid-sa-formats-1 diff --git a/tests/invalid-definitions/5.yaml b/tests/invalid-definitions/5.yaml index 36fa761..766c8ea 100644 --- a/tests/invalid-definitions/5.yaml +++ b/tests/invalid-definitions/5.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-invalid-ns-5-with-invalid-sa-formats-2 diff --git a/tests/invalid-definitions/6.yaml b/tests/invalid-definitions/6.yaml index 26cb1b8..403ca21 100644 --- a/tests/invalid-definitions/6.yaml +++ b/tests/invalid-definitions/6.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-invalid-ns-6-with-invalid-sa-formats-3 diff --git a/tests/invalid-definitions/7.yaml b/tests/invalid-definitions/7.yaml index 04c29c4..6786172 100644 --- a/tests/invalid-definitions/7.yaml +++ b/tests/invalid-definitions/7.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-invalid-ns-7-with-invalid-sa-formats-4 diff --git a/tests/tests.sh b/tests/tests.sh index cbfde27..7ebc1c6 100644 --- a/tests/tests.sh +++ b/tests/tests.sh @@ -21,6 +21,7 @@ function logTestStarted { serviceAccountsNamespace="default" darthVaderAccount="cops-controller-darth-vader" kyloRenAccount="cops-controller-kylo-ren" +namespaceAdminRole="${4:-devops-namespace-admin}" ######################################################################### # Arrange helpers # @@ -29,25 +30,33 @@ function setupCluster { installController=$1 repository=$2 tag=$3 + role=$4 if [ $installController == "yes" ]; then - kubectl -n kube-system create serviceaccount tiller --dry-run=true -o yaml | kubectl apply -f - - kubectl create clusterrolebinding tiller --clusterrole=cluster-admin --serviceaccount=kube-system:tiller --dry-run=true -o yaml | kubectl apply -f - - helm init --service-account tiller --wait --upgrade - - # ensure metacontroller dependency in the cluster - metacontrollerVersion="v0.4.0" - kubectl apply -f "https://raw.githubusercontent.com/GoogleCloudPlatform/metacontroller/${metacontrollerVersion}/manifests/metacontroller-namespace.yaml" - kubectl apply -f "https://raw.githubusercontent.com/GoogleCloudPlatform/metacontroller/${metacontrollerVersion}/manifests/metacontroller-rbac.yaml" - kubectl apply -f "https://raw.githubusercontent.com/GoogleCloudPlatform/metacontroller/${metacontrollerVersion}/manifests/metacontroller.yaml" + # ensure metacontroller dependency in the cluster (v0.4.0 used v1beta1 CRDs removed in k8s 1.22+; + # project moved to metacontroller/metacontroller and is now distributed via Helm) + helm upgrade --install metacontroller oci://ghcr.io/metacontroller/metacontroller-helm \ + --version=4.15.0 \ + --namespace metacontroller \ + --create-namespace \ + --skip-crds \ + --wait # install cops controller via the local chart copsControllerNamespace="coreops-cops-controller-component-test" kubectl apply -f deployment/crds + kubectl create namespace $copsControllerNamespace --dry-run=client -o yaml | kubectl apply -f - + + # uninstall cops-controller from any namespace it may already occupy (cluster-scoped resources + # like ClusterRoles cannot be owned by two releases simultaneously) + for ns in $(helm list -A -o json 2>/dev/null | python3 -c "import json,sys; [print(r['namespace']) for r in json.load(sys.stdin) if r['name']=='cops-controller']" 2>/dev/null); do + helm uninstall cops-controller --namespace $ns --wait 2>/dev/null || true + done - helm upgrade --install --wait --timeout 60 --namespace $copsControllerNamespace \ + helm upgrade --install --wait --timeout 60s --namespace $copsControllerNamespace \ --set image.repository=$repository \ --set image.tag=$tag \ + --set copsController.namespaceAdminRole=$role \ cops-controller deployment/cops-controller fi } @@ -62,12 +71,11 @@ function setupServiceAccount { testAccountNamespace=$2 # create account - kubectl create serviceaccount $testAccount -n $testAccountNamespace --dry-run=true -o yaml | kubectl apply -f - + kubectl create serviceaccount $testAccount -n $testAccountNamespace --dry-run=client -o yaml | kubectl apply -f - - # extract the secret, set into kubeconfig - serviceAccountSecret=$(kubectl get serviceaccount $testAccount -n $testAccountNamespace -o jsonpath={.secrets[0].name}) - token=$(kubectl get secret $serviceAccountSecret -n $testAccountNamespace -o jsonpath={.data.token} | base64 --decode) - kubectl config set-credentials $testAccount --token=$token + # create a token for the service account (works with k8s 1.24+ which no longer auto-creates SA secrets) + token=$(kubectl create token $testAccount -n $testAccountNamespace) + kubectl config set-credentials $testAccount --token=$token # create the new kubeconfig context kubectl config set-context $testAccount --user=$testAccount --cluster=$(kubectl config view --minify -o jsonpath={.clusters[0].name}) @@ -199,6 +207,13 @@ function test_shouldDeployEmpireCnsWithValidRbac { ensureAccessToNamespace $namespaceName ensureAllResourcesAreSupported $namespaceName + kubectl config use-context $INITIAL_CONTEXT + roleRefName=$(kubectl get rolebinding copsnamespace-user -n $namespaceName -o jsonpath='{.roleRef.name}') + if [ "$roleRefName" != "$namespaceAdminRole" ]; then + fail "Expected rolebinding roleRef to be $namespaceAdminRole but got $roleRefName" + fi + success "RoleBinding roleRef is $namespaceAdminRole as expected" + # no access for other accounts kubectl config use-context $kyloRenAccount @@ -249,7 +264,7 @@ function test_shouldUpdateEmpireCnsWithAdditionalRbac { # Test runner # ######################################################################### -setupCluster "$1" "$2" "$3" +setupCluster "$1" "$2" "$3" "$namespaceAdminRole" setupTheEmpireServiceAccounts diff --git a/tests/valid-definitions/1.yaml b/tests/valid-definitions/1.yaml index 79ed76a..b884c07 100644 --- a/tests/valid-definitions/1.yaml +++ b/tests/valid-definitions/1.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-valid-ns-1-typical-with-non-existing-sa @@ -12,4 +12,7 @@ spec: - serviceAccount: default # should work with existing sa namespace: default - serviceAccount: notyetthere # but also with non-existing - namespace: default \ No newline at end of file + namespace: default + project: + name: test-project-1 + costCenter: "42" \ No newline at end of file diff --git a/tests/valid-definitions/2.yaml b/tests/valid-definitions/2.yaml index 4530551..128f0dc 100644 --- a/tests/valid-definitions/2.yaml +++ b/tests/valid-definitions/2.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-valid-ns-2-only-users @@ -7,4 +7,7 @@ metadata: spec: namespaceAdminUsers: # should work without service account which are optional - Test.User@conplement.de - - Second.User@conplement.de \ No newline at end of file + - Second.User@conplement.de + project: + name: test-project-2 + costCenter: "42" \ No newline at end of file diff --git a/tests/valid-definitions/3.yaml b/tests/valid-definitions/3.yaml index 92658be..74d1d35 100644 --- a/tests/valid-definitions/3.yaml +++ b/tests/valid-definitions/3.yaml @@ -1,4 +1,4 @@ -apiVersion: coreops.conplement.cloud/v1 +apiVersion: coreops.conplement.cloud/v2 kind: CopsNamespace metadata: name: test-valid-ns-3-lot-of-users-and-sa @@ -37,4 +37,7 @@ spec: - serviceAccount: account4 namespace: custom - serviceAccount: account5 - namespace: custom \ No newline at end of file + namespace: custom + project: + name: test-project-3 + costCenter: "42" \ No newline at end of file