diff --git a/api/v1alpha1/conversion_hub.go b/api/v1alpha1/conversion_hub.go index 4ffc21b9..691f342c 100644 --- a/api/v1alpha1/conversion_hub.go +++ b/api/v1alpha1/conversion_hub.go @@ -4,15 +4,152 @@ package v1alpha1 import ( + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/conversion" capsulev1beta1 "github.com/clastix/capsule/api/v1beta1" ) +const ( + podAllowedImagePullPolicyAnnotation = "capsule.clastix.io/allowed-image-pull-policy" + podPriorityAllowedAnnotation = "priorityclass.capsule.clastix.io/allowed" + podPriorityAllowedRegexAnnotation = "priorityclass.capsule.clastix.io/allowed-regex" + enableNodePortsAnnotation = "capsule.clastix.io/enable-node-ports" +) + func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*capsulev1beta1.Tenant) + annotations := t.GetAnnotations() + + // ObjectMeta + dst.ObjectMeta = t.ObjectMeta + + // Spec + dst.Spec.NamespaceQuota = t.Spec.NamespaceQuota + dst.Spec.NodeSelector = t.Spec.NodeSelector + + dst.Spec.Owner = capsulev1beta1.OwnerSpec{ + Name: t.Spec.Owner.Name, + Kind: capsulev1beta1.Kind(t.Spec.Owner.Kind), + } + + if t.Spec.NamespacesMetadata != nil { + dst.Spec.NamespacesMetadata = &capsulev1beta1.AdditionalMetadataSpec{ + AdditionalLabels: t.Spec.NamespacesMetadata.AdditionalLabels, + AdditionalAnnotations: t.Spec.NamespacesMetadata.AdditionalAnnotations, + } + } + if t.Spec.ServicesMetadata != nil { + dst.Spec.ServicesMetadata = &capsulev1beta1.AdditionalMetadataSpec{ + AdditionalLabels: t.Spec.ServicesMetadata.AdditionalLabels, + AdditionalAnnotations: t.Spec.ServicesMetadata.AdditionalAnnotations, + } + } + if t.Spec.StorageClasses != nil { + dst.Spec.StorageClasses = &capsulev1beta1.AllowedListSpec{ + Exact: t.Spec.StorageClasses.Exact, + Regex: t.Spec.StorageClasses.Regex, + } + } + if t.Spec.IngressClasses != nil { + dst.Spec.IngressClasses = &capsulev1beta1.AllowedListSpec{ + Exact: t.Spec.IngressClasses.Exact, + Regex: t.Spec.IngressClasses.Regex, + } + } + if t.Spec.IngressHostnames != nil { + dst.Spec.IngressHostnames = &capsulev1beta1.AllowedListSpec{ + Exact: t.Spec.IngressHostnames.Exact, + Regex: t.Spec.IngressHostnames.Regex, + } + } + if t.Spec.ContainerRegistries != nil { + dst.Spec.ContainerRegistries = &capsulev1beta1.AllowedListSpec{ + Exact: t.Spec.ContainerRegistries.Exact, + Regex: t.Spec.ContainerRegistries.Regex, + } + } + if len(t.Spec.NetworkPolicies) > 0 { + dst.Spec.NetworkPolicies = &capsulev1beta1.NetworkPolicySpec{ + Items: t.Spec.NetworkPolicies, + } + } + if len(t.Spec.LimitRanges) > 0 { + dst.Spec.LimitRanges = &capsulev1beta1.LimitRangesSpec{ + Items: t.Spec.LimitRanges, + } + } + if len(t.Spec.ResourceQuota) > 0 { + dst.Spec.ResourceQuota = &capsulev1beta1.ResourceQuotaSpec{ + Items: t.Spec.ResourceQuota, + } + } + if len(t.Spec.AdditionalRoleBindings) > 0 { + for _, rb := range t.Spec.AdditionalRoleBindings { + dst.Spec.AdditionalRoleBindings = append(dst.Spec.AdditionalRoleBindings, capsulev1beta1.AdditionalRoleBindingsSpec{ + ClusterRoleName: rb.ClusterRoleName, + Subjects: rb.Subjects, + }) + } + } + 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: allowedIPs, + } + } + + pullPolicies, ok := annotations[podAllowedImagePullPolicyAnnotation] + if ok { + for _, policy := range strings.Split(pullPolicies, ",") { + dst.Spec.ImagePullPolicies = append(dst.Spec.ImagePullPolicies, capsulev1beta1.ImagePullPolicySpec(policy)) + } + } + + priorityClasses := capsulev1beta1.AllowedListSpec{} + + priorityClassAllowed, ok := annotations[podPriorityAllowedAnnotation] + if ok { + priorityClasses.Exact = strings.Split(priorityClassAllowed, ",") + } + priorityClassesRegexp, ok := annotations[podPriorityAllowedRegexAnnotation] + if ok { + priorityClasses.Regex = priorityClassesRegexp + } - println(dst) + if !reflect.ValueOf(priorityClasses).IsZero() { + dst.Spec.PriorityClasses = &priorityClasses + } + + enableNodePorts, ok := annotations[enableNodePortsAnnotation] + if ok { + val, err := strconv.ParseBool(enableNodePorts) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("unable to parse %s annotation on tenant %s", enableNodePortsAnnotation, t.GetName())) + } + dst.Spec.EnableNodePorts = val + } + + // Status + dst.Status = capsulev1beta1.TenantStatus{ + Size: t.Status.Size, + Namespaces: t.Status.Namespaces, + } + + // Remove unneeded annotations + delete(dst.ObjectMeta.Annotations, podAllowedImagePullPolicyAnnotation) + delete(dst.ObjectMeta.Annotations, podPriorityAllowedAnnotation) + delete(dst.ObjectMeta.Annotations, podPriorityAllowedRegexAnnotation) + delete(dst.ObjectMeta.Annotations, enableNodePortsAnnotation) return nil } @@ -20,7 +157,105 @@ func (t *Tenant) ConvertTo(dstRaw conversion.Hub) error { func (t *Tenant) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*capsulev1beta1.Tenant) - println(src) + // ObjectMeta + t.ObjectMeta = src.ObjectMeta + + // Spec + t.Spec.NamespaceQuota = src.Spec.NamespaceQuota + t.Spec.NodeSelector = src.Spec.NodeSelector + + t.Spec.Owner = OwnerSpec{ + Name: src.Spec.Owner.Name, + Kind: Kind(src.Spec.Owner.Kind), + } + + if src.Spec.NamespacesMetadata != nil { + t.Spec.NamespacesMetadata = &AdditionalMetadataSpec{ + AdditionalLabels: src.Spec.NamespacesMetadata.AdditionalLabels, + AdditionalAnnotations: src.Spec.NamespacesMetadata.AdditionalAnnotations, + } + } + if src.Spec.ServicesMetadata != nil { + t.Spec.ServicesMetadata = &AdditionalMetadataSpec{ + AdditionalLabels: src.Spec.ServicesMetadata.AdditionalLabels, + AdditionalAnnotations: src.Spec.ServicesMetadata.AdditionalAnnotations, + } + } + if src.Spec.StorageClasses != nil { + t.Spec.StorageClasses = &AllowedListSpec{ + Exact: src.Spec.StorageClasses.Exact, + Regex: src.Spec.StorageClasses.Regex, + } + } + if src.Spec.IngressClasses != nil { + t.Spec.IngressClasses = &AllowedListSpec{ + Exact: src.Spec.IngressClasses.Exact, + Regex: src.Spec.IngressClasses.Regex, + } + } + if src.Spec.IngressHostnames != nil { + t.Spec.IngressHostnames = &AllowedListSpec{ + Exact: src.Spec.IngressHostnames.Exact, + Regex: src.Spec.IngressHostnames.Regex, + } + } + if src.Spec.ContainerRegistries != nil { + t.Spec.ContainerRegistries = &AllowedListSpec{ + Exact: src.Spec.ContainerRegistries.Exact, + Regex: src.Spec.ContainerRegistries.Regex, + } + } + if src.Spec.NetworkPolicies != nil { + t.Spec.NetworkPolicies = src.Spec.NetworkPolicies.Items + } + if src.Spec.LimitRanges != nil { + t.Spec.LimitRanges = src.Spec.LimitRanges.Items + } + if src.Spec.ResourceQuota != nil { + t.Spec.ResourceQuota = src.Spec.ResourceQuota.Items + } + if len(src.Spec.AdditionalRoleBindings) > 0 { + for _, rb := range src.Spec.AdditionalRoleBindings { + t.Spec.AdditionalRoleBindings = append(t.Spec.AdditionalRoleBindings, AdditionalRoleBindingsSpec{ + ClusterRoleName: rb.ClusterRoleName, + Subjects: rb.Subjects, + }) + } + } + 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: allowedIPs, + } + } + if len(src.Spec.ImagePullPolicies) != 0 { + var pullPolicies []string + for _, policy := range src.Spec.ImagePullPolicies { + pullPolicies = append(pullPolicies, string(policy)) + } + t.Annotations[podAllowedImagePullPolicyAnnotation] = strings.Join(pullPolicies, ",") + } + + if src.Spec.PriorityClasses != nil { + if len(src.Spec.PriorityClasses.Exact) != 0 { + t.Annotations[podPriorityAllowedAnnotation] = strings.Join(src.Spec.PriorityClasses.Exact, ",") + } + if src.Spec.PriorityClasses.Regex != "" { + t.Annotations[podPriorityAllowedRegexAnnotation] = src.Spec.PriorityClasses.Regex + } + } + + t.Annotations[enableNodePortsAnnotation] = strconv.FormatBool(src.Spec.EnableNodePorts) + + // Status + t.Status = TenantStatus{ + Size: src.Status.Size, + Namespaces: src.Status.Namespaces, + } return nil } diff --git a/api/v1alpha1/conversion_hub_test.go b/api/v1alpha1/conversion_hub_test.go new file mode 100644 index 00000000..bdde038d --- /dev/null +++ b/api/v1alpha1/conversion_hub_test.go @@ -0,0 +1,239 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + capsulev1beta1 "github.com/clastix/capsule/api/v1beta1" +) + +func generateTenantsSpecs() (Tenant, capsulev1beta1.Tenant) { + var namespaceQuota int32 = 5 + var nodeSelector = map[string]string{ + "foo": "bar", + } + var v1alpha1AdditionalMetadataSpec = &AdditionalMetadataSpec{ + AdditionalLabels: map[string]string{ + "foo": "bar", + }, + AdditionalAnnotations: map[string]string{ + "foo": "bar", + }, + } + var v1alpha1AllowedListSpec = &AllowedListSpec{ + Exact: []string{"foo", "bar"}, + Regex: "^foo*", + } + var v1beta1AdditionalMetadataSpec = &capsulev1beta1.AdditionalMetadataSpec{ + AdditionalLabels: map[string]string{ + "foo": "bar", + }, + AdditionalAnnotations: map[string]string{ + "foo": "bar", + }, + } + var v1beta1AllowedListSpec = &capsulev1beta1.AllowedListSpec{ + Exact: []string{"foo", "bar"}, + Regex: "^foo*", + } + var networkPolicies = []networkingv1.NetworkPolicySpec{ + { + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "tenant-resources", + }, + }, + }, + { + PodSelector: &metav1.LabelSelector{}, + }, + { + IPBlock: &networkingv1.IPBlock{ + CIDR: "192.168.0.0/12", + }, + }, + }, + }, + }, + }, + } + var limitRanges = []corev1.LimitRangeSpec{ + { + Limits: []corev1.LimitRangeItem{ + { + Type: corev1.LimitTypePod, + Min: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("50m"), + corev1.ResourceMemory: resource.MustParse("5Mi"), + }, + Max: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + }, + } + var resourceQuotas = []corev1.ResourceQuotaSpec{ + { + Hard: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceLimitsCPU: resource.MustParse("8"), + corev1.ResourceLimitsMemory: resource.MustParse("16Gi"), + corev1.ResourceRequestsCPU: resource.MustParse("8"), + corev1.ResourceRequestsMemory: resource.MustParse("16Gi"), + }, + Scopes: []corev1.ResourceQuotaScope{ + corev1.ResourceQuotaScopeNotTerminating, + }, + }, + } + + var v1beta1Tnt = capsulev1beta1.Tenant{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "alice", + Labels: map[string]string{ + "foo": "bar", + }, + Annotations: map[string]string{ + "foo": "bar", + }, + }, + Spec: capsulev1beta1.TenantSpec{ + Owner: capsulev1beta1.OwnerSpec{ + Name: "alice", + Kind: "User", + }, + NamespaceQuota: &namespaceQuota, + NamespacesMetadata: v1beta1AdditionalMetadataSpec, + ServicesMetadata: v1beta1AdditionalMetadataSpec, + StorageClasses: v1beta1AllowedListSpec, + IngressClasses: v1beta1AllowedListSpec, + IngressHostnames: v1beta1AllowedListSpec, + ContainerRegistries: v1beta1AllowedListSpec, + NodeSelector: nodeSelector, + NetworkPolicies: &capsulev1beta1.NetworkPolicySpec{ + Items: networkPolicies, + }, + LimitRanges: &capsulev1beta1.LimitRangesSpec{ + Items: limitRanges, + }, + ResourceQuota: &capsulev1beta1.ResourceQuotaSpec{ + Items: resourceQuotas, + }, + AdditionalRoleBindings: []capsulev1beta1.AdditionalRoleBindingsSpec{ + { + ClusterRoleName: "crds-rolebinding", + Subjects: []rbacv1.Subject{ + { + Kind: "Group", + APIGroup: "rbac.authorization.k8s.io", + Name: "system:authenticated", + }, + }, + }, + }, + ExternalServiceIPs: &capsulev1beta1.ExternalServiceIPsSpec{ + Allowed: []capsulev1beta1.AllowedIP{"192.168.0.1"}, + }, + ImagePullPolicies: []capsulev1beta1.ImagePullPolicySpec{"Always", "IfNotPresent"}, + PriorityClasses: &capsulev1beta1.AllowedListSpec{ + Exact: []string{"default"}, + Regex: "^tier-.*$", + }, + EnableNodePorts: false, + }, + Status: capsulev1beta1.TenantStatus{ + Size: 1, + Namespaces: []string{"foo", "bar"}, + }, + } + + var v1alpha1Tnt = Tenant{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "alice", + Labels: map[string]string{ + "foo": "bar", + }, + Annotations: map[string]string{ + "foo": "bar", + podAllowedImagePullPolicyAnnotation: "Always,IfNotPresent", + enableNodePortsAnnotation: "false", + podPriorityAllowedAnnotation: "default", + podPriorityAllowedRegexAnnotation: "^tier-.*$", + }, + }, + Spec: TenantSpec{ + Owner: OwnerSpec{ + Name: "alice", + Kind: "User", + }, + NamespaceQuota: &namespaceQuota, + NamespacesMetadata: v1alpha1AdditionalMetadataSpec, + ServicesMetadata: v1alpha1AdditionalMetadataSpec, + StorageClasses: v1alpha1AllowedListSpec, + IngressClasses: v1alpha1AllowedListSpec, + IngressHostnames: v1alpha1AllowedListSpec, + ContainerRegistries: v1alpha1AllowedListSpec, + NodeSelector: nodeSelector, + NetworkPolicies: networkPolicies, + LimitRanges: limitRanges, + ResourceQuota: resourceQuotas, + AdditionalRoleBindings: []AdditionalRoleBindingsSpec{ + { + ClusterRoleName: "crds-rolebinding", + Subjects: []rbacv1.Subject{ + { + Kind: "Group", + APIGroup: "rbac.authorization.k8s.io", + Name: "system:authenticated", + }, + }, + }, + }, + ExternalServiceIPs: &ExternalServiceIPsSpec{ + Allowed: []AllowedIP{"192.168.0.1"}, + }, + }, + Status: TenantStatus{ + Size: 1, + Namespaces: []string{"foo", "bar"}, + }, + } + + return v1alpha1Tnt, v1beta1Tnt +} + +func TestConversionHub_ConvertTo(t *testing.T) { + var v1beta1ConvertedTnt = capsulev1beta1.Tenant{} + + v1alpha1Tnt, v1beta1tnt := generateTenantsSpecs() + err := v1alpha1Tnt.ConvertTo(&v1beta1ConvertedTnt) + if assert.NoError(t, err) { + assert.Equal(t, v1beta1ConvertedTnt, v1beta1tnt) + } +} + +func TestConversionHub_ConvertFrom(t *testing.T) { + var v1alpha1ConvertedTnt = Tenant{} + v1alpha1Tnt, v1beta1tnt := generateTenantsSpecs() + + err := v1alpha1ConvertedTnt.ConvertFrom(&v1beta1tnt) + if assert.NoError(t, err) { + assert.Equal(t, v1alpha1ConvertedTnt, v1alpha1Tnt) + } +} diff --git a/api/v1alpha1/tenant_webhook.go b/api/v1alpha1/tenant_webhook.go index d137e902..13443adb 100644 --- a/api/v1alpha1/tenant_webhook.go +++ b/api/v1alpha1/tenant_webhook.go @@ -7,12 +7,8 @@ import ( "io/ioutil" ctrl "sigs.k8s.io/controller-runtime" - logf "sigs.k8s.io/controller-runtime/pkg/log" ) -// log is for logging in this package. -var tenantlog = logf.Log.WithName("tenant-resource") - func (t *Tenant) SetupWebhookWithManager(mgr ctrl.Manager) error { certData, _ := ioutil.ReadFile("/tmp/k8s-webhook-server/serving-certs/tls.crt") if len(certData) == 0 { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e258ce42..fad8b8f3 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -10,7 +10,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" - "k8s.io/api/rbac/v1" + v1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime" ) diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 7147ed3c..59ea7e0e 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -10,7 +10,7 @@ package v1beta1 import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" - "k8s.io/api/rbac/v1" + v1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime" ) diff --git a/pkg/webhook/pod/imagepullpolicy_pullpolicy.go b/pkg/webhook/pod/imagepullpolicy_pullpolicy.go index d3b78c0e..5d666b98 100644 --- a/pkg/webhook/pod/imagepullpolicy_pullpolicy.go +++ b/pkg/webhook/pod/imagepullpolicy_pullpolicy.go @@ -38,7 +38,6 @@ func NewPullPolicy(tenant *capsulev1beta1.Tenant) PullPolicy { return nil } - var allowedPolicies []string for _, policy := range tenant.Spec.ImagePullPolicies { allowedPolicies = append(allowedPolicies, policy.String())