Compare commits

..

32 Commits

Author SHA1 Message Date
Dario Tranchitella
c0d4aab582 build(helm): CRD update for PriorityClass enum 2021-07-21 16:48:13 +02:00
Dario Tranchitella
6761fb93dc build(kustomize): CRD update for PriorityClass enum 2021-07-21 16:48:13 +02:00
Dario Tranchitella
bf9e0f6b10 test: PriorityClass proxy operations conversion 2021-07-21 16:48:13 +02:00
Dario Tranchitella
f937942c49 feat: capsule-proxy operations for PriorityClass resources 2021-07-21 16:48:13 +02:00
Dario Tranchitella
89d7f301c6 build(helm): CRD update for v1beta1 service options 2021-07-21 14:34:56 +02:00
Dario Tranchitella
2a6ff09340 build(kustomize): CRD update for v1beta1 service options 2021-07-21 14:34:56 +02:00
Dario Tranchitella
35f48107fc test(e2e): aligning tests to new v1beta1 structure and ExternalName case 2021-07-21 14:34:56 +02:00
Dario Tranchitella
7aa62b6f1d test: conversion for new Service options 2021-07-21 14:34:56 +02:00
Dario Tranchitella
58645f39bb chore(samples): example for ServiceOptions 2021-07-21 14:34:56 +02:00
Dario Tranchitella
0e55823a0c feat: toggling ExternalName service 2021-07-21 14:34:56 +02:00
Maksim Fedotov
ba690480a7 refactor: use OwnerListSpec to store tenant owners information 2021-07-20 11:21:40 +02:00
Maksim Fedotov
faa2306a30 chore: support multiple groups in create-{user}/{user-openshift}.sh scripts 2021-07-20 11:21:40 +02:00
Dario Tranchitella
c1448c82e9 build(installer): add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
776a56b5bc build(helm): add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
e4883bb737 build(kustomize): add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
e70afb5e77 feat: add description fields in CRD 2021-07-19 17:07:19 +02:00
Dario Tranchitella
ee7af18f98 docs: bare installation of Capsule using kubectl 2021-07-19 15:21:56 +02:00
Dario Tranchitella
ac7de3bf88 chore(github): updating steps for single YAML file installer diffs 2021-07-19 15:21:56 +02:00
Dario Tranchitella
8883b15aa9 chore: single YAML file installer 2021-07-19 15:21:56 +02:00
Dario Tranchitella
e23132c820 chore(kustomize): using single YAML file to install Capsule 2021-07-19 15:21:56 +02:00
Dario Tranchitella
bec59a585e build(kustomize): updating to v0.1.0-rc3 2021-07-19 15:21:56 +02:00
Dario Tranchitella
9c649ac7eb chore(kustomize): adding v1beta1 Tenant 2021-07-19 15:05:22 +02:00
Dario Tranchitella
3455aed503 fix(samples): Tenant v1beta1 example 2021-07-19 15:05:22 +02:00
Dario Tranchitella
ad1edf57ac fix(samples): removing empty file 2021-07-19 15:05:22 +02:00
Dario Tranchitella
d64dcb5a44 fix: preserving v1alpha1 enable node ports false value avoiding CRD default 2021-07-19 08:15:24 +02:00
Dave
76d7697703 docs: minor improvements 2021-07-16 17:52:16 +02:00
alegrey91
96f4f31c17 docs(velero): add brief explanation about new cli flag 2021-07-16 09:19:36 +02:00
alegrey91
c3f9dfe652 feat(velero): improve usage function 2021-07-16 09:19:36 +02:00
alegrey91
502e9a556f feat(velero): add possibility to specify a tenant list by cli 2021-07-16 09:19:36 +02:00
alegrey91
6f208a6e0e fix(velero): fix wrong argument behaviour 2021-07-16 09:19:36 +02:00
alegrey91
1fb52003d5 fix(velero): add possibility to fix also apiVersion parameter 2021-07-16 09:19:36 +02:00
Dario Tranchitella
98e1640d9b fix: avoid nil slice during resource conversion 2021-07-14 20:54:43 +02:00
67 changed files with 2581 additions and 232 deletions

View File

@@ -23,6 +23,8 @@ jobs:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache Go modules
uses: actions/cache@v1
env:
@@ -35,10 +37,10 @@ jobs:
restore-keys: |
${{ runner.os }}-build-
${{ runner.os }}-
- run: make manifests
- name: Checking if manifests are disaligned
- run: make installer
- name: Checking if YAML installer file is not aligned
run: if [[ $(git diff | wc -l) -gt 0 ]]; then echo ">>> Untracked generated files have not been committed" && git --no-pager diff && exit 1; fi
- name: Checking if manifests generated untracked files
- name: Checking if YAML installer generated untracked files
run: test -z "$(git ls-files --others --exclude-standard 2> /dev/null)"
- name: Checking if source code is not formatted
run: test -z "$(git diff 2> /dev/null)"

View File

@@ -47,22 +47,26 @@ manager: generate fmt vet
run: generate manifests
go run ./main.go
# Creates the single file to install Capsule without any external dependency
installer: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > config/install.yaml
# Install CRDs into a cluster
install: manifests kustomize
install: installer
$(KUSTOMIZE) build config/crd | kubectl apply -f -
# Uninstall CRDs from a cluster
uninstall: manifests kustomize
uninstall: installer
$(KUSTOMIZE) build config/crd | kubectl delete -f -
# Deploy controller in the configured Kubernetes cluster in ~/.kube/config
deploy: manifests kustomize
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | kubectl apply -f -
deploy: installer
kubectl apply -f config/install.yaml
# Remove controller in the configured Kubernetes cluster in ~/.kube/config
remove: manifests kustomize
$(KUSTOMIZE) build config/default | kubectl delete -f -
remove: installer
kubectl delete -f config/install.yaml
kubectl delete clusterroles.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found
kubectl delete clusterrolebindings.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found

View File

