diff --git a/pkg/drivers/kopiabackup/kopiabackup.go b/pkg/drivers/kopiabackup/kopiabackup.go index 66fe825c..8a55840a 100644 --- a/pkg/drivers/kopiabackup/kopiabackup.go +++ b/pkg/drivers/kopiabackup/kopiabackup.go @@ -396,7 +396,7 @@ func jobFor( } // Add security Context only if the PSA is enabled. if jobOption.PodUserId != "" || jobOption.PodGroupId != "" { - job, err = utils.AddSecurityContextToJob(job, jobOption.PodUserId, jobOption.PodGroupId) + job, err = utils.AddSecurityContextToJob(job, jobOption.PodUserId, jobOption.PodGroupId, jobOption.SourcePVCName, jobOption.SourcePVCNamespace) if err != nil { return nil, err } @@ -537,7 +537,7 @@ func buildJob(jobName string, jobOptions drivers.JobOpts) (*batchv1.Job, error) } } resourceNamespace = jobOptions.Namespace - if err := utils.SetupServiceAccount(jobName, resourceNamespace, roleFor()); err != nil { + if err := utils.SetupServiceAccount(jobName, resourceNamespace, jobOptions.SourcePVCName, roleFor()); err != nil { errMsg := fmt.Sprintf("error creating service account %s/%s: %v", resourceNamespace, jobName, err) logrus.Errorf("%s: %v", fn, errMsg) return nil, fmt.Errorf(errMsg) diff --git a/pkg/drivers/kopiarestore/kopiarestore.go b/pkg/drivers/kopiarestore/kopiarestore.go index 3544c632..265487ac 100644 --- a/pkg/drivers/kopiarestore/kopiarestore.go +++ b/pkg/drivers/kopiarestore/kopiarestore.go @@ -187,7 +187,7 @@ func jobFor( return nil, err } - if err := utils.SetupServiceAccount(jobName, jobOption.Namespace, roleFor()); err != nil { + if err := utils.SetupServiceAccount(jobName, jobOption.Namespace, jobOption.DestinationPVCName, roleFor()); err != nil { return nil, err } @@ -297,7 +297,7 @@ func jobFor( } // Add security Context only if the PSA is enabled. if jobOption.PodUserId != "" || jobOption.PodGroupId != "" { - job, err = utils.AddSecurityContextToJob(job, jobOption.PodUserId, jobOption.PodGroupId) + job, err = utils.AddSecurityContextToJob(job, jobOption.PodUserId, jobOption.PodGroupId, jobOption.DestinationPVCName, jobOption.Namespace) if err != nil { return nil, err } diff --git a/pkg/drivers/nfsbackup/nfsbackup.go b/pkg/drivers/nfsbackup/nfsbackup.go index 0fb6d767..824f4976 100644 --- a/pkg/drivers/nfsbackup/nfsbackup.go +++ b/pkg/drivers/nfsbackup/nfsbackup.go @@ -140,7 +140,7 @@ func buildJob( funct := "NfsbuildJob" // Setup service account using same role permission as stork role logrus.Infof("Inside %s function", funct) - if err := utils.SetupNFSServiceAccount(jobOptions.RestoreExportName, jobOptions.Namespace, roleFor()); err != nil { + if err := utils.SetupNFSServiceAccount(jobOptions.RestoreExportName, jobOptions.Namespace, jobOptions.SourcePVCName, roleFor()); err != nil { errMsg := fmt.Sprintf("error creating service account %s/%s: %v", jobOptions.Namespace, jobOptions.RestoreExportName, err) logrus.Errorf("%s: %v", funct, errMsg) return nil, fmt.Errorf(errMsg) @@ -300,7 +300,7 @@ func jobForBackupResource( // Not passing the groupId as we do not want to set the RunAsGroup field in the securityContext // This helps us in setting the primaryGroup ID to root for the user ID. if uid != "" { - job, err = utils.AddSecurityContextToJob(job, uid, "") + job, err = utils.AddSecurityContextToJob(job, uid, "", jobOption.SourcePVCName, jobOption.SourcePVCNamespace) if err != nil { return nil, err } diff --git a/pkg/drivers/nfscsirestore/nfscsirestore.go b/pkg/drivers/nfscsirestore/nfscsirestore.go index 60d84d62..aeb13546 100644 --- a/pkg/drivers/nfscsirestore/nfscsirestore.go +++ b/pkg/drivers/nfscsirestore/nfscsirestore.go @@ -141,7 +141,7 @@ func buildJob( logrus.Infof("Inside %s function", funct) jobName := utils.GetCsiRestoreJobName(drivers.NFSCSIRestore, jobOptions.DataExportName) - if err := utils.SetupNFSServiceAccount(jobName, jobOptions.Namespace, roleFor()); err != nil { + if err := utils.SetupNFSServiceAccount(jobName, jobOptions.Namespace, jobOptions.DestinationPVCName, roleFor()); err != nil { errMsg := fmt.Sprintf("error creating service account %s/%s: %v", jobOptions.Namespace, jobOptions.DataExportName, err) logrus.Errorf("%s: %v", funct, errMsg) return nil, fmt.Errorf(errMsg) @@ -277,7 +277,7 @@ func jobForRestoreCSISnapshot( } // Add security Context only if the PSA is enabled. if jobOption.PodUserId != "" || jobOption.PodGroupId != "" { - job, err = utils.AddSecurityContextToJob(job, jobOption.PodUserId, jobOption.PodGroupId) + job, err = utils.AddSecurityContextToJob(job, jobOption.PodUserId, jobOption.PodGroupId, jobOption.DestinationPVCName, jobOption.Namespace) if err != nil { return nil, err } diff --git a/pkg/drivers/nfsrestore/nfsrestore.go b/pkg/drivers/nfsrestore/nfsrestore.go index efc45ac5..e01f0393 100644 --- a/pkg/drivers/nfsrestore/nfsrestore.go +++ b/pkg/drivers/nfsrestore/nfsrestore.go @@ -141,7 +141,7 @@ func buildJob( funct := "NfsbuildJob" // Setup service account using same role permission as stork role logrus.Infof("Inside %s function", funct) - if err := utils.SetupNFSServiceAccount(jobOptions.RestoreExportName, jobOptions.Namespace, roleFor()); err != nil { + if err := utils.SetupNFSServiceAccount(jobOptions.RestoreExportName, jobOptions.Namespace, jobOptions.DestinationPVCName, roleFor()); err != nil { errMsg := fmt.Sprintf("error creating service account %s/%s: %v", jobOptions.Namespace, jobOptions.RestoreExportName, err) logrus.Errorf("%s: %v", funct, errMsg) return nil, fmt.Errorf(errMsg) @@ -323,7 +323,7 @@ func jobForRestoreResource( } // Not passing the groupId as we do not want to set the RunAsGroup field in the securityContext // This helps us in setting the primaryGroup ID to root for the user ID. - job, err = utils.AddSecurityContextToJob(job, utils.KdmpJobUid, "") + job, err = utils.AddSecurityContextToJob(job, utils.KdmpJobUid, "", jobOption.SourcePVCName, jobOption.SourcePVCNamespace) if err != nil { return nil, err } diff --git a/pkg/drivers/utils/common.go b/pkg/drivers/utils/common.go index 44036c01..4cf83c47 100644 --- a/pkg/drivers/utils/common.go +++ b/pkg/drivers/utils/common.go @@ -51,6 +51,8 @@ const ( // BurstKey - configmap burst key name BurstKey = "K8S_BURST" k8sMinVersionSASecretTokenNotSupport = "1.24" + SccRoleBindingNameSuffix = "-scc" + AnyUidClusterRoleName = "system:openshift:scc:anyuid" ) var ( @@ -81,7 +83,7 @@ func isServiceAccountSecretMissing() (bool, error) { } // SetupServiceAccount create a service account and bind it to a provided role. -func SetupServiceAccount(name, namespace string, role *rbacv1.Role) error { +func SetupServiceAccount(name, namespace, pvcName string, role *rbacv1.Role) error { if role != nil { role.Name, role.Namespace = name, namespace role.Annotations = map[string]string{ @@ -93,6 +95,38 @@ func SetupServiceAccount(name, namespace string, role *rbacv1.Role) error { if _, err := rbacops.Instance().CreateRoleBinding(roleBindingFor(name, namespace)); err != nil && !errors.IsAlreadyExists(err) { return fmt.Errorf("create %s/%s rolebinding: %s", namespace, name, err) } + + _, _, isOCP, err := GetOcpNsUidGid(namespace, "", "") + if err != nil { + return fmt.Errorf("failed to check if cluster is OCP: %v", err) + } + + // read the kdmp-config configmap to read a key named PROVISIONER_TO_USE_ANYUID + // If it is true only then exercise below code else return without doing anything + kdmpData, err := coreops.Instance().GetConfigMap(KdmpConfig, defaultPXNamespace) + if err != nil { + logrus.Tracef("error reading kdmp config map: %v", err) + return err + } + + provisionerName, err := GetProvisionerNameFromPvc(pvcName, namespace) + if err != nil { + return fmt.Errorf("failed to get provisioner name from pvc: %v", err) + } + + provisionersListToUseAnyUid, err := extractArrayFromConfigMap(kdmpData, provisionersToUseAnyUid) + if err != nil { + logrus.Errorf("failed to extract provisioners list from configmap: %v", err) + return err + } + if len(provisionersListToUseAnyUid) > 0 { + if isOCP && contains(provisionersListToUseAnyUid, provisionerName) { + failed, err := addRoleBindingForScc(name, namespace, AnyUidClusterRoleName) + if failed { + return err + } + } + } } var sa *corev1.ServiceAccount var err error @@ -136,6 +170,36 @@ func SetupServiceAccount(name, namespace string, role *rbacv1.Role) error { return nil } +// Check if corresponding SCC cluster role exists, then only create rolebinding for it. +// This way we will avoid creating rolebinding in non-ocp cluster. +func addRoleBindingForScc(name string, namespace string, sccClusterRoleName string) (bool, error) { + // read the kdmp-config configmap to read a key named KDMP_JOB_WITH_ANYUID + // If it is true only then exercise below code else return without doing anything + kdmpConfigMap, err := coreops.Instance().GetConfigMap(KdmpConfigmapName, KdmpConfigmapNamespace) + if err != nil { + if errors.IsNotFound(err) { + return false, nil + } + return true, fmt.Errorf("get %s/%s configmap: %s", KdmpConfigmapNamespace, KdmpConfigmapName, err) + } + + provisioners, ok := kdmpConfigMap.Data[provisionersToUseAnyUid] + if !ok || len(provisioners) == 0 { + return false, nil + } + // Check if the cluster role exists for the given SCC + if _, err := rbacops.Instance().GetClusterRole(sccClusterRoleName); err == nil { + if _, err := rbacops.Instance().CreateRoleBinding(roleBindingForScc(name, namespace, sccClusterRoleName)); err != nil && !errors.IsAlreadyExists(err) { + return true, fmt.Errorf("create %s/%s rolebinding: %s", namespace, name+SccRoleBindingNameSuffix, err) + } + } else { + if !errors.IsNotFound(err) { + return true, fmt.Errorf("get anyuid clusterrole %s failed: %s", AnyUidClusterRoleName, err) + } + } + return false, nil +} + // CleanServiceAccount removes a service account with a corresponding role and rolebinding. func CleanServiceAccount(name, namespace string) error { if err := rbacops.Instance().DeleteRole(name, namespace); err != nil && !errors.IsNotFound(err) { @@ -144,6 +208,9 @@ func CleanServiceAccount(name, namespace string) error { if err := rbacops.Instance().DeleteRoleBinding(name, namespace); err != nil && !errors.IsNotFound(err) { return fmt.Errorf("delete %s/%s rolebinding: %s", namespace, name, err) } + if err := rbacops.Instance().DeleteRoleBinding(name+SccRoleBindingNameSuffix, namespace); err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("delete %s/%s rolebinding: %s", namespace, name, err) + } if err := coreops.Instance().DeleteServiceAccount(name, namespace); err != nil && !errors.IsNotFound(err) { return fmt.Errorf("delete %s/%s serviceaccount: %s", namespace, name, err) } @@ -157,7 +224,7 @@ func CleanServiceAccount(name, namespace string) error { } // SetupNFSServiceAccount create a service account and bind it to a provided role. -func SetupNFSServiceAccount(name, namespace string, role *rbacv1.ClusterRole) error { +func SetupNFSServiceAccount(name, namespace, pvcName string, role *rbacv1.ClusterRole) error { if role != nil { role.Name, role.Namespace = name, namespace role.Annotations = map[string]string{ @@ -169,6 +236,37 @@ func SetupNFSServiceAccount(name, namespace string, role *rbacv1.ClusterRole) er if _, err := rbacops.Instance().CreateClusterRoleBinding(clusterRoleBindingFor(name, namespace)); err != nil && !errors.IsAlreadyExists(err) { return fmt.Errorf("create %s/%s cluster rolebinding: %s", namespace, name, err) } + + _, _, isOCP, err := GetOcpNsUidGid(namespace, "", "") + if err != nil { + return fmt.Errorf("failed to check if cluster is OCP: %v", err) + } + // read the kdmp-config configmap to read a key named PROVISIONERS_TO_USE_ANYUID + kdmpData, err := coreops.Instance().GetConfigMap(KdmpConfig, defaultPXNamespace) + if err != nil { + logrus.Tracef("error reading kdmp config map: %v", err) + return err + } + + provisionerName, err := GetProvisionerNameFromPvc(pvcName, namespace) + if err != nil { + return fmt.Errorf("failed to get provisioner name from pvc: %v", err) + } + + // If PROVISIONERS_TO_USE_ANYUID is set in kdmp-config, then add rolebinding for anyuid SCC + provisionersListToUseAnyUid, err := extractArrayFromConfigMap(kdmpData, provisionersToUseAnyUid) + if err != nil { + logrus.Errorf("failed to extract provisioners list from configmap: %v", err) + return err + } + if len(provisionersListToUseAnyUid) > 0 { + if isOCP && contains(provisionersListToUseAnyUid, provisionerName) { + failed, err := addRoleBindingForScc(name, namespace, AnyUidClusterRoleName) + if failed { + return err + } + } + } } var sa *corev1.ServiceAccount var err error @@ -218,6 +316,31 @@ func SetupNFSServiceAccount(name, namespace string, role *rbacv1.ClusterRole) er return nil } +// In OCP standard scc cluster role name are predefined and one pod can adhere to one SCC at a time. +func roleBindingForScc(name, namespace string, sccClusterRoleName string) *rbacv1.RoleBinding { + return &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name + SccRoleBindingNameSuffix, + Namespace: namespace, + Annotations: map[string]string{ + SkipResourceAnnotation: "true", + }, + }, + Subjects: []rbacv1.Subject{ + { + Kind: rbacv1.ServiceAccountKind, + Name: name, + Namespace: namespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + Name: sccClusterRoleName, + Kind: "ClusterRole", + APIGroup: rbacv1.GroupName, + }, + } +} + func roleBindingFor(name, namespace string) *rbacv1.RoleBinding { return &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/drivers/utils/utils.go b/pkg/drivers/utils/utils.go index 410334db..01d2b511 100644 --- a/pkg/drivers/utils/utils.go +++ b/pkg/drivers/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "encoding/json" "errors" "fmt" "os" @@ -78,7 +79,9 @@ const ( OcpGidRangeAnnotationKey = "openshift.io/sa.scc.supplemental-groups" kopiaBackupString = "kopiaexecutor backup" // if providerType in node spec has this string then it is GCP hosted cluster - GCPBasedClusterString = "gce://" + GCPBasedClusterString = "gce://" + provisionersToUseAnyUid = "PROVISIONERS_TO_USE_ANYUID" + pvcStorageProvisionerKey = "volume.kubernetes.io/storage-provisioner" ) var ( @@ -1020,7 +1023,7 @@ func GetShortUID(uid string) string { // If static uids like kdmpJobUid or kdmpJobGid is used that means // these are dummy UIDs used for backing up resources to backuplocation // which doesn't need specific UID specific permission. -func AddSecurityContextToJob(job *batchv1.Job, podUserId, podGroupId string) (*batchv1.Job, error) { +func AddSecurityContextToJob(job *batchv1.Job, podUserId, podGroupId, pvcName, pvcNamespace string) (*batchv1.Job, error) { if job == nil { return job, fmt.Errorf("recieved a nil job object to add security context") } @@ -1034,6 +1037,41 @@ func AddSecurityContextToJob(job *batchv1.Job, podUserId, podGroupId string) (*b if err != nil { return nil, err } + + // read the kdmp-config configmap to read a key named PROVISIONER_TO_USE_ANYUID + kdmpData, err := core.Instance().GetConfigMap(KdmpConfig, defaultPXNamespace) + if err != nil { + logrus.Tracef("error reading kdmp config map: %v", err) + return nil, err + } + + // If PROVISIONERS_TO_USE_ANYUID is set in kdmp-config, then add rolebinding for anyuid SCC + provisionersListToUseAnyUid, err := extractArrayFromConfigMap(kdmpData, provisionersToUseAnyUid) + if err != nil { + logrus.Errorf("failed to extract provisioners list from configmap: %v", err) + return nil, err + } + + // Get provisioner name from the pvcName, pvcNamespace + provisionerName, err := GetProvisionerNameFromPvc(pvcName, pvcNamespace) + if err != nil { + logrus.Errorf("failed to get storage class name for pvc [%s/%s]: %v", pvcNamespace, pvcName, err) + return nil, err + } + + if len(provisionersListToUseAnyUid) > 0 { + logrus.Infof("PROVISIONERS_TO_USE_ANYUID is set to use, running the job %v with anyuid SCC", job.Name) + // Add the annotation to force the pod to adopt anyuid scc in OCP + // It may not work if the pod's SA doesn't have permission to use anyuid SCC + if isOcp && contains(provisionersListToUseAnyUid, provisionerName) { + if job.Spec.Template.Annotations == nil { + job.Spec.Template.Annotations = make(map[string]string) + } + job.Spec.Template.Annotations["openshift.io/required-scc"] = "anyuid" + return job, nil + } + } + // if the namespace is OCP, then overwrite the UID and GID from the namespace annotation if isOcp { podUserId = ocpUid @@ -1178,3 +1216,39 @@ func GetAccessModeFromPvc(srcPvcName, srcPvcNameSpace string) ([]corev1.Persiste accessModes := srcPvc.Status.AccessModes return accessModes, nil } + +func GetProvisionerNameFromPvc(pvcName, pvcNamespace string) (string, error) { + pvc, err := core.Instance().GetPersistentVolumeClaim(pvcName, pvcNamespace) + if err != nil { + return "", err + } + provisionerName := pvc.Annotations[pvcStorageProvisionerKey] + logrus.Info("Deepa provisionerName: ", provisionerName) + return provisionerName, nil +} + +func extractArrayFromConfigMap(configMap *corev1.ConfigMap, key string) ([]string, error) { + // Retrieve the JSON string from the ConfigMap + jsonData, ok := configMap.Data[key] + if !ok { + return nil, fmt.Errorf("key %s not found in ConfigMap", key) + } + + // Parse the JSON string into a Go slice + var arrayData []string + if err := json.Unmarshal([]byte(jsonData), &arrayData); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %v", err) + } + + return arrayData, nil +} + +// Helper function to check if a slice contains a string +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +}