feat: enforcement of LoadBalancer service kind

This commit is contained in:
Dario Tranchitella
2021-08-17 17:21:59 +02:00
parent 225d671301
commit 60ab33337d
8 changed files with 242 additions and 0 deletions
+18
View File
@@ -26,6 +26,7 @@ const (
enableNodePortsAnnotation = "capsule.clastix.io/enable-node-ports"
enableExternalNameAnnotation = "capsule.clastix.io/enable-external-name"
enableLoadBalancerAnnotation = "capsule.clastix.io/enable-loadbalancer-service"
ownerGroupsAnnotation = "owners.capsule.clastix.io/group"
ownerUsersAnnotation = "owners.capsule.clastix.io/user"
@@ -297,6 +298,21 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
dst.Spec.ServiceOptions.AllowedServices.ExternalName = pointer.BoolPtr(val)
}
loadBalancerService, ok := annotations[enableLoadBalancerAnnotation]
if ok {
val, err := strconv.ParseBool(loadBalancerService)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableLoadBalancerAnnotation, 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.LoadBalancer = pointer.BoolPtr(val)
}
// Status
dst.Status = capsulev1beta1.TenantStatus{
Size: t.Status.Size,
@@ -309,6 +325,7 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error {
delete(dst.ObjectMeta.Annotations, podPriorityAllowedRegexAnnotation)
delete(dst.ObjectMeta.Annotations, enableNodePortsAnnotation)
delete(dst.ObjectMeta.Annotations, enableExternalNameAnnotation)
delete(dst.ObjectMeta.Annotations, enableLoadBalancerAnnotation)
delete(dst.ObjectMeta.Annotations, ownerGroupsAnnotation)
delete(dst.ObjectMeta.Annotations, ownerUsersAnnotation)
delete(dst.ObjectMeta.Annotations, ownerServiceAccountAnnotation)
@@ -530,6 +547,7 @@ func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error {
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)
t.Annotations[enableLoadBalancerAnnotation] = strconv.FormatBool(*src.Spec.ServiceOptions.AllowedServices.LoadBalancer)
}
// Status
+2
View File
@@ -52,6 +52,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
AllowedServices: &capsulev1beta1.AllowedServices{
NodePort: pointer.BoolPtr(false),
ExternalName: pointer.BoolPtr(false),
LoadBalancer: pointer.BoolPtr(false),
},
ExternalServiceIPs: &capsulev1beta1.ExternalServiceIPsSpec{
Allowed: []capsulev1beta1.AllowedIP{"192.168.0.1"},
@@ -285,6 +286,7 @@ func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) {
podAllowedImagePullPolicyAnnotation: "Always,IfNotPresent",
enableExternalNameAnnotation: "false",
enableNodePortsAnnotation: "false",
enableLoadBalancerAnnotation: "false",
podPriorityAllowedAnnotation: "default",
podPriorityAllowedRegexAnnotation: "^tier-.*$",
ownerGroupsAnnotation: "owner-foo,owner-bar",
+3
View File
@@ -10,4 +10,7 @@ type AllowedServices struct {
//+kubebuilder:default=true
// Specifies if ExternalName service type resources are allowed for the Tenant. Default is true. Optional.
ExternalName *bool `json:"externalName,omitempty"`
//+kubebuilder:default=true
// Specifies if LoadBalancer service type resources are allowed for the Tenant. Default is true. Optional.
LoadBalancer *bool `json:"loadBalancer,omitempty"`
}
+5
View File
@@ -96,6 +96,11 @@ func (in *AllowedServices) DeepCopyInto(out *AllowedServices) {
*out = new(bool)
**out = **in
}
if in.LoadBalancer != nil {
in, out := &in.LoadBalancer, &out.LoadBalancer
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedServices.
+112
View File
@@ -0,0 +1,112 @@
//+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 a LoadBalancer service when it is disabled for Tenant", func() {
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "disable-loadbalancer-service",
},
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "amazon",
Kind: "User",
},
},
ServiceOptions: &capsulev1beta1.ServiceOptions{
AllowedServices: &capsulev1beta1.AllowedServices{
LoadBalancer: pointer.BoolPtr(false),
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
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 LoadBalancer type", func() {
ns := NewNamespace("disable-loadbalancer-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-loadbalancer-service",
Namespace: ns.GetName(),
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
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())
})
})
+84
View File
@@ -0,0 +1,84 @@
//+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 a LoadBalancer service when it is enabled for Tenant", func() {
tnt := &capsulev1beta1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "enable-loadbalancer-service",
},
Spec: capsulev1beta1.TenantSpec{
Owners: capsulev1beta1.OwnerListSpec{
{
Name: "netflix",
Kind: "User",
},
},
ServiceOptions: &capsulev1beta1.ServiceOptions{
AllowedServices: &capsulev1beta1.AllowedServices{
LoadBalancer: pointer.BoolPtr(true),
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should succeed creating a service with LoadBalancer type", func() {
ns := NewNamespace("enable-loadbalancer-service")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
EventuallyCreation(func() error {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "enable-loadbalancer-service",
Namespace: ns.GetName(),
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
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
}).Should(Succeed())
})
})
+10
View File
@@ -51,3 +51,13 @@ func NewExternalNameDisabledError() error {
func (externalNameDisabled) Error() string {
return "ExternalName service types are forbidden for the tenant: please, reach out to the system administrators"
}
type loadBalancerDisabled struct{}
func NewLoadBalancerDisabled() error {
return &loadBalancerDisabled{}
}
func (loadBalancerDisabled) Error() string {
return "LoadBalancer service types are forbidden for the tenant: please, reach out to the system administrators"
}
+8
View File
@@ -60,6 +60,14 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder
return &response
}
if svc.Spec.Type == corev1.ServiceTypeLoadBalancer && tnt.Spec.ServiceOptions != nil && tnt.Spec.ServiceOptions.AllowedServices != nil && !*tnt.Spec.ServiceOptions.AllowedServices.LoadBalancer {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenLoadBalancer", "Service %s/%s cannot be type of LoadBalancer for the current Tenant", req.Namespace, req.Name)
response := admission.Denied(NewLoadBalancerDisabled().Error())
return &response
}
if svc.Spec.ExternalIPs == nil || (tnt.Spec.ServiceOptions == nil || tnt.Spec.ServiceOptions.ExternalServiceIPs == nil) {
return nil
}