@@ -61,17 +61,28 @@ Make sure you have access to a Kubernetes cluster as administrator.
There are two ways to install Capsule:
* Use the Helm Chart available [here](./charts/capsule/README.md)
* Use [`kustomize`](https://github.com/kubernetes-sigs/kustomize)
* Use the [single YAML file installer](./config/install.yaml)
## Install with kustomize
Ensure you have `kubectl` and `kustomize` installed in your `PATH`.
## Install with the single YAML file installer
Ensure you have `kubectl` installed in your `PATH`.
Clone this repository and move to the repo folder:
```
$ git clone https://github.com/clastix/capsule
$ cd capsule
$ make deploy
$ kubectl apply -f https://raw.githubusercontent.com/clastix/capsule/master/config/install.yaml
namespace/capsule-system created
customresourcedefinition.apiextensions.k8s.io/capsuleconfigurations.capsule.clastix.io created
customresourcedefinition.apiextensions.k8s.io/tenants.capsule.clastix.io created
clusterrolebinding.rbac.authorization.k8s.io/capsule-manager-rolebinding created
secret/capsule-ca created
secret/capsule-tls created
service/capsule-controller-manager-metrics-service created
service/capsule-webhook-service created
deployment.apps/capsule-controller-manager created
capsuleconfiguration.capsule.clastix.io/capsule-default created
mutatingwebhookconfiguration.admissionregistration.k8s.io/capsule-mutating-webhook-configuration created
validatingwebhookconfiguration.admissionregistration.k8s.io/capsule-validating-webhook-configuration created
```
It will install the Capsule controller in a dedicated namespace `capsule-system`.

View File

@@ -10,6 +10,7 @@ import (
"strings"
"github.com/pkg/errors"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/conversion"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
@@ -21,52 +22,52 @@ const (
podPriorityAllowedAnnotation = "priorityclass.capsule.clastix.io/allowed"
podPriorityAllowedRegexAnnotation = "priorityclass.capsule.clastix.io/allowed-regex"
enableNodePortsAnnotation = "capsule.clastix.io/enable-node-ports"
enableNodePortsAnnotation = "capsule.clastix.io/enable-node-ports"
enableExternalNameAnnotation = "capsule.clastix.io/enable-external-name"
ownerGroupsAnnotation = "owners.capsule.clastix.io/group"
ownerUsersAnnotation = "owners.capsule.clastix.io/user"
ownerServiceAccountAnnotation = "owners.capsule.clastix.io/serviceaccount"
enableNodeListingAnnotation = "capsule.clastix.io/enable-node-listing"
enableNodeUpdateAnnotation = "capsule.clastix.io/enable-node-update"
enableNodeDeletionAnnotation = "capsule.clastix.io/enable-node-deletion"
enableStorageClassListingAnnotation = "capsule.clastix.io/enable-storageclass-listing"
enableStorageClassUpdateAnnotation = "capsule.clastix.io/enable-storageclass-update"
enableStorageClassDeletionAnnotation = "capsule.clastix.io/enable-storageclass-deletion"
enableIngressClassListingAnnotation = "capsule.clastix.io/enable-ingressclass-listing"
enableIngressClassUpdateAnnotation = "capsule.clastix.io/enable-ingressclass-update"
enableIngressClassDeletionAnnotation = "capsule.clastix.io/enable-ingressclass-deletion"
listOperation = "List"
updateOperation = "Update"
deleteOperation = "Delete"
nodesServiceKind = "Nodes"
storageClassesServiceKind = "StorageClasses"
ingressClassesServiceKind = "IngressClasses"
enableNodeListingAnnotation = "capsule.clastix.io/enable-node-listing"
enableNodeUpdateAnnotation = "capsule.clastix.io/enable-node-update"
enableNodeDeletionAnnotation = "capsule.clastix.io/enable-node-deletion"
enableStorageClassListingAnnotation = "capsule.clastix.io/enable-storageclass-listing"
enableStorageClassUpdateAnnotation = "capsule.clastix.io/enable-storageclass-update"
enableStorageClassDeletionAnnotation = "capsule.clastix.io/enable-storageclass-deletion"
enableIngressClassListingAnnotation = "capsule.clastix.io/enable-ingressclass-listing"
enableIngressClassUpdateAnnotation = "capsule.clastix.io/enable-ingressclass-update"
enableIngressClassDeletionAnnotation = "capsule.clastix.io/enable-ingressclass-deletion"
enablePriorityClassListingAnnotation = "capsule.clastix.io/enable-priorityclass-listing"
enablePriorityClassUpdateAnnotation = "capsule.clastix.io/enable-priorityclass-update"
enablePriorityClassDeletionAnnotation = "capsule.clastix.io/enable-priorityclass-deletion"
)
func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() []capsulev1beta1.OwnerSpec {
func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() capsulev1beta1.OwnerListSpec {
var serviceKindToAnnotationMap = map[capsulev1beta1.ProxyServiceKind][]string{
nodesServiceKind: {enableNodeListingAnnotation, enableNodeUpdateAnnotation, enableNodeDeletionAnnotation},
storageClassesServiceKind: {enableStorageClassListingAnnotation, enableStorageClassUpdateAnnotation, enableStorageClassDeletionAnnotation},
ingressClassesServiceKind: {enableIngressClassListingAnnotation, enableIngressClassUpdateAnnotation, enableIngressClassDeletionAnnotation},
capsulev1beta1.NodesProxy: {enableNodeListingAnnotation, enableNodeUpdateAnnotation, enableNodeDeletionAnnotation},
capsulev1beta1.StorageClassesProxy: {enableStorageClassListingAnnotation, enableStorageClassUpdateAnnotation, enableStorageClassDeletionAnnotation},
capsulev1beta1.IngressClassesProxy: {enableIngressClassListingAnnotation, enableIngressClassUpdateAnnotation, enableIngressClassDeletionAnnotation},
capsulev1beta1.PriorityClassesProxy: {enablePriorityClassListingAnnotation, enablePriorityClassUpdateAnnotation, enablePriorityClassDeletionAnnotation},
}
var annotationToOperationMap = map[string]capsulev1beta1.ProxyOperation{
enableNodeListingAnnotation: listOperation,
enableNodeUpdateAnnotation: updateOperation,
enableNodeDeletionAnnotation: deleteOperation,
enableStorageClassListingAnnotation: listOperation,
enableStorageClassUpdateAnnotation: updateOperation,
enableStorageClassDeletionAnnotation: deleteOperation,
enableIngressClassListingAnnotation: listOperation,
enableIngressClassUpdateAnnotation: updateOperation,
enableIngressClassDeletionAnnotation: deleteOperation,
enableNodeListingAnnotation: capsulev1beta1.ListOperation,
enableNodeUpdateAnnotation: capsulev1beta1.UpdateOperation,
enableNodeDeletionAnnotation: capsulev1beta1.DeleteOperation,
enableStorageClassListingAnnotation: capsulev1beta1.ListOperation,
enableStorageClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
enableStorageClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
enableIngressClassListingAnnotation: capsulev1beta1.ListOperation,
enableIngressClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
enableIngressClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
enablePriorityClassListingAnnotation: capsulev1beta1.ListOperation,
enablePriorityClassUpdateAnnotation: capsulev1beta1.UpdateOperation,
enablePriorityClassDeletionAnnotation: capsulev1beta1.DeleteOperation,
}
var annotationToOwnerKindMap = map[string]capsulev1beta1.OwnerKind{
ownerUsersAnnotation: "User",
ownerGroupsAnnotation: "Group",
ownerServiceAccountAnnotation: "ServiceAccount",
ownerUsersAnnotation: capsulev1beta1.UserOwner,
ownerGroupsAnnotation: capsulev1beta1.GroupOwner,
ownerServiceAccountAnnotation: capsulev1beta1.ServiceAccountOwner,
}
annotations := t.GetAnnotations()
@@ -86,7 +87,7 @@ func (t *Tenant) convertV1Alpha1OwnerToV1Beta1() []capsulev1beta1.OwnerSpec {
}
}
var owners []capsulev1beta1.OwnerSpec
var owners capsulev1beta1.OwnerListSpec
var getProxySettingsForOwner = func(ownerName string) (settings []capsulev1beta1.ProxySettings) {
ownerOperations, ok := operations[ownerName]
@@ -143,9 +144,13 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
}
}
if t.Spec.ServicesMetadata != nil {
dst.Spec.ServicesMetadata = &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: t.Spec.ServicesMetadata.AdditionalLabels,
AdditionalAnnotations: t.Spec.ServicesMetadata.AdditionalAnnotations,
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{
AdditionalMetadata: &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: t.Spec.ServicesMetadata.AdditionalLabels,
AdditionalAnnotations: t.Spec.ServicesMetadata.AdditionalAnnotations,
},
}
}
}
if t.Spec.StorageClasses != nil {
@@ -196,13 +201,12 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
}
}
if t.Spec.ExternalServiceIPs != nil {
var allowedIPs []capsulev1beta1.AllowedIP
for _, IP := range t.Spec.ExternalServiceIPs.Allowed {
allowedIPs = append(allowedIPs, capsulev1beta1.AllowedIP(IP))
dst.Spec.ExternalServiceIPs = &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: make([]capsulev1beta1.AllowedIP, len(t.Spec.ExternalServiceIPs.Allowed)),
}
dst.Spec.ExternalServiceIPs = &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: allowedIPs,
for i, IP := range t.Spec.ExternalServiceIPs.Allowed {
dst.Spec.ExternalServiceIPs.Allowed[i] = capsulev1beta1.AllowedIP(IP)
}
}
@@ -234,7 +238,28 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableNodePortsAnnotation, t.GetName()))
}
dst.Spec.EnableNodePorts = val
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
if dst.Spec.ServiceOptions.AllowedServices == nil {
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
}
dst.Spec.ServiceOptions.AllowedServices.NodePort = pointer.BoolPtr(val)
}
enableExternalName, ok := annotations[enableExternalNameAnnotation]
if ok {
val, err := strconv.ParseBool(enableExternalName)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableExternalNameAnnotation, t.GetName()))
}
if dst.Spec.ServiceOptions == nil {
dst.Spec.ServiceOptions = &capsulev1beta1.ServiceOptions{}
}
if dst.Spec.ServiceOptions.AllowedServices == nil {
dst.Spec.ServiceOptions.AllowedServices = &capsulev1beta1.AllowedServices{}
}
dst.Spec.ServiceOptions.AllowedServices.ExternalName = pointer.BoolPtr(val)
}
// Status
@@ -248,6 +273,7 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
delete(dst.ObjectMeta.Annotations, podPriorityAllowedAnnotation)
delete(dst.ObjectMeta.Annotations, podPriorityAllowedRegexAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodePortsAnnotation)
delete(dst.ObjectMeta.Annotations, enableExternalNameAnnotation)
delete(dst.ObjectMeta.Annotations, ownerGroupsAnnotation)
delete(dst.ObjectMeta.Annotations, ownerUsersAnnotation)
delete(dst.ObjectMeta.Annotations, ownerServiceAccountAnnotation)
@@ -260,6 +286,9 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
delete(dst.ObjectMeta.Annotations, enableIngressClassListingAnnotation)
delete(dst.ObjectMeta.Annotations, enableIngressClassUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enableIngressClassDeletionAnnotation)
delete(dst.ObjectMeta.Annotations, enablePriorityClassListingAnnotation)
delete(dst.ObjectMeta.Annotations, enablePriorityClassUpdateAnnotation)
delete(dst.ObjectMeta.Annotations, enablePriorityClassDeletionAnnotation)
return nil
}
@@ -291,46 +320,57 @@ func (t *Tenant) convertV1Beta1OwnerToV1Alpha1(src *capsulev1beta1.Tenant) {
}
} else {
switch owner.Kind {
case "User":
case capsulev1beta1.UserOwner:
ownersAnnotations[ownerUsersAnnotation] = append(ownersAnnotations[ownerUsersAnnotation], owner.Name)
case "Group":
case capsulev1beta1.GroupOwner:
ownersAnnotations[ownerGroupsAnnotation] = append(ownersAnnotations[ownerGroupsAnnotation], owner.Name)
case "ServiceAccount":
case capsulev1beta1.ServiceAccountOwner:
ownersAnnotations[ownerServiceAccountAnnotation] = append(ownersAnnotations[ownerServiceAccountAnnotation], owner.Name)
}
}
for _, setting := range owner.ProxyOperations {
switch setting.Kind {
case nodesServiceKind:
case capsulev1beta1.NodesProxy:
for _, operation := range setting.Operations {
switch operation {
case listOperation:
case capsulev1beta1.ListOperation:
proxyAnnotations[enableNodeListingAnnotation] = append(proxyAnnotations[enableNodeListingAnnotation], owner.Name)
case updateOperation:
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enableNodeUpdateAnnotation] = append(proxyAnnotations[enableNodeUpdateAnnotation], owner.Name)
case deleteOperation:
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enableNodeDeletionAnnotation] = append(proxyAnnotations[enableNodeDeletionAnnotation], owner.Name)
}
}
case storageClassesServiceKind:
case capsulev1beta1.PriorityClassesProxy:
for _, operation := range setting.Operations {
switch operation {
case listOperation:
case capsulev1beta1.ListOperation:
proxyAnnotations[enablePriorityClassListingAnnotation] = append(proxyAnnotations[enablePriorityClassListingAnnotation], owner.Name)
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enablePriorityClassUpdateAnnotation] = append(proxyAnnotations[enablePriorityClassUpdateAnnotation], owner.Name)
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enablePriorityClassDeletionAnnotation] = append(proxyAnnotations[enablePriorityClassDeletionAnnotation], owner.Name)
}
}
case capsulev1beta1.StorageClassesProxy:
for _, operation := range setting.Operations {
switch operation {
case capsulev1beta1.ListOperation:
proxyAnnotations[enableStorageClassListingAnnotation] = append(proxyAnnotations[enableStorageClassListingAnnotation], owner.Name)
case updateOperation:
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enableStorageClassUpdateAnnotation] = append(proxyAnnotations[enableStorageClassUpdateAnnotation], owner.Name)
case deleteOperation:
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enableStorageClassDeletionAnnotation] = append(proxyAnnotations[enableStorageClassDeletionAnnotation], owner.Name)
}
}
case ingressClassesServiceKind:
case capsulev1beta1.IngressClassesProxy:
for _, operation := range setting.Operations {
switch operation {
case listOperation:
case capsulev1beta1.ListOperation:
proxyAnnotations[enableIngressClassListingAnnotation] = append(proxyAnnotations[enableIngressClassListingAnnotation], owner.Name)
case updateOperation:
case capsulev1beta1.UpdateOperation:
proxyAnnotations[enableIngressClassUpdateAnnotation] = append(proxyAnnotations[enableIngressClassUpdateAnnotation], owner.Name)
case deleteOperation:
case capsulev1beta1.DeleteOperation:
proxyAnnotations[enableIngressClassDeletionAnnotation] = append(proxyAnnotations[enableIngressClassDeletionAnnotation], owner.Name)
}
}
@@ -372,10 +412,10 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
AdditionalAnnotations: src.Spec.NamespacesMetadata.AdditionalAnnotations,
}
}
if src.Spec.ServicesMetadata != nil {
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AdditionalMetadata != nil {
t.Spec.ServicesMetadata = &AdditionalMetadataSpec{
AdditionalLabels: src.Spec.ServicesMetadata.AdditionalLabels,
AdditionalAnnotations: src.Spec.ServicesMetadata.AdditionalAnnotations,
AdditionalLabels: src.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels,
AdditionalAnnotations: src.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations,
}
}
if src.Spec.StorageClasses != nil {
@@ -420,13 +460,12 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
}
}
if src.Spec.ExternalServiceIPs != nil {
var allowedIPs []AllowedIP
for _, IP := range src.Spec.ExternalServiceIPs.Allowed {
allowedIPs = append(allowedIPs, AllowedIP(IP))
t.Spec.ExternalServiceIPs = &ExternalServiceIPsSpec{
Allowed: make([]AllowedIP, len(src.Spec.ExternalServiceIPs.Allowed)),
}
t.Spec.ExternalServiceIPs = &ExternalServiceIPsSpec{
Allowed: allowedIPs,
for i, IP := range src.Spec.ExternalServiceIPs.Allowed {
t.Spec.ExternalServiceIPs.Allowed[i] = AllowedIP(IP)
}
}
if len(src.Spec.ImagePullPolicies) != 0 {
@@ -446,7 +485,10 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
}
}
t.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(src.Spec.EnableNodePorts)
if src.Spec.ServiceOptions != nil && src.Spec.ServiceOptions.AllowedServices != nil {
t.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.NodePort)
t.Annotations[enableExternalNameAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.ExternalName)
}
// Status
t.Status = TenantStatus{

View File

@@ -13,6 +13,7 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
@@ -42,6 +43,13 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
"foo": "bar",
},
}
var v1beta1ServiceOptions = &capsulev1beta1.ServiceOptions{
AdditionalMetadata: v1beta1AdditionalMetadataSpec,
AllowedServices: &capsulev1beta1.AllowedServices{
NodePort: pointer.BoolPtr(false),
ExternalName: pointer.BoolPtr(false),
},
}
var v1beta1AllowedListSpec = &capsulev1beta1.AllowedListSpec{
Exact: []string{"foo", "bar"},
Regex: "^foo*",
@@ -114,7 +122,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
},
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Kind: "User",
Name: "alice",
@@ -163,6 +171,10 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
Kind: "StorageClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
{
Kind: "PriorityClasses",
Operations: []capsulev1beta1.ProxyOperation{"List"},
},
},
},
{
@@ -212,7 +224,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
},
NamespaceQuota: &namespaceQuota,
NamespacesMetadata: v1beta1AdditionalMetadataSpec,
ServicesMetadata: v1beta1AdditionalMetadataSpec,
ServiceOptions: v1beta1ServiceOptions,
StorageClasses: v1beta1AllowedListSpec,
IngressClasses: v1beta1AllowedListSpec,
IngressHostnames: v1beta1AllowedListSpec,
@@ -247,7 +259,6 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
Exact: []string{"default"},
Regex: "^tier-.*$",
},
EnableNodePorts: false,
},
Status: capsulev1beta1.TenantStatus{
Size: 1,
@@ -265,6 +276,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
Annotations: map[string]string{
"foo": "bar",
podAllowedImagePullPolicyAnnotation: "Always,IfNotPresent",
enableExternalNameAnnotation: "false",
enableNodePortsAnnotation: "false",
podPriorityAllowedAnnotation: "default",
podPriorityAllowedRegexAnnotation: "^tier-.*$",
@@ -279,6 +291,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
enableIngressClassListingAnnotation: "alice,owner-foo,owner-bar",
enableIngressClassUpdateAnnotation: "alice,bob",
enableIngressClassDeletionAnnotation: "alice,jack",
enablePriorityClassListingAnnotation: "jack",
},
},
Spec: TenantSpec{

View File

@@ -3,10 +3,12 @@
package v1beta1
// OwnerSpec defines tenant owner name and kind
type OwnerSpec struct {
Kind OwnerKind `json:"kind"`
Name string `json:"name"`
// Kind of tenant owner. Possible values are "User", "Group", and "ServiceAccount"
Kind OwnerKind `json:"kind"`
// Name of tenant owner.
Name string `json:"name"`
// Proxy settings for tenant owner.
ProxyOperations []ProxySettings `json:"proxySettings,omitempty"`
}
@@ -29,9 +31,24 @@ func (p ProxyOperation) String() string {
return string(p)
}
// +kubebuilder:validation:Enum=Nodes;StorageClasses;IngressClasses
// +kubebuilder:validation:Enum=Nodes;StorageClasses;IngressClasses;PriorityClasses
type ProxyServiceKind string
func (p ProxyServiceKind) String() string {
return string(p)
}
const (
NodesProxy ProxyServiceKind = "Nodes"
StorageClassesProxy ProxyServiceKind = "StorageClasses"
IngressClassesProxy ProxyServiceKind = "IngressClasses"
PriorityClassesProxy ProxyServiceKind = "PriorityClasses"
ListOperation ProxyOperation = "List"
UpdateOperation ProxyOperation = "Update"
DeleteOperation ProxyOperation = "Delete"
UserOwner OwnerKind = "User"
GroupOwner OwnerKind = "Group"
ServiceAccountOwner OwnerKind = "ServiceAccount"
)

34
api/v1beta1/owner_list.go Normal file
View File

@@ -0,0 +1,34 @@
package v1beta1
import (
"sort"
)
type OwnerListSpec []OwnerSpec
func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec) {
sort.Sort(ByKindAndName(o))
i := sort.Search(len(o), func(i int) bool {
return o[i].Kind >= kind && o[i].Name >= name
})
if i < len(o) && o[i].Kind == kind && o[i].Name == name {
return o[i]
}
return
}
type ByKindAndName OwnerListSpec
func (b ByKindAndName) Len() int {
return len(b)
}
func (b ByKindAndName) Less(i, j int) bool {
if b[i].Kind.String() != b[j].Kind.String() {
return b[i].Kind.String() < b[j].Kind.String()
}
return b[i].Name < b[j].Name
}
func (b ByKindAndName) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}

