Support Groups as Subject Kind for Tenant Namespace RoleBindings created by Capsule (#71)

Modified CRD to support Owner struct.

Added Tenant name validation webhook.

Rewrote owner_reference hook logic.

Updated and added new e2e tests.

Co-authored-by: Maksim Fedotov <m_fedotov@wargaming.net>
This commit is contained in:
Maxim Fedotov
2020-09-10 17:02:22 +02:00
committed by GitHub
co-authored by Maksim Fedotov
parent 10dcfeab82
commit 303fc4d69c
33 changed files with 752 additions and 84 deletions
+16 -2
View File
@@ -34,7 +34,7 @@ type AdditionalMetadata struct {
// TenantSpec defines the desired state of Tenant
type TenantSpec struct {
Owner string `json:"owner"`
Owner OwnerSpec `json:"owner"`
// +kubebuilder:validation:Optional
NamespacesMetadata AdditionalMetadata `json:"namespacesMetadata"`
// +kubebuilder:validation:Optional
@@ -51,6 +51,19 @@ type TenantSpec struct {
ResourceQuota []corev1.ResourceQuotaSpec `json:"resourceQuotas"`
}
// OwnerSpec defines tenant owner name and kind
type OwnerSpec struct {
Name string `json:"name"`
Kind Kind `json:"kind"`
}
// +kubebuilder:validation:Enum=User;Group
type Kind string
func (k Kind) String() string {
return string(k)
}
// TenantStatus defines the observed state of Tenant
type TenantStatus struct {
Size uint `json:"size"`
@@ -64,7 +77,8 @@ type TenantStatus struct {
// +kubebuilder:resource:scope=Cluster,shortName=tnt
// +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceQuota",description="The max amount of Namespaces can be created"
// +kubebuilder:printcolumn:name="Namespace count",type="integer",JSONPath=".status.size",description="The total amount of Namespaces in use"
// +kubebuilder:printcolumn:name="Owner",type="string",JSONPath=".spec.owner",description="The assigned Tenant owner"
// +kubebuilder:printcolumn:name="Owner name",type="string",JSONPath=".spec.owner.name",description="The assigned Tenant owner"
// +kubebuilder:printcolumn:name="Owner kind",type="string",JSONPath=".spec.owner.kind",description="The assigned Tenant owner kind"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// Tenant is the Schema for the tenants API
+16
View File
@@ -93,6 +93,21 @@ func (in NamespaceList) DeepCopy() NamespaceList {
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
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerSpec.
func (in *OwnerSpec) DeepCopy() *OwnerSpec {
if in == nil {
return nil
}
out := new(OwnerSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in StorageClassList) DeepCopyInto(out *StorageClassList) {
{
@@ -174,6 +189,7 @@ func (in *TenantList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = *in
out.Owner = in.Owner
in.NamespacesMetadata.DeepCopyInto(&out.NamespacesMetadata)
in.ServicesMetadata.DeepCopyInto(&out.ServicesMetadata)
if in.StorageClasses != nil {
@@ -17,9 +17,13 @@ spec:
description: The total amount of Namespaces in use
name: Namespace count
type: integer
- JSONPath: .spec.owner
- JSONPath: .spec.owner.name
description: The assigned Tenant owner
name: Owner
name: Owner name
type: string
- JSONPath: .spec.owner.kind
description: The assigned Tenant owner kind
name: Owner kind
type: string
- JSONPath: .metadata.creationTimestamp
description: Age
@@ -617,7 +621,19 @@ spec:
type: string
type: object
owner:
type: string
description: OwnerSpec defines tenant owner name and kind
properties:
kind:
enum:
- User
- Group
type: string
name:
type: string
required:
- kind
- name
type: object
resourceQuotas:
items:
description: ResourceQuotaSpec defines the desired hard limits to
+3 -1
View File
@@ -66,7 +66,9 @@ spec:
- Egress
nodeSelector:
kubernetes.io/os: linux
owner: alice
owner:
name: alice
kind: User
resourceQuotas:
-
hard:
+17
View File
@@ -125,6 +125,23 @@ webhooks:
- CREATE
resources:
- persistentvolumeclaims
- clientConfig:
caBundle: Cg==
service:
name: webhook-service
namespace: system
path: /validating-v1-tenant-name
failurePolicy: Fail
name: tenant.name.capsule.clastix.io
rules:
- apiGroups:
- capsule.clastix.io
apiVersions:
- v1alpha1
operations:
- CREATE
resources:
- tenants
- clientConfig:
caBundle: Cg==
service:
+2 -2
View File
@@ -519,8 +519,8 @@ func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) erro
l := map[string]string{tl: tenant.Name}
s := []rbacv1.Subject{
{
Kind: "User",
Name: tenant.Spec.Owner,
Kind: tenant.Spec.Owner.Kind.String(),
Name: tenant.Spec.Owner.Name,
},
}
+5 -2
View File
@@ -32,10 +32,13 @@ import (
var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-group", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-assigned-custom-group",
Name: "tenantassignedcustomgroup",
},
Spec: v1alpha1.TenantSpec{
Owner: "alice",
Owner: v1alpha1.OwnerSpec{
Name: "alice",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
+96
View File
@@ -0,0 +1,96 @@
//+build e2e
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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"
"github.com/clastix/capsule/api/v1alpha1"
)
var _ = Describe("creating a Namespace with --force-tenant-name flag", func() {
t1 := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "first",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
IngressClasses: []string{},
LimitRanges: []corev1.LimitRangeSpec{},
NamespaceQuota: 10,
NodeSelector: map[string]string{},
ResourceQuota: []corev1.ResourceQuotaSpec{},
},
}
t2 := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "second",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
IngressClasses: []string{},
LimitRanges: []corev1.LimitRangeSpec{},
NamespaceQuota: 10,
NodeSelector: map[string]string{},
ResourceQuota: []corev1.ResourceQuotaSpec{},
},
}
JustBeforeEach(func() {
t1.ResourceVersion = ""
t2.ResourceVersion = ""
Expect(k8sClient.Create(context.TODO(), t1)).Should(Succeed())
Expect(k8sClient.Create(context.TODO(), t2)).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed())
})
It("should fail", func() {
args := append(defaulManagerPodArgs, []string{"--force-tenant-prefix"}...)
ModifyCapsuleManagerPodArgs(args)
ns := NewNamespace("test")
NamespaceCreationShouldNotSucceed(ns, t1, podRecreationTimeoutInterval)
})
It("should be assigned to the second Tenant", func() {
ns := NewNamespace("second-test")
ns2 := NewNamespace("second-test2")
NamespaceCreationShouldSucceed(ns, t2, podRecreationTimeoutInterval)
NamespaceShouldBeManagedByTenant(ns, t2, podRecreationTimeoutInterval)
NamespaceCreationShouldNotSucceed(ns2, t1, podRecreationTimeoutInterval)
args := defaulManagerPodArgs
ModifyCapsuleManagerPodArgs(args)
})
})
+5 -2
View File
@@ -38,10 +38,13 @@ import (
var _ = Describe("when Tenant handles Ingress classes", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-class",
Name: "ingressclass",
},
Spec: v1alpha1.TenantSpec{
Owner: "ingress",
Owner: v1alpha1.OwnerSpec{
Name: "ingress",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
+4 -1
View File
@@ -32,7 +32,10 @@ var _ = Describe("Namespace creation with no Tenant assigned", func() {
It("should fail", func() {
tnt := &v1alpha1.Tenant{
Spec: v1alpha1.TenantSpec{
Owner: "missing",
Owner: v1alpha1.OwnerSpec{
Name: "missing",
Kind: "User",
},
},
}
ns := NewNamespace("no-namespace")
+5 -2
View File
@@ -33,10 +33,13 @@ import (
var _ = Describe("creating a Namespace for a Tenant with additional metadata", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-metadata",
Name: "tenantmetadata",
},
Spec: v1alpha1.TenantSpec{
Owner: "gatsby",
Owner: v1alpha1.OwnerSpec{
Name: "gatsby",
Kind: "User",
},
StorageClasses: []string{},
IngressClasses: []string{},
NamespacesMetadata: v1alpha1.AdditionalMetadata{
+5 -2
View File
@@ -32,10 +32,13 @@ import (
var _ = Describe("creating a Namespace as Tenant owner", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-assigned",
Name: "tenantassigned",
},
Spec: v1alpha1.TenantSpec{
Owner: "alice",
Owner: v1alpha1.OwnerSpec{
Name: "alice",
Kind: "User",
},
StorageClasses: []string{},
IngressClasses: []string{},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
+5 -2
View File
@@ -32,10 +32,13 @@ import (
var _ = Describe("creating a Namespace over-quota", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "overquota-tenant",
Name: "overquotatenant",
},
Spec: v1alpha1.TenantSpec{
Owner: "bob",
Owner: v1alpha1.OwnerSpec{
Name: "bob",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
+5 -2
View File
@@ -36,10 +36,13 @@ import (
var _ = Describe("when Tenant owner interacts with the webhooks", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-owner",
Name: "tenantowner",
},
Spec: v1alpha1.TenantSpec{
Owner: "ruby",
Owner: v1alpha1.OwnerSpec{
Name: "ruby",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{
+5 -2
View File
@@ -35,7 +35,10 @@ var _ = Describe("creating a Namespace with --protected-namespace-regex enabled"
Name: "tenantprotectednamespace",
},
Spec: v1alpha1.TenantSpec{
Owner: "alice",
Owner: v1alpha1.OwnerSpec{
Name: "alice",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
@@ -61,8 +64,8 @@ var _ = Describe("creating a Namespace with --protected-namespace-regex enabled"
NamespaceShouldBeManagedByTenant(ns, tnt, podRecreationTimeoutInterval)
})
It("should fail", func() {
ModifyCapsuleManagerPodArgs(defaulManagerPodArgs)
ns := NewNamespace("test-system")
NamespaceCreationShouldNotSucceed(ns, tnt, podRecreationTimeoutInterval)
ModifyCapsuleManagerPodArgs(defaulManagerPodArgs)
})
})
+5 -2
View File
@@ -39,10 +39,13 @@ import (
var _ = Describe("exceeding Tenant resource quota", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-resources-changes",
Name: "tenantresourceschanges",
},
Spec: v1alpha1.TenantSpec{
Owner: "bobby",
Owner: v1alpha1.OwnerSpec{
Name: "bobby",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
+6 -3
View File
@@ -32,10 +32,13 @@ import (
var _ = Describe("creating a Namespace trying to select a third Tenant", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-non-owned",
Name: "tenantnonowned",
},
Spec: v1alpha1.TenantSpec{
Owner: "undefined",
Owner: v1alpha1.OwnerSpec{
Name: "undefined",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
@@ -65,7 +68,7 @@ var _ = Describe("creating a Namespace trying to select a third Tenant", func()
})
})
cs := ownerClient(&v1alpha1.Tenant{Spec: v1alpha1.TenantSpec{Owner: "dale"}})
cs := ownerClient(&v1alpha1.Tenant{Spec: v1alpha1.TenantSpec{Owner: v1alpha1.OwnerSpec{Name: "dale", Kind: "User"}}})
_, err := cs.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
Expect(err).ShouldNot(Succeed())
})
+144
View File
@@ -0,0 +1,144 @@
//+build e2e
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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"
"github.com/clastix/capsule/api/v1alpha1"
)
var _ = Describe("creating a Namespace without a Tenant selector when user owns multiple Tenants", func() {
t1 := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenantone",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
IngressClasses: []string{},
LimitRanges: []corev1.LimitRangeSpec{},
NamespaceQuota: 10,
NodeSelector: map[string]string{},
ResourceQuota: []corev1.ResourceQuotaSpec{},
},
}
t2 := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenanttwo",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
IngressClasses: []string{},
LimitRanges: []corev1.LimitRangeSpec{},
NamespaceQuota: 10,
NodeSelector: map[string]string{},
ResourceQuota: []corev1.ResourceQuotaSpec{},
},
}
t3 := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenantthree",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "Group",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
IngressClasses: []string{},
LimitRanges: []corev1.LimitRangeSpec{},
NamespaceQuota: 10,
NodeSelector: map[string]string{},
ResourceQuota: []corev1.ResourceQuotaSpec{},
},
}
t4 := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenantfour",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "Group",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
IngressClasses: []string{},
LimitRanges: []corev1.LimitRangeSpec{},
NamespaceQuota: 10,
NodeSelector: map[string]string{},
ResourceQuota: []corev1.ResourceQuotaSpec{},
},
}
It("should fail", func() {
ns := NewNamespace("fail-ns")
By("user owns 2 tenants", func() {
Expect(k8sClient.Create(context.TODO(), t1)).Should(Succeed())
Expect(k8sClient.Create(context.TODO(), t2)).Should(Succeed())
NamespaceCreationShouldNotSucceed(ns, t1, defaultTimeoutInterval)
NamespaceCreationShouldNotSucceed(ns, t2, defaultTimeoutInterval)
Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed())
})
By("group owns 2 tenants", func() {
Expect(k8sClient.Create(context.TODO(), t3)).Should(Succeed())
Expect(k8sClient.Create(context.TODO(), t4)).Should(Succeed())
NamespaceCreationShouldNotSucceed(ns, t3, defaultTimeoutInterval)
NamespaceCreationShouldNotSucceed(ns, t4, defaultTimeoutInterval)
Expect(k8sClient.Delete(context.TODO(), t3)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t4)).Should(Succeed())
})
By("user and group owns 4 tenants", func() {
t1.ResourceVersion, t2.ResourceVersion, t3.ResourceVersion, t4.ResourceVersion = "", "", "", ""
Expect(k8sClient.Create(context.TODO(), t1)).Should(Succeed())
Expect(k8sClient.Create(context.TODO(), t2)).Should(Succeed())
Expect(k8sClient.Create(context.TODO(), t3)).Should(Succeed())
Expect(k8sClient.Create(context.TODO(), t4)).Should(Succeed())
NamespaceCreationShouldNotSucceed(ns, t1, defaultTimeoutInterval)
NamespaceCreationShouldNotSucceed(ns, t2, defaultTimeoutInterval)
NamespaceCreationShouldNotSucceed(ns, t3, defaultTimeoutInterval)
NamespaceCreationShouldNotSucceed(ns, t4, defaultTimeoutInterval)
Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t3)).Should(Succeed())
Expect(k8sClient.Delete(context.TODO(), t4)).Should(Succeed())
})
})
})
@@ -29,13 +29,16 @@ import (
"github.com/clastix/capsule/api/v1alpha1"
)
var _ = Describe("creating a Namespace with Tenant selector", func() {
var _ = Describe("creating a Namespace with Tenant selector when user owns multiple tenants", func() {
t1 := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-one",
Name: "tenantone",
},
Spec: v1alpha1.TenantSpec{
Owner: "john",
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
@@ -48,10 +51,13 @@ var _ = Describe("creating a Namespace with Tenant selector", func() {
}
t2 := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-two",
Name: "tenanttwo",
},
Spec: v1alpha1.TenantSpec{
Owner: "john",
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
+5 -2
View File
@@ -34,10 +34,13 @@ import (
var _ = Describe("creating a Service/Endpoint/EndpointSlice for a Tenant with additional metadata", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "service-metadata",
Name: "servicemetadata",
},
Spec: v1alpha1.TenantSpec{
Owner: "gatsby",
Owner: v1alpha1.OwnerSpec{
Name: "gatsby",
Kind: "User",
},
StorageClasses: []string{},
IngressClasses: []string{},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
+5 -2
View File
@@ -35,10 +35,13 @@ import (
var _ = Describe("when Tenant handles Storage classes", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "storage-class",
Name: "storageclass",
},
Spec: v1alpha1.TenantSpec{
Owner: "storage",
Owner: v1alpha1.OwnerSpec{
Name: "storage",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{
+7 -7
View File
@@ -44,10 +44,11 @@ import (
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var (
cfg *rest.Config
k8sClient client.Client
testEnv *envtest.Environment
defaulManagerPodArgs []string
cfg *rest.Config
k8sClient client.Client
testEnv *envtest.Environment
defaulManagerPodArgs []string
tenantRoleBindingNames = []string{"namespace:admin", "namespace:deleter"}
)
const (
@@ -95,7 +96,6 @@ var _ = BeforeSuite(func(done Done) {
}
}
Expect(defaulManagerPodArgs).ToNot(BeEmpty())
close(done)
}, 60)
@@ -107,8 +107,8 @@ var _ = AfterSuite(func() {
func ownerClient(tenant *capsulev1alpha.Tenant) (cs kubernetes.Interface) {
c, err := config.GetConfig()
Expect(err).ToNot(HaveOccurred())
c.Impersonate.Groups = []string{capsulev1alpha.GroupVersion.Group}
c.Impersonate.UserName = tenant.Spec.Owner
c.Impersonate.Groups = []string{capsulev1alpha.GroupVersion.Group, tenant.Spec.Owner.Name}
c.Impersonate.UserName = tenant.Spec.Owner.Name
cs, err = kubernetes.NewForConfig(c)
Expect(err).ToNot(HaveOccurred())
return
+55
View File
@@ -0,0 +1,55 @@
//+build e2e
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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"
"github.com/clastix/capsule/api/v1alpha1"
)
var _ = Describe("creating a Tenant with wrong name", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "wrong-name",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
IngressClasses: []string{},
LimitRanges: []corev1.LimitRangeSpec{},
NamespaceQuota: 10,
NodeSelector: map[string]string{},
ResourceQuota: []corev1.ResourceQuotaSpec{},
},
}
It("should fail", func() {
Expect(k8sClient.Create(context.TODO(), tnt)).ShouldNot(Succeed())
})
})
+65
View File
@@ -0,0 +1,65 @@
//+build e2e
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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"
"github.com/clastix/capsule/api/v1alpha1"
)
var _ = Describe("creating a Namespace with group Tenant owner", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenantgroupowner",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "alice",
Kind: "Group",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
IngressClasses: []string{},
LimitRanges: []corev1.LimitRangeSpec{},
NamespaceQuota: 10,
NodeSelector: map[string]string{},
ResourceQuota: []corev1.ResourceQuotaSpec{},
},
}
JustBeforeEach(func() {
tnt.ResourceVersion = ""
Expect(k8sClient.Create(context.TODO(), tnt)).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should succeed and be available in Tenant namespaces list", func() {
ns := NewNamespace("gto-namespace")
NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval)
NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval)
GroupShouldBeUsedInTenantRoleBinding(ns, tnt, defaultTimeoutInterval)
})
})
+5 -2
View File
@@ -37,10 +37,13 @@ import (
var _ = Describe("changing Tenant managed Kubernetes resources", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-resources-changes",
Name: "tenantresourceschanges",
},
Spec: v1alpha1.TenantSpec{
Owner: "laura",
Owner: v1alpha1.OwnerSpec{
Name: "laura",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
+17 -14
View File
@@ -27,7 +27,7 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/networking/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -38,10 +38,13 @@ import (
var _ = Describe("creating namespaces within a Tenant with resources", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-resources",
Name: "tenantresources",
},
Spec: v1alpha1.TenantSpec{
Owner: "john",
Owner: v1alpha1.OwnerSpec{
Name: "john",
Kind: "User",
},
NamespacesMetadata: v1alpha1.AdditionalMetadata{},
ServicesMetadata: v1alpha1.AdditionalMetadata{},
StorageClasses: []string{},
@@ -91,11 +94,11 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() {
},
},
},
NetworkPolicies: []v1.NetworkPolicySpec{
NetworkPolicies: []networkingv1.NetworkPolicySpec{
{
Ingress: []v1.NetworkPolicyIngressRule{
Ingress: []networkingv1.NetworkPolicyIngressRule{
{
From: []v1.NetworkPolicyPeer{
From: []networkingv1.NetworkPolicyPeer{
{
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -107,18 +110,18 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() {
PodSelector: &metav1.LabelSelector{},
},
{
IPBlock: &v1.IPBlock{
IPBlock: &networkingv1.IPBlock{
CIDR: "192.168.0.0/12",
},
},
},
},
},
Egress: []v1.NetworkPolicyEgressRule{
Egress: []networkingv1.NetworkPolicyEgressRule{
{
To: []v1.NetworkPolicyPeer{
To: []networkingv1.NetworkPolicyPeer{
{
IPBlock: &v1.IPBlock{
IPBlock: &networkingv1.IPBlock{
CIDR: "0.0.0.0/0",
Except: []string{
"192.168.0.0/12",
@@ -129,9 +132,9 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() {
},
},
PodSelector: metav1.LabelSelector{},
PolicyTypes: []v1.PolicyType{
v1.PolicyTypeIngress,
v1.PolicyTypeEgress,
PolicyTypes: []networkingv1.PolicyType{
networkingv1.PolicyTypeIngress,
networkingv1.PolicyTypeEgress,
},
},
},
@@ -195,7 +198,7 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() {
By("checking Network Policy resources", func() {
for i, s := range tnt.Spec.NetworkPolicies {
n := fmt.Sprintf("capsule-%s-%d", tnt.GetName(), i)
np := &v1.NetworkPolicy{}
np := &networkingv1.NetworkPolicy{}
Eventually(func() error {
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: n, Namespace: name}, np)
}, 10*time.Second, time.Second).Should(Succeed())
+11
View File
@@ -115,3 +115,14 @@ func ModifyCapsuleManagerPodArgs(args []string) {
// had to add sleep in order to manager be started
time.Sleep(defaultTimeoutInterval)
}
func GroupShouldBeUsedInTenantRoleBinding(ns *corev1.Namespace, t *v1alpha1.Tenant, timeout time.Duration) {
for _, roleBindingName := range tenantRoleBindingNames {
tenantRoleBindig := &rbacv1.RoleBinding{}
Eventually(func() string {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: roleBindingName, Namespace: ns.GetName()}, tenantRoleBindig)).Should(Succeed())
return tenantRoleBindig.Subjects[0].Kind
}, timeout, defaultPollInterval).Should(BeIdenticalTo("Group"))
}
}
+6 -1
View File
@@ -45,6 +45,7 @@ import (
"github.com/clastix/capsule/pkg/webhook/owner_reference"
"github.com/clastix/capsule/pkg/webhook/pvc"
"github.com/clastix/capsule/pkg/webhook/service_labels"
"github.com/clastix/capsule/pkg/webhook/tenant_name"
"github.com/clastix/capsule/pkg/webhook/tenant_prefix"
"github.com/clastix/capsule/pkg/webhook/utils"
"github.com/clastix/capsule/version"
@@ -91,6 +92,7 @@ func main() {
"This is useful to avoid Namespace name collision in a public CaaS environment.")
flag.StringVar(&protectedNamespaceRegexpString, "protected-namespace-regex", "", "Disallow creation of namespaces, whose name matches this regexp")
opts := zap.Options{}
opts.BindFlags(flag.CommandLine)
flag.Parse()
@@ -129,6 +131,8 @@ func main() {
_ = mgr.AddReadyzCheck("ping", healthz.Ping)
_ = mgr.AddHealthzCheck("ping", healthz.Ping)
setupLog.Info("starting with following options:", "metricsAddr", metricsAddr, "enableLeaderElection", enableLeaderElection, "forceTenantPrefix", forceTenantPrefix)
if err = (&controllers.TenantReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
@@ -144,11 +148,12 @@ func main() {
make([]webhook.Webhook, 0),
ingress.Webhook(utils.InCapsuleGroup(capsuleGroup, ingress.Handler())),
pvc.Webhook(utils.InCapsuleGroup(capsuleGroup, pvc.Handler())),
owner_reference.Webhook(utils.InCapsuleGroup(capsuleGroup, owner_reference.Handler())),
owner_reference.Webhook(utils.InCapsuleGroup(capsuleGroup, owner_reference.Handler(forceTenantPrefix))),
namespace_quota.Webhook(utils.InCapsuleGroup(capsuleGroup, namespace_quota.Handler())),
network_policies.Webhook(utils.InCapsuleGroup(capsuleGroup, network_policies.Handler())),
service_labels.Webhook(utils.InCapsuleGroup(capsuleGroup, service_labels.Handler())),
tenant_prefix.Webhook(utils.InCapsuleGroup(capsuleGroup, tenant_prefix.Handler(forceTenantPrefix, protectedNamespaceRegexp))),
tenant_name.Webhook(tenant_name.Handler()),
)
if err = webhook.Register(mgr, wl...); err != nil {
setupLog.Error(err, "unable to setup webhooks")
+3 -2
View File
@@ -21,6 +21,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/clastix/capsule/api/v1alpha1"
"github.com/clastix/capsule/pkg/utils"
)
type OwnerReference struct {
@@ -31,12 +32,12 @@ func (o OwnerReference) Object() runtime.Object {
}
func (o OwnerReference) Field() string {
return ".spec.owner"
return ".spec.owner.ownerkind"
}
func (o OwnerReference) Func() client.IndexerFunc {
return func(object runtime.Object) []string {
tenant := object.(*v1alpha1.Tenant)
return []string{tenant.Spec.Owner}
return []string{utils.GetOwnerWithKind(tenant)}
}
}
+23
View File
@@ -0,0 +1,23 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import "github.com/clastix/capsule/api/v1alpha1"
func GetOwnerWithKind(tenant *v1alpha1.Tenant) string {
return tenant.Spec.Owner.Kind.String() + ":" + tenant.Spec.Owner.Name
}
+82 -17
View File
@@ -19,18 +19,21 @@ package owner_reference
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/api/v1alpha1"
capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1"
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
authenticationv1 "k8s.io/api/authentication/v1"
)
// +kubebuilder:webhook:path=/mutate-v1-namespace-owner-reference,mutating=true,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=owner.namespace.capsule.clastix.io
@@ -56,10 +59,13 @@ func (w *webhook) GetPath() string {
}
type handler struct {
forceTenantPrefix bool
}
func Handler() capsulewebhook.Handler {
return &handler{}
func Handler(forceTenantPrefix bool) capsulewebhook.Handler {
return &handler{
forceTenantPrefix: forceTenantPrefix,
}
}
func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsulewebhook.Func {
@@ -68,12 +74,12 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul
if err := decoder.Decode(req, ns); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
ln, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
// If we already had TenantName label on NS -> assign to it
if len(ns.ObjectMeta.Labels) > 0 {
ln, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
l, ok := ns.ObjectMeta.Labels[ln]
// assigning namespace to Tenant in case of label
if ok {
@@ -83,7 +89,7 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul
return admission.Errored(http.StatusBadRequest, err)
}
// Tenant owner must adhere to user that asked for NS creation
if t.Spec.Owner != req.UserInfo.Username {
if !h.isTenantOwner(t.Spec.Owner, req.UserInfo) {
return admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
}
// Patching the response
@@ -91,16 +97,52 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul
}
}
tl := &capsulev1alpha1.TenantList{}
if err := clt.List(ctx, tl, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".spec.owner", req.UserInfo.Username),
}); err != nil {
return admission.Errored(http.StatusBadRequest, err)
// If we forceTenantPrefix -> find Tenant from NS name
if h.forceTenantPrefix {
t := &v1alpha1.Tenant{}
tenantName := strings.Split(ns.GetName(), "-")[0]
if err := clt.Get(ctx, types.NamespacedName{Name: tenantName}, t); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return h.patchResponseForOwnerRef(t, ns)
}
if len(tl.Items) > 0 {
return h.patchResponseForOwnerRef(&tl.Items[0], ns)
tenants := []*capsulev1alpha1.Tenant{}
// Find tenants belonging to user
tlu, err := h.listTenantsForOwnerKind(ctx, "User", req.UserInfo.Username, clt)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
// No groups single tenant short-circuit
if len(req.UserInfo.Groups) == 0 && len(tlu.Items) == 1 {
return h.patchResponseForOwnerRef(&tlu.Items[0], ns)
}
switch userTenants := len(tlu.Items); {
case userTenants > 1:
return admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace")
case userTenants == 1:
tenants = append(tenants, &tlu.Items[0])
}
for _, group := range req.UserInfo.Groups {
tl, err := h.listTenantsForOwnerKind(ctx, "Group", group, clt)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
for _, item := range tl.Items {
tenants = append(tenants, &item)
}
// more than one tenant found, returning error
if len(tenants) > 1 {
return admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace")
}
}
// Single tenant found for group
if len(tenants) == 1 {
return h.patchResponseForOwnerRef(tenants[0], ns)
}
return admission.Denied("You do not have any Tenant assigned: please, reach out the system administrators")
@@ -131,3 +173,26 @@ func (h *handler) patchResponseForOwnerRef(tenant *capsulev1alpha1.Tenant, ns *c
c, _ := json.Marshal(ns)
return admission.PatchResponseFromRaw(o, c)
}
func (h *handler) listTenantsForOwnerKind(ctx context.Context, ownerKind string, ownerName string, clt client.Client) (*v1alpha1.TenantList, error) {
tl := &v1alpha1.TenantList{}
f := client.MatchingFields{
".spec.owner.ownerkind": fmt.Sprintf("%s:%s", ownerKind, ownerName),
}
err := clt.List(ctx, tl, f)
return tl, err
}
func (h *handler) isTenantOwner(os v1alpha1.OwnerSpec, userInfo authenticationv1.UserInfo) bool {
if os.Kind == "User" && userInfo.Username == os.Name {
return true
}
if os.Kind == "Group" {
for _, group := range userInfo.Groups {
if group == os.Name {
return true
}
}
}
return false
}
+85
View File
@@ -0,0 +1,85 @@
/*
Copyright 2020 Clastix Labs.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tenant_name
import (
"context"
"net/http"
"regexp"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/clastix/capsule/api/v1alpha1"
capsulewebhook "github.com/clastix/capsule/pkg/webhook"
)
// +kubebuilder:webhook:path=/validating-v1-tenant-name,mutating=false,failurePolicy=fail,groups="capsule.clastix.io",resources=tenants,verbs=create,versions=v1alpha1,name=tenant.name.capsule.clastix.io
type webhook struct {
handler capsulewebhook.Handler
}
func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook {
return &webhook{handler: handler}
}
func (w webhook) GetName() string {
return "TenantName"
}
func (w webhook) GetPath() string {
return "/validating-v1-tenant-name"
}
func (w webhook) GetHandler() capsulewebhook.Handler {
return w.handler
}
type handler struct {
}
func Handler() capsulewebhook.Handler {
return &handler{}
}
func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) admission.Response {
tnt := &v1alpha1.Tenant{}
if err := decoder.Decode(req, tnt); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
matched, _ := regexp.MatchString(`^[a-z0-9]([a-z0-9]*[a-z0-9])?$`, tnt.GetName())
if !matched {
return admission.Denied("Tenant name has forbidden characters")
}
return admission.Allowed("")
}
}
func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) admission.Response {
return admission.Allowed("")
}
}
func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) admission.Response {
return admission.Allowed("")
}
}
+9 -4
View File
@@ -58,7 +58,9 @@ metadata:
annotations:
name: oil
spec:
owner: alice
owner:
name: alice
kind: User
nodeSelector:
vpc: oil
ingressClasses:
@@ -127,13 +129,16 @@ spec:
- 192.168.0.0/16
```
> N.B.: Tenant name can only consist of alphanumeric characters. No other symbols are allowed.
Bill checks the new tenant is created and operational:
```
bill@caas# kubectl get tenants
NAME NAMESPACE QUOTA NAMESPACE COUNT OWNER AGE
oil 3 0 alice 3m
foo 10 9 bar 30d
NAME NAMESPACE QUOTA NAMESPACE COUNT OWNER NAME OWNER KIND AGE
oil 3 0 alice User 3m
foo 10 9 bar User 30d
```
> Note that namespaces are not yet assigned to the new tenant.