Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Models/CopsNamespace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,17 @@ public partial class CopsSpec
{
[JsonProperty("namespaceAdminUsers")]
public string[] NamespaceAdminUsers { get; set; }

[JsonProperty("namespaceAdminServiceAccounts")]
public CopsAdminServiceAccountSpec[] NamespaceAdminServiceAccounts { get; set; }
}

public class CopsAdminServiceAccountSpec
{
[JsonProperty("serviceAccount")]
public string ServiceAccount { get; set; }

[JsonProperty("namespace")]
public string Namespace { get; set; }
}
}
5 changes: 4 additions & 1 deletion Models/K8sClusterRole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ public class K8sClusterRole

public static K8sClusterRole CopsNamespaceEdit(string namespacename)
{
// this is the cluster role to ensure copsnamespace edit rights are given to the admin users after the namespace is created
// We must do this with cluster role because we need to restrict this right only to the namespace that the user created,
// and since cops namespace is a cluster scoped resource, we must do it with a cluster role with a resource filter
return new K8sClusterRole
{
Kind = "ClusterRole",
ApiVersion = "rbac.authorization.k8s.io/v1",
Metadata = new K8sMetadata { Name = $"{namespacename}-copsnamespace-edit-role" },
Metadata = new K8sMetadata { Name = $"copsnamespace-editor-{namespacename}" },
Rules = new K8sRule[]
{
new K8sRule(new[]{ "coreops.conplement.cloud" }, new[]{ "copsnamespaces" }, new[] { namespacename }, new[]{ "get", "list", "update", "patch", "delete" })
Expand Down
18 changes: 14 additions & 4 deletions Models/K8sClusterRoleBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,25 @@ public class K8sClusterRoleBinding
[JsonProperty("roleRef")]
public K8sRoleRef RoleRef { get; set; }

public static K8sClusterRoleBinding CopsNamespaceEditBinding(string namespacename, string[] users)
public static K8sClusterRoleBinding CopsNamespaceEditBinding(string namespacename, string[] users, CopsAdminServiceAccountSpec[] serviceAccounts)
{
var subjects = users.ToList()
.Select(user => { return new K8sUserSubjectItem(user, "rbac.authorization.k8s.io"); }).ToList<K8sSubjectBaseItem>()
.Concat(serviceAccounts.ToList()
.Select(sa =>
{
return new K8sServiceAccountSubjectItem(sa.ServiceAccount, sa.Namespace);
}).ToList<K8sSubjectBaseItem>()
);

// this is the concrete binding of admin users to the cops namespace edit role (which allows for the edit / delete of own namespaces)
return new K8sClusterRoleBinding
{
Kind = "ClusterRoleBinding",
ApiVersion = "rbac.authorization.k8s.io/v1",
Metadata = new K8sMetadata { Name = $"{namespacename}-copsnamespace-edit-clusterrolebinding" },
RoleRef = new K8sRoleRef("ClusterRole", $"{namespacename}-copsnamespace-edit-role", "rbac.authorization.k8s.io"),
Subjects = users.ToList().Select(user => { return new K8sUserSubjectItem(user, "rbac.authorization.k8s.io"); }).ToList<K8sSubjectBaseItem>().ToArray()
Metadata = new K8sMetadata { Name = $"copsnamespace-editor-{namespacename}" },
RoleRef = new K8sRoleRef("ClusterRole", $"copsnamespace-editor-{namespacename}", "rbac.authorization.k8s.io"),
Subjects = subjects.ToArray()
};
}
}
Expand Down
56 changes: 15 additions & 41 deletions Models/K8sRoleBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class K8sRoleBinding
[JsonProperty("roleRef")]
public K8sRoleRef RoleRef { get; set; }

public static K8sRoleBinding NamespaceFullAccess(string namespacename, string[] users)
public static K8sRoleBinding NamespaceFullAccess(string namespacename, string[] users, CopsAdminServiceAccountSpec[] serviceAccounts)
{
if (string.IsNullOrEmpty(namespacename))
{
Expand All @@ -32,15 +32,26 @@ public static K8sRoleBinding NamespaceFullAccess(string namespacename, string[]
throw new System.ArgumentNullException(nameof(users));
}

// this is the binding for admin users to get full access inside a cops namespace. The cluster role itself is
// not managed by the controller since it is a static resource deployed with rest of the static resources
// (check the Helm chart)
var roleBinding = new K8sRoleBinding
{
Kind = "RoleBinding",
ApiVersion = "rbac.authorization.k8s.io/v1",
Metadata = new K8sMetadata { Name = $"copsnamespace-full-access-rolebinding", Namespace = namespacename },
RoleRef = new K8sRoleRef("ClusterRole", "copsnamespace-full-access-role", "rbac.authorization.k8s.io")
Metadata = new K8sMetadata { Name = $"copsnamespace-user", Namespace = namespacename },
RoleRef = new K8sRoleRef("ClusterRole", "copsnamespace-user", "rbac.authorization.k8s.io")
};

var subjects = users.ToList().Select(user => { return new K8sUserSubjectItem(user, "rbac.authorization.k8s.io"); }).ToList<K8sSubjectBaseItem>();
var subjects = users.ToList()
.Select(user => { return new K8sUserSubjectItem(user, "rbac.authorization.k8s.io"); }).ToList<K8sSubjectBaseItem>()
.Concat(serviceAccounts.ToList()
.Select(sa =>
{
return new K8sServiceAccountSubjectItem(sa.ServiceAccount, sa.Namespace);
}).ToList<K8sSubjectBaseItem>()
);

roleBinding.Subjects = subjects.ToArray();

return roleBinding;
Expand All @@ -65,41 +76,4 @@ public K8sRoleRef(string kind, string name, string apigroup)
ApiGroup = apigroup;
}
}

public abstract class K8sSubjectBaseItem
{
[JsonProperty("kind")]
public string Kind { get; set; }

[JsonProperty("name")]
public string Name { get; set; }

public K8sSubjectBaseItem(string kind, string name)
{
Kind = kind;
Name = name;
}
}

public class K8sUserSubjectItem : K8sSubjectBaseItem
{
[JsonProperty("apiGroup")]
public string ApiGroup { get; set; }

public K8sUserSubjectItem(string name, string apigroup) : base("User", name)
{
ApiGroup = apigroup;
}
}

public class K8sServiceAccountSubjectItem : K8sSubjectBaseItem
{
[JsonProperty("namespace")]
public string Namespace { get; set; }

public K8sServiceAccountSubjectItem(string name, string @namespace) : base("ServiceAccount", name)
{
Namespace = @namespace;
}
}
}
41 changes: 41 additions & 0 deletions Models/K8sSubjects.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Newtonsoft.Json;

namespace ConplementAG.CopsController.Models
{
public abstract class K8sSubjectBaseItem
{
[JsonProperty("kind")]
public string Kind { get; set; }

[JsonProperty("name")]
public string Name { get; set; }

public K8sSubjectBaseItem(string kind, string name)
{
Kind = kind;
Name = name;
}
}

public class K8sUserSubjectItem : K8sSubjectBaseItem
{
[JsonProperty("apiGroup")]
public string ApiGroup { get; set; }

public K8sUserSubjectItem(string name, string apigroup) : base("User", name)
{
ApiGroup = apigroup;
}
}

public class K8sServiceAccountSubjectItem : K8sSubjectBaseItem
{
[JsonProperty("namespace")]
public string Namespace { get; set; }

public K8sServiceAccountSubjectItem(string name, string @namespace) : base("ServiceAccount", name)
{
Namespace = @namespace;
}
}
}
4 changes: 2 additions & 2 deletions Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Serilog;
using Serilog.Formatting.Elasticsearch;
using Serilog.Formatting.Json;

namespace ConplementAG.CopsController
{
Expand All @@ -18,7 +18,7 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
{
config.Enrich.FromLogContext();
config.MinimumLevel.Debug();
config.WriteTo.Console(new ElasticsearchJsonFormatter());
config.WriteTo.Console(new JsonFormatter());
})
.UseStartup<Startup>();
}
Expand Down
6 changes: 3 additions & 3 deletions Services/K8sResourceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ public static IList<object> Create(CopsResource resource)
return (IList<object>)method.Invoke(null, new[] { source });
}