View File

@@ -0,0 +1,83 @@
package v1beta1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOwnerListSpec_FindOwner(t *testing.T) {
var bla = OwnerSpec{
Kind: UserOwner,
Name: "bla",
ProxyOperations: []ProxySettings{
{
Kind: IngressClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
var bar = OwnerSpec{
Kind: GroupOwner,
Name: "bar",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
var baz = OwnerSpec{
Kind: UserOwner,
Name: "baz",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Update"},
},
},
}
var fim = OwnerSpec{
Kind: ServiceAccountOwner,
Name: "fim",
ProxyOperations: []ProxySettings{
{
Kind: NodesProxy,
Operations: []ProxyOperation{"List"},
},
},
}
var bom = OwnerSpec{
Kind: GroupOwner,
Name: "bom",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"Delete"},
},
{
Kind: NodesProxy,
Operations: []ProxyOperation{"Delete"},
},
},
}
var qip = OwnerSpec{
Kind: ServiceAccountOwner,
Name: "qip",
ProxyOperations: []ProxySettings{
{
Kind: StorageClassesProxy,
Operations: []ProxyOperation{"List", "Delete"},
},
},
}
var owners = OwnerListSpec{bom, qip, bla, bar, baz, fim}
assert.Equal(t, owners.FindOwner("bom", GroupOwner), bom)
assert.Equal(t, owners.FindOwner("qip", ServiceAccountOwner), qip)
assert.Equal(t, owners.FindOwner("bla", UserOwner), bla)
assert.Equal(t, owners.FindOwner("bar", GroupOwner), bar)
assert.Equal(t, owners.FindOwner("baz", UserOwner), baz)
assert.Equal(t, owners.FindOwner("fim", ServiceAccountOwner), fim)
assert.Equal(t, owners.FindOwner("notfound", ServiceAccountOwner), OwnerSpec{})
}

