/* Copyright 2019 The KubeSphere Authors. 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 workspacetemplate import ( "bytes" "context" "fmt" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/record" iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2" typesv1beta1 "kubesphere.io/kubesphere/pkg/apis/types/v1beta1" "kubesphere.io/kubesphere/pkg/constants" controllerutils "kubesphere.io/kubesphere/pkg/controller/utils/controller" "reflect" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) const ( controllerName = "workspacetemplate-controller" ) // Reconciler reconciles a WorkspaceRoleBinding object type Reconciler struct { client.Client Logger logr.Logger Recorder record.EventRecorder MaxConcurrentReconciles int MultiClusterEnabled bool } func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { if r.Client == nil { r.Client = mgr.GetClient() } if r.Logger == nil { r.Logger = ctrl.Log.WithName("controllers").WithName(controllerName) } if r.Recorder == nil { r.Recorder = mgr.GetEventRecorderFor(controllerName) } if r.MaxConcurrentReconciles <= 0 { r.MaxConcurrentReconciles = 1 } return ctrl.NewControllerManagedBy(mgr). Named(controllerName). WithOptions(controller.Options{ MaxConcurrentReconciles: r.MaxConcurrentReconciles, }). For(&tenantv1alpha2.WorkspaceTemplate{}). Complete(r) } // +kubebuilder:rbac:groups=iam.kubesphere.io,resources=workspacerolebindings,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=types.kubefed.io,resources=federatedworkspacerolebindings,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=tenant.kubesphere.io,resources=workspaces,verbs=get;list;watch; func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { logger := r.Logger.WithValues("workspacetemplate", req.NamespacedName) rootCtx := context.Background() workspaceTemplate := &tenantv1alpha2.WorkspaceTemplate{} if err := r.Get(rootCtx, req.NamespacedName, workspaceTemplate); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if r.MultiClusterEnabled { if err := r.multiClusterSync(rootCtx, logger, workspaceTemplate); err != nil { return ctrl.Result{}, err } } else { if err := r.singleClusterSync(rootCtx, logger, workspaceTemplate); err != nil { return ctrl.Result{}, err } } if err := r.initWorkspaceRoles(rootCtx, logger, workspaceTemplate); err != nil { return ctrl.Result{}, err } if err := r.initManagerRoleBinding(rootCtx, logger, workspaceTemplate); err != nil { return ctrl.Result{}, err } r.Recorder.Event(workspaceTemplate, corev1.EventTypeNormal, controllerutils.SuccessSynced, controllerutils.MessageResourceSynced) return ctrl.Result{}, nil } func (r *Reconciler) singleClusterSync(ctx context.Context, logger logr.Logger, workspaceTemplate *tenantv1alpha2.WorkspaceTemplate) error { workspace := &tenantv1alpha1.Workspace{} if err := r.Get(ctx, types.NamespacedName{Name: workspaceTemplate.Name}, workspace); err != nil { if errors.IsNotFound(err) { if workspace, err := newWorkspace(workspaceTemplate); err != nil { logger.Error(err, "generate workspace failed") return err } else { if err := r.Create(ctx, workspace); err != nil { logger.Error(err, "create workspace failed") return err } } } logger.Error(err, "get workspace failed") return err } if !reflect.DeepEqual(workspace.Spec, workspaceTemplate.Spec.Template.Spec) || !reflect.DeepEqual(workspace.Labels, workspaceTemplate.Spec.Template.Labels) { workspace = workspace.DeepCopy() workspace.Spec = workspaceTemplate.Spec.Template.Spec workspace.Labels = workspaceTemplate.Spec.Template.Labels if err := r.Update(ctx, workspace); err != nil { logger.Error(err, "update workspace failed") return err } } return nil } func (r *Reconciler) multiClusterSync(ctx context.Context, logger logr.Logger, workspaceTemplate *tenantv1alpha2.WorkspaceTemplate) error { if err := r.ensureNotControlledByKubefed(ctx, logger, workspaceTemplate); err != nil { return err } federatedWorkspace := &typesv1beta1.FederatedWorkspace{} if err := r.Client.Get(ctx, types.NamespacedName{Name: workspaceTemplate.Name}, federatedWorkspace); err != nil { if errors.IsNotFound(err) { if federatedWorkspace, err := newFederatedWorkspace(workspaceTemplate); err != nil { logger.Error(err, "generate federated workspace failed") return err } else { if err := r.Create(ctx, federatedWorkspace); err != nil { logger.Error(err, "create federated workspace failed") return err } } } logger.Error(err, "get federated workspace failed") return err } if !reflect.DeepEqual(federatedWorkspace.Spec, workspaceTemplate.Spec) || !reflect.DeepEqual(federatedWorkspace.Labels, workspaceTemplate.Labels) { federatedWorkspace.Spec = workspaceTemplate.Spec federatedWorkspace.Labels = workspaceTemplate.Labels if err := r.Update(ctx, federatedWorkspace); err != nil { logger.Error(err, "update federated workspace failed") return err } } return nil } func newFederatedWorkspace(template *tenantv1alpha2.WorkspaceTemplate) (*typesv1beta1.FederatedWorkspace, error) { federatedWorkspace := &typesv1beta1.FederatedWorkspace{ TypeMeta: metav1.TypeMeta{ Kind: typesv1beta1.FederatedWorkspaceRoleKind, APIVersion: typesv1beta1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: template.Name, Labels: template.Labels, }, Spec: template.Spec, } if err := controllerutil.SetControllerReference(template, federatedWorkspace, scheme.Scheme); err != nil { return nil, err } return federatedWorkspace, nil } func newWorkspace(template *tenantv1alpha2.WorkspaceTemplate) (*tenantv1alpha1.Workspace, error) { workspace := &tenantv1alpha1.Workspace{ ObjectMeta: metav1.ObjectMeta{ Name: template.Name, Labels: template.Spec.Template.Labels, }, Spec: template.Spec.Template.Spec, } if err := controllerutil.SetControllerReference(template, workspace, scheme.Scheme); err != nil { return nil, err } return workspace, nil } func (r *Reconciler) ensureNotControlledByKubefed(ctx context.Context, logger logr.Logger, workspaceTemplate *tenantv1alpha2.WorkspaceTemplate) error { if workspaceTemplate.Labels[constants.KubefedManagedLabel] != "false" { if workspaceTemplate.Labels == nil { workspaceTemplate.Labels = make(map[string]string) } workspaceTemplate = workspaceTemplate.DeepCopy() workspaceTemplate.Labels[constants.KubefedManagedLabel] = "false" logger.V(4).Info("update kubefed managed label") if err := r.Update(ctx, workspaceTemplate); err != nil { logger.Error(err, "update kubefed managed label failed") return err } } return nil } func (r *Reconciler) initWorkspaceRoles(ctx context.Context, logger logr.Logger, workspace *tenantv1alpha2.WorkspaceTemplate) error { var templates iamv1alpha2.RoleBaseList if err := r.List(ctx, &templates); err != nil { logger.Error(err, "list role base failed") return err } for _, template := range templates.Items { var expected iamv1alpha2.WorkspaceRole if err := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(template.Role.Raw), 1024).Decode(&expected); err == nil && expected.Kind == iamv1alpha2.ResourceKindWorkspaceRole { expected.Name = fmt.Sprintf("%s-%s", workspace.Name, expected.Name) if expected.Labels == nil { expected.Labels = make(map[string]string) } expected.Labels[tenantv1alpha1.WorkspaceLabel] = workspace.Name var existed iamv1alpha2.WorkspaceRole if err := r.Get(ctx, types.NamespacedName{Name: expected.Name}, &existed); err != nil { if errors.IsNotFound(err) { logger.V(4).Info("create workspace role", "workspacerole", expected.Name) if err := r.Create(ctx, &expected); err != nil { logger.Error(err, "create workspace role failed") return err } continue } else { logger.Error(err, "get workspace role failed") return err } } if !reflect.DeepEqual(expected.Labels, existed.Labels) || !reflect.DeepEqual(expected.Annotations, existed.Annotations) || !reflect.DeepEqual(expected.Rules, existed.Rules) { updated := existed.DeepCopy() updated.Labels = expected.Labels updated.Annotations = expected.Annotations updated.Rules = expected.Rules logger.V(4).Info("update workspace role", "workspacerole", updated.Name) if err := r.Update(ctx, updated); err != nil { logger.Error(err, "update workspace role failed") return err } } } else if err != nil { logger.Error(fmt.Errorf("invalid role base found"), "init workspace roles failed", "name", template.Name) } } return nil } func (r *Reconciler) initManagerRoleBinding(ctx context.Context, logger logr.Logger, workspace *tenantv1alpha2.WorkspaceTemplate) error { manager := workspace.Spec.Template.Spec.Manager if manager == "" { return nil } var user iamv1alpha2.User if err := r.Get(ctx, types.NamespacedName{Name: manager}, &user); err != nil { return client.IgnoreNotFound(err) } // skip if user has been deleted if !user.DeletionTimestamp.IsZero() { return nil } workspaceAdminRoleName := fmt.Sprintf("%s-admin", workspace.Name) managerRoleBinding := &iamv1alpha2.WorkspaceRoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: workspaceAdminRoleName, }, } if _, err := ctrl.CreateOrUpdate(ctx, r.Client, managerRoleBinding, workspaceRoleBindingChanger(managerRoleBinding, workspace.Name, manager, workspaceAdminRoleName)); err != nil { logger.Error(err, "create workspace manager role binding failed") return err } return nil } func workspaceRoleBindingChanger(workspaceRoleBinding *iamv1alpha2.WorkspaceRoleBinding, workspace, username, workspaceRoleName string) controllerutil.MutateFn { return func() error { workspaceRoleBinding.Labels = map[string]string{ tenantv1alpha1.WorkspaceLabel: workspace, iamv1alpha2.UserReferenceLabel: username, } workspaceRoleBinding.RoleRef = rbacv1.RoleRef{ APIGroup: iamv1alpha2.SchemeGroupVersion.Group, Kind: iamv1alpha2.ResourceKindWorkspaceRole, Name: workspaceRoleName, } workspaceRoleBinding.Subjects = []rbacv1.Subject{ { Name: username, Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, }, } return nil } }