// Methode used by reflection call
// Method used by reflection call
private static IList<object> Create(CopsNamespace copsNamespace)
{
return new List<object>
{
new K8sNamespace(copsNamespace.Metadata.Name),
K8sRoleBinding.NamespaceFullAccess(copsNamespace.Metadata.Name, copsNamespace.Spec.NamespaceAdminUsers),
K8sClusterRoleBinding.CopsNamespaceEditBinding(copsNamespace.Metadata.Name, copsNamespace.Spec.NamespaceAdminUsers),
K8sRoleBinding.NamespaceFullAccess(copsNamespace.Metadata.Name, copsNamespace.Spec.NamespaceAdminUsers, copsNamespace.Spec.NamespaceAdminServiceAccounts),
K8sClusterRoleBinding.CopsNamespaceEditBinding(copsNamespace.Metadata.Name, copsNamespace.Spec.NamespaceAdminUsers, copsNamespace.Spec.NamespaceAdminServiceAccounts),
K8sClusterRole.CopsNamespaceEdit(copsNamespace.Metadata.Name)
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: copsnamespace-inital-create-role
name: copsnamespace-creator
rules:
- apiGroups: ["coreops.conplement.cloud"]
resources: ["CopsNamespace"]
resources: ["copsnamespaces"]
verbs: ["get", "list", "create"]
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: copsnamespace-full-access-role
name: copsnamespace-user
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: [ "rolebindings", "roles" ]
Expand Down
22 changes: 19 additions & 3 deletions deployment/crds/copsnamespace.crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,25 @@ spec:
properties:
namespaceAdminUsers:
description: |-
Array with all the namespace administrators. You need to reference the administrators by specifying
Array with all the namespace administrator users. You need to reference the administrators by specifying
their Conplement Azure AD User IDs, which is usually in the form of
Firstname.Lastname@conplement.de (notice the capital letters!)"
Firstname.Lastname@conplement.de (notice the capital letters!)
type: array
items:
type: string
type: string
namespaceAdminServiceAccounts:
description: |-
Array with all the namespace administrator service accounts.
type: array
items:
type: object
required:
- serviceAccount
- namespace
properties:
serviceAccount:
description: |-
Service account name
namespace:
description: |-
Namespace where the service account is located
6 changes: 0 additions & 6 deletions examples/invalid-namespace-definition.yaml

This file was deleted.

8 changes: 0 additions & 8 deletions examples/valid-namespace-definition.yaml

This file was deleted.

101 changes: 101 additions & 0 deletions run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/bin/bash

set -eo pipefail
programname=$0

UNIVERSAL_TEST_IDENTIFIED="cops-controller-component-tests"

function usage {
echo "usage: $programname [--install-helm-and-cops-controller] [-r repository] [-t tag]"
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 " "
echo "Prerequisites: "
echo " To run the tests, you need a running k8s cluster and docker engine"
echo " "
echo "Test cluster setup:"
echo " 1. For example, you can install Docker for Windows or Minikube, or use a remote cluster"
echo " Ubuntu instructions:"
echo " - install VirtualBox"
echo " - curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && chmod +x minikube"
echo " - sudo install minikube /usr/local/bin/"
echo " 2. The cluster has to have access to the docker registry with the pushed image. Either you push to a remote registry,"
echo " or you can use something like minikube docker-env to share the images on your machine."
echo " Ubuntu with minikube instructions:"
echo " - minikube start"
echo " - import variables with eval from: minikube docker-env"
echo " - docker build . -t cops-controller:latest"
echo " - ./run_tests.sh --install-helm-and-cops-controller -r cops-controller -t latest"
exit 1
}

function colorecho {
RED="\033[0;31m"
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
# ... ADD MORE COLORS
NC="\033[0m" # No Color

printf "${!1}${2} ${NC}\n"
}

# the cleanup function will be the exit point
INITIAL_CONTEXT=$(kubectl config current-context)

function cleanup {
exit_code=$?
echo "Setting back the initial context $INITIAL_CONTEXT"

kubectl config use-context $INITIAL_CONTEXT

echo "Deleting all cops namespaces for cleanup..."

namespaces=$(kubectl get cns -l tests=$UNIVERSAL_TEST_IDENTIFIED -o name)

if [ -n "$namespaces" ]; then
kubectl delete $namespaces --ignore-not-found
fi

echo "Deleting all remaining RBAC rules..."
clusterRoles=$(kubectl get clusterroles -o name -l tests=$UNIVERSAL_TEST_IDENTIFIED)
clusterRoleBindings=$(kubectl get clusterrolebindings -o name -l tests=$UNIVERSAL_TEST_IDENTIFIED)

if [ -n "$clusterRoles" ]; then
kubectl delete $clusterRoles --ignore-not-found
fi

if [ -n "$clusterRoleBindings" ]; then
kubectl delete $clusterRoleBindings --ignore-not-found
fi

if [ $exit_code != 0 ]; then
colorecho "RED" "Tests failed, check for the last error occured."
else
colorecho "GREEN" "All tests succeeded."
colorecho "GREEN" "Stdout could contain some failure statements, but this is because we pipe everything to stdout, even the failure checks where we expect to fail."
fi

exit $exit_code
}

installController="no"
repository=""
tag=""

if [ -n "$1" ]; then # if any argument specified
# all parameters mandatory now, in correct order
if [ $1 != "--install-helm-and-cops-controller" ] || [ $2 != "-r" ] || [ -z "$3" ] || [ $4 != "-t" ] || [ -z "$5" ]; then
usage
else
installController="yes"
repository=$3
tag=$5
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"
Loading