View File

@@ -0,0 +1,20 @@
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1beta1
type ServiceOptions struct {
// Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
AdditionalMetadata *AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
// Block or deny certain type of Services. Optional.
AllowedServices *AllowedServices `json:"allowedServices,omitempty"`
}
type AllowedServices struct {
//+kubebuilder:default=true
// Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
NodePort *bool `json:"nodePort,omitempty"`
//+kubebuilder:default=true
// Specifies if ExternalName service type resources are allowed for the Tenant. Default is true. Optional.
ExternalName *bool `json:"externalName,omitempty"`
}

View File

@@ -36,3 +36,7 @@ func (t *Tenant) AssignNamespaces(namespaces []corev1.Namespace) {
t.Status.Namespaces = l
t.Status.Size = uint(len(l))
}
func (t *Tenant) GetOwnerProxySettings(name string, kind OwnerKind) []ProxySettings {
return t.Spec.Owners.FindOwner(name, kind).ProxyOperations
}

View File

@@ -11,10 +11,13 @@ const (
TenantStateCordoned tenantState = "cordoned"
)
// TenantStatus defines the observed state of Tenant
// Returns the observed state of the Tenant
type TenantStatus struct {
//+kubebuilder:default=active
State tenantState `json:"state"`
Size uint `json:"size"`
Namespaces []string `json:"namespaces,omitempty"`
// The operational state of the Tenant. Possible values are "active", "cordoned".
State tenantState `json:"state"`
// How many namespaces are assigned to the Tenant.
Size uint `json:"size"`
// List of namespaces assigned to the Tenant.
Namespaces []string `json:"namespaces,omitempty"`
}

View File

