diff --git a/PROJECT b/PROJECT index b8616a85f..2c830f745 100644 --- a/PROJECT +++ b/PROJECT @@ -5,6 +5,7 @@ domain: clastix.io layout: - go.kubebuilder.io/v4 +multigroup: true plugins: manifests.sdk.operatorframework.io/v2: {} scorecard.sdk.operatorframework.io/v2: {} @@ -64,4 +65,13 @@ resources: kind: ResourcePoolClaim path: github.com/projectcapsule/capsule/api/v1beta2 version: v1beta2 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: clastix.io + group: capsule + kind: TenantPermission + path: github.com/projectcapsule/capsule/api/capsule/v1aplha2 + version: v1aplha2 version: "3" diff --git a/api/v1beta2/permission.go b/api/v1beta2/permission.go new file mode 100644 index 000000000..e140ce185 --- /dev/null +++ b/api/v1beta2/permission.go @@ -0,0 +1,13 @@ +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package v1beta2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type PermissionSpec struct { + // Defines roleBindings between the ClusterRole and the Subjet in the Tenants namespaces. + AllowedClusterBindings []metav1.LabelSelectorRequirement `json:"allowedClusterBindings,omitempty"` +} diff --git a/api/v1beta2/tenant_types.go b/api/v1beta2/tenant_types.go index cdff32002..5560f9891 100644 --- a/api/v1beta2/tenant_types.go +++ b/api/v1beta2/tenant_types.go @@ -67,6 +67,10 @@ type TenantSpec struct { // If unset, Tenant uses CapsuleConfiguration's forceTenantPrefix // Optional ForceTenantPrefix *bool `json:"forceTenantPrefix,omitempty"` + // Specifies RBAC permissions for the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. + // Capsule will add the subjects to the ClusterRoleBinding, and the RoleBinding will be created in each namespace, or each namespace specified in the namespace selector. + // Optional. + Permissions []PermissionSpec `json:"permissions,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1beta2/tenantpermission.go b/api/v1beta2/tenantpermission.go new file mode 100644 index 000000000..e0170532e --- /dev/null +++ b/api/v1beta2/tenantpermission.go @@ -0,0 +1,25 @@ +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package v1beta2 + +import ( + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type TenantPermissionSpec struct { + // Defines additional cluster-roles for the specific Owner. + // +kubebuilder:default={admin,capsule-namespace-deleter} + Bindings []string `json:"bindings,omitempty"` + // kubebuilder:validation:Minimum=1 + Subjects []rbacv1.Subject `json:"subjects"` + //+kubebuilder:default:=false + ActAsOwner bool `json:"actAsOwner,omitempty"` +} + +type TenantPermission struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec TenantPermissionSpec `json:"spec,omitempty"` +} diff --git a/api/v1beta2/tenantpermission_types.go b/api/v1beta2/tenantpermission_types.go new file mode 100644 index 000000000..366e12706 --- /dev/null +++ b/api/v1beta2/tenantpermission_types.go @@ -0,0 +1,51 @@ +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package v1beta2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// TenantPermissionSpec defines the desired state of TenantPermission +type TenantPermissionSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of TenantPermission. Edit tenantpermission_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// TenantPermissionStatus defines the observed state of TenantPermission +type TenantPermissionStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// TenantPermission is the Schema for the tenantpermissions API +type TenantPermission struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TenantPermissionSpec `json:"spec,omitempty"` + Status TenantPermissionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// TenantPermissionList contains a list of TenantPermission +type TenantPermissionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TenantPermission `json:"items"` +} + +func init() { + SchemeBuilder.Register(&TenantPermission{}, &TenantPermissionList{}) +} diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 2b3b9115a..5505e2d15 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -463,6 +463,28 @@ func (in *OwnerSpec) DeepCopy() *OwnerSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PermissionSpec) DeepCopyInto(out *PermissionSpec) { + *out = *in + if in.AllowedClusterBindings != nil { + in, out := &in.AllowedClusterBindings, &out.AllowedClusterBindings + *out = make([]metav1.LabelSelectorRequirement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PermissionSpec. +func (in *PermissionSpec) DeepCopy() *PermissionSpec { + if in == nil { + return nil + } + out := new(PermissionSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in ProcessedItems) DeepCopyInto(out *ProcessedItems) { { @@ -997,6 +1019,49 @@ func (in *TenantList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantPermission) DeepCopyInto(out *TenantPermission) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantPermission. +func (in *TenantPermission) DeepCopy() *TenantPermission { + if in == nil { + return nil + } + out := new(TenantPermission) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantPermissionSpec) DeepCopyInto(out *TenantPermissionSpec) { + *out = *in + if in.Bindings != nil { + in, out := &in.Bindings, &out.Bindings + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Subjects != nil { + in, out := &in.Subjects, &out.Subjects + *out = make([]v1.Subject, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantPermissionSpec. +func (in *TenantPermissionSpec) DeepCopy() *TenantPermissionSpec { + if in == nil { + return nil + } + out := new(TenantPermissionSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TenantResource) DeepCopyInto(out *TenantResource) { *out = *in @@ -1178,6 +1243,13 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) { *out = new(bool) **out = **in } + if in.Permissions != nil { + in, out := &in.Permissions, &out.Permissions + *out = make([]PermissionSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSpec. diff --git a/controllers/tenantpermission/tenantpermission_controller.go b/controllers/tenantpermission/tenantpermission_controller.go new file mode 100644 index 000000000..d511dffe2 --- /dev/null +++ b/controllers/tenantpermission/tenantpermission_controller.go @@ -0,0 +1,51 @@ +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package tenantpermission + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + apierrors "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" +) + +// TenantPermissionReconciler reconciles a TenantPermission object +type TenantPermissionReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// SetupWithManager sets up the controller with the Manager. +func (r *TenantPermissionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&capsulev1beta2.TenantPermission{}). + Complete(r) +} + +// +kubebuilder:rbac:groups=capsule.clastix.io,resources=tenantpermissions,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=capsule.clastix.io,resources=tenantpermissions/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=capsule.clastix.io,resources=tenantpermissions/finalizers,verbs=update +func (r *TenantPermissionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + tntPermission := &capsulev1beta2.TenantPermission{} + if err := r.client.Get(ctx, req.NamespacedName, tntPermission); err != nil { + if apierrors.IsNotFound(err) != nil { + log.Info("Request object not found, could have been deleted after reconcile request") + + return ctrl.Result{}, err + } + + return reconcile.Result{}, err + } + + return ctrl.Result{}, nil +} diff --git a/e2e/tenantpermission_controller_test.go b/e2e/tenantpermission_controller_test.go new file mode 100644 index 000000000..0c56316a0 --- /dev/null +++ b/e2e/tenantpermission_controller_test.go @@ -0,0 +1,71 @@ +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package capsule + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + capsulev1aplha2 "github.com/projectcapsule/capsule/api/capsule/v1aplha2" +) + +var _ = Describe("TenantPermission Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + tenantpermission := &capsulev1aplha2.TenantPermission{} + + BeforeEach(func() { + By("creating the custom resource for the Kind TenantPermission") + err := k8sClient.Get(ctx, typeNamespacedName, tenantpermission) + if err != nil && errors.IsNotFound(err) { + resource := &capsulev1aplha2.TenantPermission{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &capsulev1aplha2.TenantPermission{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance TenantPermission") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &TenantPermissionReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/go.sum b/go.sum index 2a02deec8..e1ef3ac45 100644 --- a/go.sum +++ b/go.sum @@ -278,12 +278,8 @@ k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97 k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/cluster-api v1.10.1 h1:5vsLNgQ4SkPudJ1USK532B0SIdJxRsCNKt2DZtBf+ww= -sigs.k8s.io/cluster-api v1.10.1/go.mod h1:aiPMrNPoaJc/GuJ4TCpWX8bVe11+iCJ4HI0f3c9QiJg= sigs.k8s.io/cluster-api v1.10.2 h1:xfvtNu4Fy/41grL0ryH5xSKQjpJEWdO8HiV2lPCCozQ= sigs.k8s.io/cluster-api v1.10.2/go.mod h1:/b9Un5Imprib6S7ZOcJitC2ep/5wN72b0pXpMQFfbTw= -sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= -sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M= diff --git a/hack/create-user-openshift.sh b/hack/create-user-openshift.sh old mode 100755 new mode 100644 diff --git a/hack/create-user.sh b/hack/create-user.sh old mode 100755 new mode 100644 diff --git a/hack/local-test-with-kind.sh b/hack/local-test-with-kind.sh old mode 100755 new mode 100644 diff --git a/hack/velero-restore.sh b/hack/velero-restore.sh old mode 100755 new mode 100644