@@ -9,27 +9,40 @@ import (
// TenantSpec defines the desired state of Tenant
type TenantSpec struct {
Owners []OwnerSpec `json:"owners"`
// Specifies the owners of the Tenant. Mandatory.
Owners OwnerListSpec `json:"owners"`
//+kubebuilder:validation:Minimum=1
NamespaceQuota *int32 `json:"namespaceQuota,omitempty"`
NamespacesMetadata *AdditionalMetadataSpec `json:"namespacesMetadata,omitempty"`
ServicesMetadata *AdditionalMetadataSpec `json:"servicesMetadata,omitempty"`
StorageClasses *AllowedListSpec `json:"storageClasses,omitempty"`
IngressClasses *AllowedListSpec `json:"ingressClasses,omitempty"`
IngressHostnames *AllowedListSpec `json:"ingressHostnames,omitempty"`
ContainerRegistries *AllowedListSpec `json:"containerRegistries,omitempty"`
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
NetworkPolicies *NetworkPolicySpec `json:"networkPolicies,omitempty"`
LimitRanges *LimitRangesSpec `json:"limitRanges,omitempty"`
ResourceQuota *ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
NamespaceQuota *int32 `json:"namespaceQuota,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
NamespacesMetadata *AdditionalMetadataSpec `json:"namespacesMetadata,omitempty"`
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
ServiceOptions *ServiceOptions `json:"serviceOptions,omitempty"`
// Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
StorageClasses *AllowedListSpec `json:"storageClasses,omitempty"`
// Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
IngressClasses *AllowedListSpec `json:"ingressClasses,omitempty"`
// Specifies the allowed hostnames in Ingresses for the given Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed hostnames. Optional.
IngressHostnames *AllowedListSpec `json:"ingressHostnames,omitempty"`
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
ContainerRegistries *AllowedListSpec `json:"containerRegistries,omitempty"`
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namesapces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
NetworkPolicies *NetworkPolicySpec `json:"networkPolicies,omitempty"`
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
LimitRanges *LimitRangesSpec `json:"limitRanges,omitempty"`
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
ResourceQuota *ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
AdditionalRoleBindings []AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalServiceIPs,omitempty"`
ImagePullPolicies []ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
PriorityClasses *AllowedListSpec `json:"priorityClasses,omitempty"`
//+kubebuilder:default=true
EnableNodePorts bool `json:"enableNodePorts,omitempty"`
// Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means all the IPs are allowed. Optional.
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalServiceIPs,omitempty"`
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
ImagePullPolicies []ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
PriorityClasses *AllowedListSpec `json:"priorityClasses,omitempty"`
}
//+kubebuilder:object:root=true

View File

@@ -83,6 +83,52 @@ func (in *AllowedListSpec) DeepCopy() *AllowedListSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedServices) DeepCopyInto(out *AllowedServices) {
*out = *in
if in.NodePort != nil {
in, out := &in.NodePort, &out.NodePort
*out = new(bool)
**out = **in
}
if in.ExternalName != nil {
in, out := &in.ExternalName, &out.ExternalName
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedServices.
func (in *AllowedServices) DeepCopy() *AllowedServices {
if in == nil {
return nil
}
out := new(AllowedServices)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ByKindAndName) DeepCopyInto(out *ByKindAndName) {
{
in := &in
*out = make(ByKindAndName, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ByKindAndName.
func (in ByKindAndName) DeepCopy() ByKindAndName {
if in == nil {
return nil
}
out := new(ByKindAndName)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalServiceIPsSpec) DeepCopyInto(out *ExternalServiceIPsSpec) {
*out = *in
@@ -147,6 +193,27 @@ func (in *NetworkPolicySpec) DeepCopy() *NetworkPolicySpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in OwnerListSpec) DeepCopyInto(out *OwnerListSpec) {
{
in := &in
*out = make(OwnerListSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerListSpec.
func (in OwnerListSpec) DeepCopy() OwnerListSpec {
if in == nil {
return nil
}
out := new(OwnerListSpec)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) {
*out = *in
@@ -211,6 +278,31 @@ func (in *ResourceQuotaSpec) DeepCopy() *ResourceQuotaSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceOptions) DeepCopyInto(out *ServiceOptions) {
*out = *in
if in.AdditionalMetadata != nil {
in, out := &in.AdditionalMetadata, &out.AdditionalMetadata
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.AllowedServices != nil {
in, out := &in.AllowedServices, &out.AllowedServices
*out = new(AllowedServices)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceOptions.
func (in *ServiceOptions) DeepCopy() *ServiceOptions {
if in == nil {
return nil
}
out := new(ServiceOptions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Tenant) DeepCopyInto(out *Tenant) {
*out = *in
@@ -275,7 +367,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = *in
if in.Owners != nil {
in, out := &in.Owners, &out.Owners
*out = make([]OwnerSpec, len(*in))
*out = make(OwnerListSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -290,9 +382,9 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = new(AdditionalMetadataSpec)
(*in).DeepCopyInto(*out)
}
if in.ServicesMetadata != nil {
in, out := &in.ServicesMetadata, &out.ServicesMetadata
*out = new(AdditionalMetadataSpec)
if in.ServiceOptions != nil {
in, out := &in.ServiceOptions, &out.ServiceOptions
*out = new(ServiceOptions)
(*in).DeepCopyInto(*out)
}
if in.StorageClasses != nil {

View File

@@ -3,6 +3,7 @@ kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.5.0
creationTimestamp: null
name: tenants.capsule.clastix.io
spec:
conversion:
@@ -564,7 +565,7 @@ spec:
served: true
storage: false
subresources:
status: { }
status: {}
- additionalPrinterColumns:
- description: The actual state of the Tenant
jsonPath: .status.state
@@ -603,6 +604,7 @@ spec:
description: TenantSpec defines the desired state of Tenant
properties:
additionalRoleBindings:
description: Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
items:
properties:
clusterRoleName:
@@ -635,6 +637,7 @@ spec:
type: object
type: array
containerRegistries:
description: Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
properties:
allowed:
items:
@@ -643,10 +646,8 @@ spec:
allowedRegex:
type: string
type: object
enableNodePorts:
default: true
type: boolean
externalServiceIPs:
description: Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means all the IPs are allowed. Optional.
properties:
allowed:
items:
@@ -657,6 +658,7 @@ spec:
- allowed
type: object
imagePullPolicies:
description: Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
items:
enum:
- Always
@@ -665,6 +667,7 @@ spec:
type: string
type: array
ingressClasses:
description: Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
properties:
allowed:
items:
@@ -674,6 +677,7 @@ spec:
type: string
type: object
ingressHostnames:
description: Specifies the allowed hostnames in Ingresses for the given Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed hostnames. Optional.
properties:
allowed:
items:
@@ -683,6 +687,7 @@ spec:
type: string
type: object
limitRanges:
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
properties:
items:
items:
@@ -751,10 +756,12 @@ spec:
type: array
type: object
namespaceQuota:
description: Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
format: int32
minimum: 1
type: integer
namespacesMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
properties:
additionalAnnotations:
additionalProperties:
@@ -766,6 +773,7 @@ spec:
type: object
type: object
networkPolicies:
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
properties:
items:
items:
@@ -1025,20 +1033,24 @@ spec:
nodeSelector:
additionalProperties:
type: string
description: Specifies the label to control the placement of pods on a given pool of worker nodes. All namesapces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
type: object
owners:
description: Specifies the owners of the Tenant. Mandatory.
items:
description: OwnerSpec defines tenant owner name and kind
properties:
kind:
description: Kind of tenant owner. Possible values are "User", "Group", and "ServiceAccount"
enum:
- User
- Group
- ServiceAccount
type: string
name:
description: Name of tenant owner.
type: string
proxySettings:
description: Proxy settings for tenant owner.
items:
properties:
kind:
@@ -1046,6 +1058,7 @@ spec:
- Nodes
- StorageClasses
- IngressClasses
- PriorityClasses
type: string
operations:
items:
@@ -1066,6 +1079,7 @@ spec:
type: object
type: array
priorityClasses:
description: Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
properties:
allowed:
items:
@@ -1075,6 +1089,7 @@ spec:
type: string
type: object
resourceQuotas:
description: Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
properties:
items:
items:
@@ -1123,18 +1138,36 @@ spec:
type: object
type: array
type: object
servicesMetadata:
serviceOptions:
description: Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
properties:
additionalAnnotations:
additionalProperties:
type: string
additionalMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
properties:
additionalAnnotations:
additionalProperties:
type: string
type: object
additionalLabels:
additionalProperties:
type: string
type: object
type: object
additionalLabels:
additionalProperties:
type: string
allowedServices:
description: Block or deny certain type of Services. Optional.
properties:
externalName:
default: true
description: Specifies if ExternalName service type resources are allowed for the Tenant. Default is true. Optional.
type: boolean
nodePort:
default: true
description: Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
type: boolean
type: object
type: object
storageClasses:
description: Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
properties:
allowed:
items:
@@ -1147,16 +1180,19 @@ spec:
- owners
type: object
status:
description: TenantStatus defines the observed state of Tenant
description: Returns the observed state of the Tenant
properties:
namespaces:
description: List of namespaces assigned to the Tenant.
items:
type: string
type: array
size:
description: How many namespaces are assigned to the Tenant.
type: integer
state:
default: active
description: The operational state of the Tenant. Possible values are "active", "cordoned".
enum:
- cordoned
- active
@@ -1169,7 +1205,7 @@ spec:
served: true
storage: true
subresources:
status: { }
status: {}
status:
acceptedNames:
kind: ""

View File

@@ -604,6 +604,7 @@ spec:
description: TenantSpec defines the desired state of Tenant
properties:
additionalRoleBindings:
description: Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
items:
properties:
clusterRoleName:
@@ -636,6 +637,7 @@ spec:
type: object
type: array
containerRegistries:
description: Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
properties:
allowed:
items:
@@ -644,10 +646,8 @@ spec:
allowedRegex:
type: string
type: object
enableNodePorts:
default: true
type: boolean
externalServiceIPs:
description: Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means all the IPs are allowed. Optional.
properties:
allowed:
items:
@@ -658,6 +658,7 @@ spec:
- allowed
type: object
imagePullPolicies:
description: Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
items:
enum:
- Always
@@ -666,6 +667,7 @@ spec:
type: string
type: array
ingressClasses:
description: Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
properties:
allowed:
items:
@@ -675,6 +677,7 @@ spec:
type: string
type: object
ingressHostnames:
description: Specifies the allowed hostnames in Ingresses for the given Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed hostnames. Optional.
properties:
allowed:
items:
@@ -684,6 +687,7 @@ spec:
type: string
type: object
limitRanges:
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
properties:
items:
items:
@@ -752,10 +756,12 @@ spec:
type: array
type: object
namespaceQuota:
description: Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
format: int32
minimum: 1
type: integer
namespacesMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
properties:
additionalAnnotations:
additionalProperties:
@@ -767,6 +773,7 @@ spec:
type: object
type: object
networkPolicies:
description: Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
properties:
items:
items:
@@ -1026,20 +1033,24 @@ spec:
nodeSelector:
additionalProperties:
type: string
description: Specifies the label to control the placement of pods on a given pool of worker nodes. All namesapces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
type: object
owners:
description: Specifies the owners of the Tenant. Mandatory.
items:
description: OwnerSpec defines tenant owner name and kind
properties:
kind:
description: Kind of tenant owner. Possible values are "User", "Group", and "ServiceAccount"
enum:
- User
- Group
- ServiceAccount
type: string
name:
description: Name of tenant owner.
type: string
proxySettings:
description: Proxy settings for tenant owner.
items:
properties:
kind:
@@ -1047,6 +1058,7 @@ spec:
- Nodes
- StorageClasses
- IngressClasses
- PriorityClasses
type: string
operations:
items:
@@ -1067,6 +1079,7 @@ spec:
type: object
type: array
priorityClasses:
description: Specifies the allowed IngressClasses assigned to the Tenant. Capsule assures that all Ingress resources created in the Tenant can use only one of the allowed IngressClasses. Optional.
properties:
allowed:
items:
@@ -1076,6 +1089,7 @@ spec:
type: string
type: object
resourceQuotas:
description: Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
properties:
items:
items:
@@ -1124,18 +1138,36 @@ spec:
type: object
type: array
type: object
servicesMetadata:
serviceOptions:
description: Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
properties:
additionalAnnotations:
additionalProperties:
type: string
additionalMetadata:
description: Specifies additional labels and annotations the Capsule operator places on any Service resource in the Tenant. Optional.
properties:
additionalAnnotations:
additionalProperties:
type: string
type: object
additionalLabels:
additionalProperties:
type: string
type: object
type: object
additionalLabels:
additionalProperties:
type: string
allowedServices:
description: Block or deny certain type of Services. Optional.
properties:
externalName:
default: true
description: Specifies if ExternalName service type resources are allowed for the Tenant. Default is true. Optional.
type: boolean
nodePort:
default: true
description: Specifies if NodePort service type resources are allowed for the Tenant. Default is true. Optional.
type: boolean
type: object
type: object
storageClasses:
description: Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
properties:
allowed:
items:
@@ -1148,16 +1180,19 @@ spec:
- owners
type: object
status:
description: TenantStatus defines the observed state of Tenant
description: Returns the observed state of the Tenant
properties:
namespaces:
description: List of namespaces assigned to the Tenant.
items:
type: string
type: array
size:
description: How many namespaces are assigned to the Tenant.
type: integer
state:
default: active
description: The operational state of the Tenant. Possible values are "active", "cordoned".
enum:
- cordoned
- active

1642
config/install.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,4 +7,4 @@ kind: Kustomization
images:
- name: controller
newName: quay.io/clastix/capsule
newTag: v0.1.0-rc2
newTag: v0.1.0-rc4

View File

@@ -1,7 +1,136 @@
---
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: tenant-sample
name: gas
spec:
# Add fields here
foo: bar
additionalRoleBindings:
-
clusterRoleName: tenant-sample-viewer
subjects:
-
kind: User
name: bob
containerRegistries:
allowed:
- docker.io
- quay.io
allowedRegex: ^\w+.gcr.io$
serviceOptions:
additionalMetadata:
additionalAnnotations:
capsule.clastix.io/bgp: "true"
additionalLabels:
capsule.clastix.io/pool: gas
allowedServices:
nodePort: false
externalName: false
externalServiceIPs:
allowed:
- 10.20.0.0/16
- "10.96.42.42"
imagePullPolicies:
- Always
ingressClasses:
allowed:
- default
allowedRegex: ^\w+-lb$
ingressHostnames:
allowed:
- gas.acmecorp.com
allowedRegex: ^.*acmecorp.com$
limitRanges:
items:
-
limits:
-
max:
cpu: "1"
memory: 1Gi
min:
cpu: 50m
memory: 5Mi
type: Pod
-
default:
cpu: 200m
memory: 100Mi
defaultRequest:
cpu: 100m
memory: 10Mi
max:
cpu: "1"
memory: 1Gi
min:
cpu: 50m
memory: 5Mi
type: Container
-
max:
storage: 10Gi
min:
storage: 1Gi
type: PersistentVolumeClaim
namespaceQuota: 3
namespacesMetadata:
additionalAnnotations:
capsule.clastix.io/backup: "false"
additionalLabels:
capsule.clastix.io/tenant: gas
networkPolicies:
items:
-
egress:
-
to:
-
ipBlock:
cidr: 0.0.0.0/0
except:
- 192.168.0.0/12
ingress:
-
from:
-
namespaceSelector:
matchLabels:
capsule.clastix.io/tenant: gas
-
podSelector: {}
-
ipBlock:
cidr: 192.168.0.0/12
podSelector: {}
policyTypes:
- Ingress
- Egress
nodeSelector:
kubernetes.io/os: linux
owners:
-
kind: User
name: bob
priorityClasses:
allowed:
- shared-nodes
allowedRegex: ^\w-gas$
resourceQuotas:
items:
-
hard:
limits.cpu: "8"
limits.memory: 16Gi
requests.cpu: "8"
requests.memory: 16Gi
scopes:
- NotTerminating
-
hard:
pods: "10"
-
hard:
requests.storage: 100Gi
storageClasses:
allowed:
- default
allowedRegex: ^\w+fs$

View File

@@ -2,3 +2,4 @@
resources:
- capsule_v1alpha1_capsuleconfiguration.yaml
- capsule_v1alpha1_tenant.yaml
- capsule_v1beta1_tenant.yaml

View File

@@ -53,8 +53,8 @@ func (r *abstractServiceLabelsReconciler) Reconcile(ctx context.Context, request
}
_, err = controllerutil.CreateOrUpdate(ctx, r.client, r.obj, func() (err error) {
r.obj.SetLabels(r.sync(r.obj.GetLabels(), tenant.Spec.ServicesMetadata.AdditionalLabels))
r.obj.SetAnnotations(r.sync(r.obj.GetAnnotations(), tenant.Spec.ServicesMetadata.AdditionalAnnotations))
r.obj.SetLabels(r.sync(r.obj.GetLabels(), tenant.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels))
r.obj.SetAnnotations(r.sync(r.obj.GetAnnotations(), tenant.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations))
return nil
})
@@ -78,7 +78,7 @@ func (r *abstractServiceLabelsReconciler) getTenant(ctx context.Context, namespa
return nil, err
}
if tenant.Spec.ServicesMetadata == nil {
if tenant.Spec.ServiceOptions == nil || tenant.Spec.ServiceOptions.AdditionalMetadata == nil {
return nil, NewNoServicesMetadata(namespacedName.Name)
}

View File

@@ -23,11 +23,11 @@ spec:
kind: User
```
> If you need to address specific use-case, the said annotation supports multiple values comme separated
>
> ```yaml
> capsule.clastix.io/allowed-image-pull-policy: Always,IfNotPresent
> ```
If you need to address specific use-case, the said annotation supports multiple values comma separated
```yaml
capsule.clastix.io/allowed-image-pull-policy: Always,IfNotPresent
```
# Whats next

View File

@@ -85,14 +85,14 @@ capsule-oil-0 <none> 42h
production-network-policy <none> 3m
```
an delete the namespace network-policies
And delete the namespace network policies
```
alice@caas# kubectl -n oil-production delete networkpolicy production-network-policy
```
However, the Capsule controller prevents Alice to delete the tenant network policy:
However, the Capsule controller prevents Alice from deleting the tenant network policy:
```
alice@caas# kubectl -n oil-production delete networkpolicy capsule-oil-0

View File

@@ -1,7 +1,7 @@
# Assign permissions
Alice acts as the tenant admin. Other users can operate inside the tenant with different levels of permissions and authorizations. Alice is responsible for creating additional roles and assigning these roles to other users to work in the same tenant.
One of the key design principles of the Capsule is the self-provisioning management from the tenant owner's perspective. Alice, the tenant owner, does not need to interact with Bill, the cluster admin, to complete her day-by-day duties. On the other side, Bill has not to deal with multiple requests coming from multiple tenant owners that probably will overwhelm him.
One of the key design principles of the Capsule is the self-provisioning management from the tenant owner's perspective. Alice, the tenant owner, does not need to interact with Bill, the cluster admin, to complete her day-by-day duties. On the other side, Bill does not have to deal with multiple requests coming from multiple tenant owners that probably will overwhelm him.
Capsule leaves Alice the freedom to create RBAC roles at the namespace level, or using the pre-defined cluster roles already available in Kubernetes, and assign them to other users in the tenant. Since roles and rolebindings are limited to a namespace scope, Alice can assign the roles to the other users accessing the same tenant only after the namespace is created. This gives Alice the power to administer the tenant without the intervention of the cluster admin.

View File

@@ -70,7 +70,7 @@ roleRef:
name: 'psp:privileged'
```
With the above example, Capsule is forbidding to any authenticated user in `oil-production` namespace to run privileged pods and let them to performs privilege escalation as declared by the Cluster Role `psp:privileged`.
With the above example, Capsule is forbidding any authenticated user in `oil-production` namespace to run privileged pods and to perform privilege escalation as declared by the Cluster Role `psp:privileged`.
# Whats next
See how Bill, the cluster admin, can assign to Alice the permissions to create custom resources in her tenant. [Create Custom Resources](./custom-resources.md).

View File

@@ -19,7 +19,7 @@ spec:
scopes:
- NotTerminating
- hard:
pods: "100"
pods: "10"
services: "50"
- hard:
requests.storage: 10Gi

View File

@@ -16,4 +16,12 @@ In case of a data loss, the right thing to do is to restore the cluster with **V
./velero-restore.sh --kubeconfing /path/to/your/kubeconfig restore
```
Running this command, we are going to patch the tenant's namespaces manifests that are actually `ownerReferences`-less. Once the command has finished its run, you got the cluster back.
Running this command, we are going to patch the tenant's namespaces manifests that are actually `ownerReferences`-less. Once the command has finished its run, you got the cluster back.
Additionally you can also specify a selected range of tenants to be restored:
```bash
./velero-restore.sh --tenant "gas oil" restore
```
In this way, only the tenants **gas** and **oil** will be restored.

View File

@@ -23,7 +23,7 @@ var _ = Describe("creating a Namespace with an additional Role Binding", func()
Name: "additional-role-binding",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "dale",
Kind: "User",

View File

@@ -23,7 +23,7 @@ var _ = Describe("enforcing an allowed set of Service external IPs", func() {
Name: "allowed-external-ip",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "google",
Kind: "User",

View File

@@ -23,7 +23,7 @@ var _ = Describe("enforcing a Container Registry", func() {
Name: "container-registry",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "matt",
Kind: "User",

View File

@@ -23,7 +23,7 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
Name: "tenant-assigned-custom-group",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "alice",
Kind: "User",

View File

@@ -0,0 +1,108 @@
//+build e2e
// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
var _ = Describe("creating an ExternalName service when it is disabled for Tenant", func() {
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "disable-external-service",
},
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "google",
Kind: "User",
},
},
ServiceOptions: &capsulev1beta1.ServiceOptions{
AllowedServices: &capsulev1beta1.AllowedServices{
ExternalName: pointer.BoolPtr(false),
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should fail creating a service with ExternalService type", func() {
ns := NewNamespace("disable-external-service")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
EventuallyCreation(func() error {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-ip",
Namespace: ns.GetName(),
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Ports: []corev1.ServicePort{
{
Port: 8888,
TargetPort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 8888,
},
Protocol: corev1.ProtocolTCP,
},
},
},
}
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).Should(Succeed())
EventuallyCreation(func() error {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "disable-external-service",
Namespace: ns.GetName(),
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeExternalName,
Ports: []corev1.ServicePort{
{
Port: 9999,
TargetPort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 9999,
},
Protocol: corev1.ProtocolTCP,
},
},
},
}
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Services(ns.Name).Create(context.Background(), svc, metav1.CreateOptions{})
return err
}).ShouldNot(Succeed())
})
})

View File

@@ -13,6 +13,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
@@ -23,13 +24,17 @@ var _ = Describe("creating a nodePort service when it is disabled for Tenant", f
Name: "disable-node-ports",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "google",
Kind: "User",
},
},
EnableNodePorts: false,
ServiceOptions: &capsulev1beta1.ServiceOptions{
AllowedServices: &capsulev1beta1.AllowedServices{
NodePort: pointer.BoolPtr(false),
},
},
},
}

View File

@@ -23,7 +23,7 @@ var _ = Describe("creating a nodePort service when it is enabled for Tenant", fu
Name: "enable-node-ports",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "google",
Kind: "User",

View File

@@ -23,7 +23,7 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement", fun
Name: "awesome",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
@@ -36,7 +36,7 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement", fun
Name: "awesome-tenant",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",

View File

@@ -22,7 +22,7 @@ var _ = Describe("enforcing some defined ImagePullPolicy", func() {
Name: "image-pull-policies",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "alex",
Kind: "User",

View File

@@ -22,7 +22,7 @@ var _ = Describe("enforcing a defined ImagePullPolicy", func() {
Name: "image-pull-policy",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "axel",
Kind: "User",

View File

@@ -24,7 +24,7 @@ var _ = Describe("when Tenant handles Ingress classes", func() {
Name: "ingress-class",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "ingress",
Kind: "User",

View File

@@ -25,7 +25,7 @@ var _ = Describe("when handling Ingress hostnames collision", func() {
Name: "ingress-hostnames-allowed-collision",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "ingress-allowed",
Kind: "User",

View File

@@ -25,7 +25,7 @@ var _ = Describe("when handling Ingress hostnames collision", func() {
Name: "ingress-hostnames-denied-collision",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "ingress-denied",
Kind: "User",

View File

@@ -25,7 +25,7 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
Name: "ingress-hostnames",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "hostname",
Kind: "User",

View File

@@ -19,7 +19,7 @@ var _ = Describe("creating a Namespace creation with no Tenant assigned", func()
It("should fail", func() {
tnt := &capsulev1beta1.Tenant{
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "missing",
Kind: "User",

View File

@@ -23,7 +23,7 @@ var _ = Describe("creating several Namespaces for a Tenant", func() {
Name: "capsule-labels",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "charlie",
Kind: "User",

View File

@@ -22,7 +22,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", f
Name: "tenant-metadata",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "gatsby",
Kind: "User",

View File

@@ -21,7 +21,7 @@ var _ = Describe("creating a Namespaces as different type of Tenant owners", fun
Name: "tenant-assigned",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "alice",
Kind: "User",

View File

@@ -22,7 +22,7 @@ var _ = Describe("creating a Namespace in over-quota of three", func() {
Name: "over-quota-tenant",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "bob",
Kind: "User",

View File

@@ -26,7 +26,7 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() {
Name: "tenant-owner",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "ruby",
Kind: "User",

View File

@@ -23,7 +23,7 @@ var _ = Describe("enforcing a Priority Class", func() {
Name: "priority-class",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "george",
Kind: "User",

View File

@@ -23,7 +23,7 @@ var _ = Describe("creating a Namespace with a protected Namespace regex enabled"
Name: "tenant-protected-namespace",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "alice",
Kind: "User",

View File

@@ -27,7 +27,7 @@ var _ = Describe("exceeding a Tenant resource quota", func() {
Name: "tenant-resources-changes",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "bobby",
Kind: "User",

View File

@@ -22,7 +22,7 @@ var _ = Describe("creating a Namespace trying to select a third Tenant", func()
Name: "tenant-non-owned",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "undefined",
Kind: "User",

View File

@@ -21,7 +21,7 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns
Name: "tenant-one",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
@@ -34,7 +34,7 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns
Name: "tenant-two",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
@@ -47,7 +47,7 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns
Name: "tenant-three",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "Group",
@@ -60,7 +60,7 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns
Name: "tenant-four",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "Group",

View File

@@ -21,7 +21,7 @@ var _ = Describe("creating a Namespace with Tenant selector when user owns multi
Name: "tenant-one",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",
@@ -34,7 +34,7 @@ var _ = Describe("creating a Namespace with Tenant selector when user owns multi
Name: "tenant-two",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",

View File

@@ -27,20 +27,22 @@ var _ = Describe("adding metadata to Service objects", func() {
Name: "service-metadata",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "gatsby",
Kind: "User",
},
},
ServicesMetadata: &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
"k8s.io/custom-label": "foo",
"clastix.io/custom-label": "bar",
},
AdditionalAnnotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
"clastix.io/custom-annotation": "buzz",
ServiceOptions: &capsulev1beta1.ServiceOptions{
AdditionalMetadata: &capsulev1beta1.AdditionalMetadataSpec{
AdditionalLabels: map[string]string{
"k8s.io/custom-label": "foo",
"clastix.io/custom-label": "bar",
},
AdditionalAnnotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
"clastix.io/custom-annotation": "buzz",
},
},
},
AdditionalRoleBindings: []capsulev1beta1.AdditionalRoleBindingsSpec{
@@ -98,7 +100,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional labels", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: svc.GetName(), Namespace: ns.GetName()}, svc)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalLabels {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels {
ok, _ = HaveKeyWithValue(k, v).Match(svc.Labels)
if !ok {
return false
@@ -110,7 +112,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional annotations", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: svc.GetName(), Namespace: ns.GetName()}, svc)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalAnnotations {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations {
ok, _ = HaveKeyWithValue(k, v).Match(svc.Annotations)
if !ok {
return false
@@ -153,7 +155,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional labels", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ep.GetName(), Namespace: ns.GetName()}, ep)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalLabels {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels {
ok, _ = HaveKeyWithValue(k, v).Match(ep.Labels)
if !ok {
return false
@@ -165,7 +167,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional annotations", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ep.GetName(), Namespace: ns.GetName()}, ep)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalAnnotations {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations {
ok, _ = HaveKeyWithValue(k, v).Match(ep.Annotations)
if !ok {
return false
@@ -212,7 +214,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional annotations EndpointSlice", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: eps.GetName(), Namespace: ns.GetName()}, eps)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalAnnotations {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalAnnotations {
ok, _ = HaveKeyWithValue(k, v).Match(eps.Annotations)
if !ok {
return false
@@ -224,7 +226,7 @@ var _ = Describe("adding metadata to Service objects", func() {
By("checking additional labels on EndpointSlice", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: eps.GetName(), Namespace: ns.GetName()}, eps)).Should(Succeed())
for k, v := range tnt.Spec.ServicesMetadata.AdditionalLabels {
for k, v := range tnt.Spec.ServiceOptions.AdditionalMetadata.AdditionalLabels {
ok, _ = HaveKeyWithValue(k, v).Match(eps.Labels)
if !ok {
return false

View File

@@ -24,7 +24,7 @@ var _ = Describe("when Tenant handles Storage classes", func() {
Name: "storage-class",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "storage",
Kind: "User",

View File

@@ -24,7 +24,7 @@ var _ = Describe("cordoning a Tenant", func() {
Name: "tenant-cordoning",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "jim",
Kind: "User",

View File

@@ -23,7 +23,7 @@ var _ = Describe("when a second Tenant contains an already declared allowed Ingr
Name: "allowed-collision-ingress-hostnames",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "first-user",
Kind: "User",
@@ -59,7 +59,7 @@ var _ = Describe("when a second Tenant contains an already declared allowed Ingr
Name: fmt.Sprintf("%s-%d", tnt.GetName(), i),
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "second-user",
Kind: "User",
@@ -100,7 +100,7 @@ var _ = Describe("when a second Tenant contains an already declared allowed Ingr
Name: fmt.Sprintf("%s-%d", tnt.GetName(), i),
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "second-user",
Kind: "User",

View File

@@ -22,7 +22,7 @@ var _ = Describe("when a second Tenant contains an already declared allowed Ingr
Name: "no-collision-ingress-hostnames",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "first-user",
Kind: "User",
@@ -53,7 +53,7 @@ var _ = Describe("when a second Tenant contains an already declared allowed Ingr
Name: fmt.Sprintf("%s-%d", tnt.GetName(), i),
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "second-user",
Kind: "User",

View File

@@ -21,7 +21,7 @@ var _ = Describe("creating a Tenant with wrong name", func() {
Name: "non_rfc_dns_1123",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",

View File

@@ -27,7 +27,7 @@ var _ = Describe("changing Tenant managed Kubernetes resources", func() {
Name: "tenant-resources-changes",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "laura",
Kind: "User",

View File

@@ -27,7 +27,7 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() {
Name: "tenant-resources",
},
Spec: capsulev1beta1.TenantSpec{
Owners: []capsulev1beta1.OwnerSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "john",
Kind: "User",

View File

@@ -30,6 +30,7 @@ fi
USER=$1
TENANT=$2
GROUP=$3
if [[ -z ${USER} ]]; then
echo "User has not been specified!"
@@ -41,13 +42,18 @@ if [[ -z ${TENANT} ]]; then
exit 1
fi
GROUP=capsule.clastix.io
if [[ -z ${GROUP} ]]; then
GROUP=capsule.clastix.io
fi
TMPDIR=$(mktemp -d)
echo "creating certs in TMPDIR ${TMPDIR} "
MERGED_GROUPS=$(echo "/O=$GROUP" | sed "s/,/\/O=/g")
echo "merging groups ${MERGED_GROUPS}"
openssl genrsa -out ${TMPDIR}/tls.key 2048
openssl req -new -key ${TMPDIR}/tls.key -subj "/CN=${USER}/O=${GROUP}" -out ${TMPDIR}/${USER}-${TENANT}.csr
openssl req -new -key ${TMPDIR}/tls.key -subj "/CN=${USER}${MERGED_GROUPS}" -out ${TMPDIR}/${USER}-${TENANT}.csr
# Clean any previously created CSR for the same user.
kubectl delete csr ${USER}-${TENANT} 2>/dev/null || true

View File

@@ -24,6 +24,7 @@ fi
USER=$1
TENANT=$2
GROUP=$3
if [[ -z ${USER} ]]; then
echo "User has not been specified!"
@@ -35,13 +36,19 @@ if [[ -z ${TENANT} ]]; then
exit 1
fi
GROUP=capsule.clastix.io
if [[ -z ${GROUP} ]]; then
GROUP=capsule.clastix.io
fi
TMPDIR=$(mktemp -d)
echo "creating certs in TMPDIR ${TMPDIR} "
MERGED_GROUPS=$(echo "/O=$GROUP" | sed "s/,/\/O=/g")
echo "merging groups ${MERGED_GROUPS}"
openssl genrsa -out ${USER}-${TENANT}.key 2048
openssl req -new -key ${USER}-${TENANT}.key -subj "/CN=${USER}/O=${GROUP}" -out ${TMPDIR}/${USER}-${TENANT}.csr
openssl req -new -key ${USER}-${TENANT}.key -subj "/CN=${USER}${MERGED_GROUPS}" -out ${TMPDIR}/${USER}-${TENANT}.csr
# Clean any previously created CSR for the same user.
kubectl delete csr ${USER}-${TENANT} 2>/dev/null || true

View File

@@ -10,9 +10,10 @@
# let script exit if an unsed variable is used
#set -o nounset
KUBECFGFILE="~/.kube/config"
KUBECFGFILE="$HOME/.kube/config"
KUBEOPTIONS="--kubeconfig=$KUBECFGFILE"
TMPDIR=/tmp
TENANTS=""
# Print usage to stdout.
# Arguments:
@@ -22,11 +23,13 @@ TMPDIR=/tmp
usage () {
printf "Usage: $0 [flags] commands\n"
printf "Flags:\n"
printf "\t-c, --kubeconfig\tPath to the kubeconfig file to use for CLI requests.\n"
printf "\t-c, --kubeconfig /path/to/config\tPath to the kubeconfig file to use for CLI requests.\n"
printf "\t-t, --tenant \"gas oil\"\t\tSpecify one or more tenants to be restored.\n"
printf "Commands:\n"
printf "\trestore\t\t\tPerform the restore on the cluster, patching the right object fields.\n"
printf "\n"
printf "E.g. [restore]:\t$0 -c /path/to/kubeconfig restore\n"
printf "E.g. [restore]:\t$0 -t \"oil\" restore\n"
}
# Update KUBEOPTIONS global var.
@@ -59,9 +62,15 @@ check_prerequisite () {
# Outputs:
# list of the tenants.
get_tenant_list () {
tenants=$(kubectl "$KUBEOPTIONS" get tnt \
--no-headers -o custom-columns=":.metadata.name")
echo $tenants
if [ ! -z "$TENANTS" ]; then
echo "$TENANTS"
return
else
tenants=$(kubectl "$KUBEOPTIONS" get tnt \
--no-headers -o custom-columns=":.metadata.name")
echo $tenants
fi
return
}
# Retrive namespace list.
@@ -87,12 +96,13 @@ get_namespace_list () {
cluster_backup () {
# Retrieve tenant names and uids.
owner_reference=$(kubectl "$KUBEOPTIONS" get tenants.capsule.clastix.io --no-headers \
-o custom-columns=":.metadata.name,:.metadata.uid")
-o custom-columns=":apiVersion,:.metadata.name,:.metadata.uid")
# Store information inside /tmp/tenant_$(tenant_name).
while IFS= read -r line; do
tnt=$(echo "$line" | awk '{ print $1 }')
uid=$(echo "$line" | awk '{ print $2 }')
apiVersion=$(echo "$line" | awk '{ print $1 }')
tnt=$(echo "$line" | awk '{ print $2 }')
uid=$(echo "$line" | awk '{ print $3 }')
cat <<EOF > "$TMPDIR/tenant_$tnt"
{
@@ -100,7 +110,7 @@ cluster_backup () {
"path": "/metadata/ownerReferences",
"value": [
{
"apiVersion": "capsule.clastix.io/v1alpha1",
"apiVersion": "${apiVersion}",
"blockOwnerDeletion": true,
"controller": true,
"kind": "Tenant",
@@ -168,6 +178,10 @@ while :; do
update_kube_options
shift
;;
-t|--tenant)
TENANTS="${2}"
shift
;;
*)
break
esac

View File

@@ -189,7 +189,7 @@ func (h *handler) listTenantsForOwnerKind(ctx context.Context, ownerKind string,
return tntList, err
}
func (h *handler) isTenantOwner(owners []capsulev1beta1.OwnerSpec, userInfo authenticationv1.UserInfo) bool {
func (h *handler) isTenantOwner(owners capsulev1beta1.OwnerListSpec, userInfo authenticationv1.UserInfo) bool {
for _, owner := range owners {
switch owner.Kind {
case "User", "ServiceAccount":

View File

@@ -41,3 +41,13 @@ func NewNodePortDisabledError() error {
func (nodePortDisabled) Error() string {
return "NodePort service types are forbidden for the tenant: please, reach out to the system administrators"
}
type externalNameDisabled struct{}
func NewExternalNameDisabledError() error {
return &externalNameDisabled{}
}
func (externalNameDisabled) Error() string {
return "ExternalName service types are forbidden for the tenant: please, reach out to the system administrators"
}

View File

@@ -44,7 +44,7 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder
tnt := tntList.Items[0]
if svc.Spec.Type == corev1.ServiceTypeNodePort && !tnt.Spec.EnableNodePorts {
if svc.Spec.Type == corev1.ServiceTypeNodePort && tnt.Spec.ServiceOptions != nil && tnt.Spec.ServiceOptions.AllowedServices != nil && !*tnt.Spec.ServiceOptions.AllowedServices.NodePort {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenNodePort", "Service %s/%s cannot be type of NodePort for the current Tenant", req.Namespace, req.Name)
response := admission.Denied(NewNodePortDisabledError().Error())
@@ -52,6 +52,14 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder
return &response
}
if svc.Spec.Type == corev1.ServiceTypeExternalName && tnt.Spec.ServiceOptions != nil && tnt.Spec.ServiceOptions.AllowedServices != nil && !*tnt.Spec.ServiceOptions.AllowedServices.ExternalName {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenExternalName", "Service %s/%s cannot be type of ExternalName for the current Tenant", req.Namespace, req.Name)
response := admission.Denied(NewExternalNameDisabledError().Error())
return &response
}
if svc.Spec.ExternalIPs == nil || tnt.Spec.ExternalServiceIPs == nil {
return nil
}