From c52a2a556ed67be1baf1d835285656d5b9d8dd69 Mon Sep 17 00:00:00 2001 From: Romy <35330373+romayalon@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:36:36 +0300 Subject: [PATCH] bucket access class impl Signed-off-by: Romy <35330373+romayalon@users.noreply.github.com> --- doc/cosi-provisioner.md | 19 +- pkg/apis/noobaa/v1alpha1/cosi_types.go | 24 +- pkg/cosi/cosi.go | 4 +- pkg/cosi/cosi_access_class.go | 166 ++++++++++++ pkg/cosi/cosi_bucket_access.go | 338 +++++++++++++++++++++++++ pkg/cosi/cosi_bucket_claim.go | 2 +- pkg/cosi/cosi_bucket_class.go | 3 +- pkg/cosi/cosi_cli_test.go | 175 ++++++++++++- 8 files changed, 719 insertions(+), 12 deletions(-) create mode 100644 pkg/cosi/cosi_access_class.go create mode 100644 pkg/cosi/cosi_bucket_access.go diff --git a/doc/cosi-provisioner.md b/doc/cosi-provisioner.md index 2f40d50f0..a1678e9e8 100644 --- a/doc/cosi-provisioner.md +++ b/doc/cosi-provisioner.md @@ -104,8 +104,9 @@ parameters: ``` The equivalent noobaa cli command - +```shell noobaa cosi bucketclass create placement-bucketclass my-cosi-bucket-class --backingstores noobaa-default-backing-store - +```   Example of namespace bucketclass: @@ -122,7 +123,10 @@ parameters: ``` The equivalent noobaa cli command - +```shell noobaa cosi bucketclass create namespace-bucketclass single my-cosi-ns-bucket-class --resource nsr-name +``` +   Example of namespace bucketclass with a replication policy: @@ -144,6 +148,8 @@ The equivalent noobaa cli command - ```shell noobaa cosi bucketclass create placement-bucketclass my-cosi-ns-bucket-class --backingstores noobaa-default-backing-store --replication-policy=/path/to/json-file.json +``` + ## COSI Bucket Claim @@ -168,7 +174,10 @@ spec: ``` The equivalent noobaa cli command - +```shell noobaa cosi bucketclaim create my-cosi-bucket-claim --bucketclass my-cosi-bucket-class +``` + ## COSI BucketAccessClass @@ -185,6 +194,10 @@ driverName: noobaa.objectstorage.k8s.io authenticationType: KEY ``` +The equivalent noobaa cli command - +```shell +noobaa cosi accessclass create my-cosi-bucket-access-class +``` # COSI BucketAccess claim An administrator of a noobaa deployment can create BucketAccess claim that refers to a BucketAccessClass in order to get credentials that will provide access to a COSI bucket claim. NooBaa will generate an account and will return credentials as the bucket access claim response, then a Secret (named by credentialsSecretName property) containing the bucket info will be created. The properties bucketClaimName, bucketAccessClassName and credentialsSecretName are all required values. @@ -204,6 +217,10 @@ spec: credentialsSecretName: my-cosi-bucket-creds ``` +The equivalent noobaa cli command - +```shell +noobaa cosi accessclaim create my-cosi-bucket-access --bucket-claim=my-cosi-bucket-claim --bucket-access-class=my-cosi-bucket-access-class --creds-secret-name=my-cosi-bucket-creds +``` # Using the COSI bucket claim Once the COSI bucket claim is provisioned by the operator, a bucket will be created in NooBaa, a Secret bucket info will be created. For the example above, the Secret will be named `my-cosi-bucket-creds`. diff --git a/pkg/apis/noobaa/v1alpha1/cosi_types.go b/pkg/apis/noobaa/v1alpha1/cosi_types.go index ee2f7cad4..a01630b70 100644 --- a/pkg/apis/noobaa/v1alpha1/cosi_types.go +++ b/pkg/apis/noobaa/v1alpha1/cosi_types.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + cosiapis "sigs.k8s.io/container-object-storage-interface-api/apis" cosiapi "sigs.k8s.io/container-object-storage-interface-api/apis/objectstorage" cosiv1 "sigs.k8s.io/container-object-storage-interface-api/apis/objectstorage/v1alpha1" ) @@ -41,5 +42,26 @@ const COSIS3Protocol = cosiv1.ProtocolS3 // COSIDeletionPolicyRetain is a constant represents a retain deletion policy const COSIDeletionPolicyRetain = cosiv1.DeletionPolicyRetain -// COSIDeletionPolicyDelete is a constant represents a delete deletion policy +// COSIDeletionPolicyDelete is a constant represents a delete deletion policy const COSIDeletionPolicyDelete = cosiv1.DeletionPolicyDelete + +// COSIBucketAccessClass is the API type for submitting access classes +type COSIBucketAccessClass = cosiv1.BucketAccessClass + +// COSIBucketAccessClassList is a list of COSIAccessClass +type COSIBucketAccessClassList = cosiv1.BucketAccessClassList + +// COSIAuthenticationType is the API type represents bucket access class authentication type +type COSIAuthenticationType = cosiv1.AuthenticationType + +// COSIKEYAuthenticationType is a constant represents a KEY authentication type (secret tokens based authentication) +const COSIKEYAuthenticationType = cosiv1.AuthenticationTypeKey + +// COSIBucketAccessClaim is the API type for submitting bucket access claims +type COSIBucketAccessClaim = cosiv1.BucketAccess + +// COSIBucketAccessClaimList is a list of COSIBucketAccessClaim +type COSIBucketAccessClaimList = cosiv1.BucketAccessList + +// COSIBucketInfo is the API type represents bucket info +type COSIBucketInfo = cosiapis.BucketInfo diff --git a/pkg/cosi/cosi.go b/pkg/cosi/cosi.go index 11d34a4ab..e4e2cf548 100644 --- a/pkg/cosi/cosi.go +++ b/pkg/cosi/cosi.go @@ -11,8 +11,10 @@ func Cmd() *cobra.Command { Short: "Manage cosi resources", } cmd.AddCommand( - CmdCOSIBucketClaim(), CmdCOSIBucketClass(), + CmdCOSIBucketClaim(), + CmdCOSIBucketAccessClass(), + CmdCOSIBucketAccessClaim(), ) return cmd } diff --git a/pkg/cosi/cosi_access_class.go b/pkg/cosi/cosi_access_class.go new file mode 100644 index 000000000..bdfb46fb2 --- /dev/null +++ b/pkg/cosi/cosi_access_class.go @@ -0,0 +1,166 @@ +package cosi + +import ( + "fmt" + "time" + + "github.com/noobaa/noobaa-operator/v5/pkg/bundle" + "github.com/noobaa/noobaa-operator/v5/pkg/options" + "github.com/noobaa/noobaa-operator/v5/pkg/util" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + nbv1 "github.com/noobaa/noobaa-operator/v5/pkg/apis/noobaa/v1alpha1" +) + +// CmdCOSIBucketAccessClass returns a CLI command +func CmdCOSIBucketAccessClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "accessclass", + Short: "Manage cosi access class", + } + cmd.AddCommand( + CmdCreateAccessClass(), + CmdDeleteAccessClass(), + CmdStatusAccessClass(), + CmdListAccessClass(), + ) + return cmd +} + +// CmdCreateAccessClass returns a CLI command +func CmdCreateAccessClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a COSI access class", + Run: RunCreateAccessClass, + } + // AuthenticationType - valid types are KEY / IAM - currently the only supported type is KEY + // Parameters - currently no extra parameters are supported + return cmd +} + +// CmdDeleteAccessClass returns a CLI command +func CmdDeleteAccessClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a COSI access class", + Run: RunDeleteAccessClass, + } + + return cmd +} + +// CmdStatusAccessClass returns a CLI command +func CmdStatusAccessClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "status ", + Short: "Status of a COSI access class", + Run: RunStatusAccessClass, + } + return cmd +} + +// CmdListAccessClass returns a CLI command +func CmdListAccessClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List COSI access classes", + Run: RunListAccessClass, + Args: cobra.NoArgs, + } + return cmd +} + +// RunCreateAccessClass runs a CLI command +func RunCreateAccessClass(cmd *cobra.Command, args []string) { + log := util.Logger() + + if len(args) != 1 || args[0] == "" { + log.Fatalf(`Missing expected arguments: %s`, cmd.UsageString()) + } + name := args[0] + + cosiAccessClass := util.KubeObject(bundle.File_deploy_cosi_bucket_access_class_yaml).(*nbv1.COSIBucketAccessClass) + cosiAccessClass.Name = name + cosiAccessClass.DriverName = options.COSIDriverName() + cosiAccessClass.AuthenticationType = nbv1.COSIKEYAuthenticationType + + if !util.KubeCreateFailExisting(cosiAccessClass) { + log.Fatalf(`❌ Could not create COSI access class %q (conflict)`, cosiAccessClass.Name) + } + + log.Printf("") + log.Printf("") + log.Printf("") + RunStatusAccessClass(cmd, args) +} + +// RunDeleteAccessClass runs a CLI command +func RunDeleteAccessClass(cmd *cobra.Command, args []string) { + log := util.Logger() + + if len(args) != 1 || args[0] == "" { + log.Fatalf(`Missing expected arguments: %s`, cmd.UsageString()) + } + + cosiAccessClass := util.KubeObject(bundle.File_deploy_cosi_bucket_access_class_yaml).(*nbv1.COSIBucketAccessClass) + cosiAccessClass.Name = args[0] + + if !util.KubeDelete(cosiAccessClass) { + log.Fatalf(`❌ Could not delete COSI access class %q `, cosiAccessClass.Name) + } +} + +// RunStatusAccessClass runs a CLI command +func RunStatusAccessClass(cmd *cobra.Command, args []string) { + log := util.Logger() + + if len(args) != 1 || args[0] == "" { + log.Fatalf(`Missing expected arguments: %s`, cmd.UsageString()) + } + + cosiAccessClass := util.KubeObject(bundle.File_deploy_cosi_bucket_access_class_yaml).(*nbv1.COSIBucketAccessClass) + cosiAccessClass.Name = args[0] + + if !util.KubeCheck(cosiAccessClass) { + log.Fatalf(`❌ Could not find COSI access class %q`, cosiAccessClass.Name) + } + + fmt.Println() + fmt.Println("# AccessClass spec:") + fmt.Printf("Name:\n %s\n", cosiAccessClass.Name) + fmt.Printf("Driver Name:\n %s\n", cosiAccessClass.DriverName) + fmt.Printf("Authentication Type:\n %+v", cosiAccessClass.AuthenticationType) + fmt.Println() +} + +// RunListAccessClass runs a CLI command +func RunListAccessClass(cmd *cobra.Command, args []string) { + list := &nbv1.COSIBucketAccessClassList{ + TypeMeta: metav1.TypeMeta{Kind: "AccessClass"}, + } + if !util.KubeList(list) { + return + } + if len(list.Items) == 0 { + fmt.Printf("No COSI access classes found.\n") + return + } + table := (&util.PrintTable{}).AddRow( + "NAME", + "DRIVER-NAME", + "AUTHENTICATION-TYPE", + "AGE", + ) + for i := range list.Items { + cosiAccessClass := &list.Items[i] + table.AddRow( + cosiAccessClass.Name, + cosiAccessClass.DriverName, + string(cosiAccessClass.AuthenticationType), + util.HumanizeDuration(time.Since(cosiAccessClass.CreationTimestamp.Time).Round(time.Second)), + ) + } + fmt.Print(table.String()) +} diff --git a/pkg/cosi/cosi_bucket_access.go b/pkg/cosi/cosi_bucket_access.go new file mode 100644 index 000000000..399eac091 --- /dev/null +++ b/pkg/cosi/cosi_bucket_access.go @@ -0,0 +1,338 @@ +package cosi + +import ( + "encoding/json" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + + "github.com/noobaa/noobaa-operator/v5/pkg/bundle" + "github.com/noobaa/noobaa-operator/v5/pkg/nb" + "github.com/noobaa/noobaa-operator/v5/pkg/options" + "github.com/noobaa/noobaa-operator/v5/pkg/system" + "github.com/noobaa/noobaa-operator/v5/pkg/util" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + nbv1 "github.com/noobaa/noobaa-operator/v5/pkg/apis/noobaa/v1alpha1" + "k8s.io/apimachinery/pkg/util/wait" +) + +// CmdCOSIBucketAccessClaim returns a CLI command +func CmdCOSIBucketAccessClaim() *cobra.Command { + cmd := &cobra.Command{ + Use: "accessclaim", + Short: "Manage cosi access claims", + } + cmd.AddCommand( + CmdCreateBucketAccessClaim(), + CmdDeleteBucketAccessClaim(), + CmdStatusBucketAccessClaim(), + CmdListBucketAccessClaim(), + ) + return cmd +} + +// CmdCreateBucketAccessClaim returns a CLI command +func CmdCreateBucketAccessClaim() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a COSI bucket access claim", + Run: RunCreateBucketAccessClaim, + } + cmd.Flags().String("app-namespace", "", + "Set the namespace of the application where the COSI bucket access claim should be created") + cmd.Flags().String("bucket-claim", "", + "Set the bucket claim name to which the user require access credentials") + cmd.Flags().String("bucket-access-class", "", + "Set bucket access class name to specify the bucket access policy") + cmd.Flags().String("creds-secret-name", "", + "Set the secret name in which COSI will set the access credentials to the bucket") + return cmd +} + +// CmdDeleteBucketAccessClaim returns a CLI command +func CmdDeleteBucketAccessClaim() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a COSI bucket access claim", + Run: RunDeleteBucketAccessClaim, + } + cmd.Flags().String("app-namespace", "", + "Set the namespace of the application where the COSI bucket access claim exists") + return cmd +} + +// CmdStatusBucketAccessClaim returns a CLI command +func CmdStatusBucketAccessClaim() *cobra.Command { + cmd := &cobra.Command{ + Use: "status ", + Short: "Status of a COSI bucket access claim", + Run: RunStatusBucketAccessClaim, + } + cmd.Flags().String("app-namespace", "", + "Set the namespace of the application where the COSI bucket access claim exists") + return cmd +} + +// CmdListBucketAccessClaim returns a CLI command +func CmdListBucketAccessClaim() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List COSI bucket access claims", + Run: RunListBucketAccessClaim, + Args: cobra.NoArgs, + } + return cmd +} + +// RunCreateBucketAccessClaim runs a CLI command +func RunCreateBucketAccessClaim(cmd *cobra.Command, args []string) { + log := util.Logger() + + if len(args) != 1 || args[0] == "" { + log.Fatalf(`Missing expected arguments: %s`, cmd.UsageString()) + } + name := args[0] + + appNamespace, _ := cmd.Flags().GetString("app-namespace") + bucketClaimName := util.GetFlagStringOrPrompt(cmd, "bucket-claim") + bucketAccessClassName := util.GetFlagStringOrPrompt(cmd, "bucket-access-class") + credsSecretName := util.GetFlagStringOrPrompt(cmd, "creds-secret-name") + + cosiBucketAccessClaim := util.KubeObject(bundle.File_deploy_cosi_bucket_access_claim_yaml).(*nbv1.COSIBucketAccessClaim) + cosiBucketAccessClaim.Name = name + cosiBucketAccessClaim.Namespace = options.Namespace + if appNamespace != "" { + cosiBucketAccessClaim.Namespace = appNamespace + } + + cosiBucketAccessClaim.Spec.BucketClaimName = bucketClaimName + cosiBucketAccessClaim.Spec.CredentialsSecretName = credsSecretName + cosiBucketAccessClaim.Spec.BucketAccessClassName = bucketAccessClassName + + bucketAccessClass := util.KubeObject(bundle.File_deploy_cosi_bucket_access_class_yaml).(*nbv1.COSIBucketAccessClass) + bucketAccessClass.Name = bucketAccessClassName + if !util.KubeCheck(bucketAccessClass) { + log.Fatalf(`❌ Could not get bucketAccessClass %q`, bucketAccessClass.Name) + } + + bucketClaim := util.KubeObject(bundle.File_deploy_cosi_bucket_claim_yaml).(*nbv1.COSIBucketClaim) + bucketClaim.Name = bucketClaimName + bucketClaim.Namespace = cosiBucketAccessClaim.Namespace + if !util.KubeCheck(bucketClaim) { + log.Fatalf(`❌ Could not get BucketClaim %q`, bucketClaim.Name) + } + + // NOTE - when/if extra parameters are supported in bucketAccessClass we will need to validate them here + + if !util.KubeCreateFailExisting(cosiBucketAccessClaim) { + log.Fatalf(`❌ Could not create COSI bucket access claim %q in namespace %q (conflict)`, cosiBucketAccessClaim.Name, cosiBucketAccessClaim.Namespace) + } + + log.Printf("") + util.PrintThisNoteWhenFinishedApplyingAndStartWaitLoop() + log.Printf("") + log.Printf("COSI bucket access claim Wait Ready:") + if WaitBucketAccessClaimReady(cosiBucketAccessClaim) { + log.Printf("") + log.Printf("") + RunStatusBucketAccessClaim(cmd, args) + } +} + +// RunDeleteBucketAccessClaim runs a CLI command +func RunDeleteBucketAccessClaim(cmd *cobra.Command, args []string) { + log := util.Logger() + + if len(args) != 1 || args[0] == "" { + log.Fatalf(`Missing expected arguments: %s`, cmd.UsageString()) + } + appNamespace, _ := cmd.Flags().GetString("app-namespace") + + cosiBucketAccessClaim := util.KubeObject(bundle.File_deploy_cosi_bucket_access_claim_yaml).(*nbv1.COSIBucketAccessClaim) + cosiBucketAccessClaim.Name = args[0] + cosiBucketAccessClaim.Namespace = options.Namespace + if appNamespace != "" { + cosiBucketAccessClaim.Namespace = appNamespace + } + + if !util.KubeDelete(cosiBucketAccessClaim) { + log.Fatalf(`❌ Could not delete COSI bucket access claim %q in namespace %q`, + cosiBucketAccessClaim.Name, cosiBucketAccessClaim.Namespace) + } +} + +// RunStatusBucketAccessClaim runs a CLI command +func RunStatusBucketAccessClaim(cmd *cobra.Command, args []string) { + log := util.Logger() + + if len(args) != 1 || args[0] == "" { + log.Fatalf(`Missing expected arguments: %s`, cmd.UsageString()) + } + + appNamespace, _ := cmd.Flags().GetString("app-namespace") + + cosiBucketAccessClaim := util.KubeObject(bundle.File_deploy_cosi_bucket_access_claim_yaml).(*nbv1.COSIBucketAccessClaim) + secret := util.KubeObject(bundle.File_deploy_internal_secret_empty_yaml).(*corev1.Secret) + + cosiBucketAccessClaim.Name = args[0] + cosiBucketAccessClaim.Namespace = options.Namespace + secret.Namespace = options.Namespace + if appNamespace != "" { + cosiBucketAccessClaim.Namespace = appNamespace + secret.Namespace = appNamespace + + } + + if !util.KubeCheck(cosiBucketAccessClaim) { + log.Fatalf(`❌ Could not find COSI bucket access claim %q in namespace %q`, cosiBucketAccessClaim.Name, cosiBucketAccessClaim.Namespace) + } + + bucketAccessClass := &nbv1.COSIBucketAccessClass{ + TypeMeta: metav1.TypeMeta{Kind: "BucketAccessClass"}, + ObjectMeta: metav1.ObjectMeta{ + Name: cosiBucketAccessClaim.Spec.BucketAccessClassName, + }, + } + + if !util.KubeCheck(bucketAccessClass) { + log.Errorf(`❌ Could not get BucketAccessClass %s`, bucketAccessClass.Name) + } + + secret.Name = cosiBucketAccessClaim.Spec.CredentialsSecretName + if !util.KubeCheck(secret) { + log.Fatalf(`❌ Could not find COSI bucket access claim secret %q in namespace %q`, secret.Name, cosiBucketAccessClaim.Namespace) + } + + sysClient, err := system.Connect(true) + if err != nil { + util.Logger().Fatalf("❌ %s", err) + } + var a *nb.AccountInfo + if cosiBucketAccessClaim.Status.AccountID != "" { + nbClient := sysClient.NBClient + account, err := nbClient.ReadAccountAPI(nb.ReadAccountParams{Email: cosiBucketAccessClaim.Status.AccountID}) + if err == nil { + a = &account + } + } + + fmt.Printf("\n") + fmt.Printf("COSI BucketAccessClaim info:\n") + fmt.Printf(" %-22s : %t\n", "Bucket Access Granted", cosiBucketAccessClaim.Status.AccessGranted) + fmt.Printf(" %-22s : kubectl get -n %s bucketaccessclaim %s\n", "COSIBucketAccessClaim", cosiBucketAccessClaim.Namespace, cosiBucketAccessClaim.Name) + fmt.Printf(" %-22s : kubectl get bucketaccessclasses.objectstorage.k8s.io %s\n", "BucketAccessClass", bucketAccessClass.Name) + fmt.Printf("\n") + + credsEnv := "" + bucketInfo := secret.StringData["BucketInfo"] + if bucketInfo != "" { + var bucketInfoObj nbv1.COSIBucketInfo + if err := json.Unmarshal([]byte(bucketInfo), &bucketInfoObj); err != nil { + log.Fatal("error deserializing secret BucketInfo") + } + accessKey := bucketInfoObj.Spec.S3.AccessKeyID + secretKey := bucketInfoObj.Spec.S3.AccessSecretKey + accessKeyProperty := "AWS_ACCESS_KEY_ID" + secretKeyProperty := "AWS_SECRET_ACCESS_KEY" + if options.ShowSecrets { + fmt.Printf(" %-22s : %s\n", accessKeyProperty, accessKey) + fmt.Printf(" %-22s : %s\n", accessKeyProperty, secretKey) + credsEnv += accessKeyProperty + "=" + accessKey + " " + credsEnv += secretKeyProperty + "=" + secretKey + " " + } else { + fmt.Printf(" %-22s : %s\n", accessKeyProperty, nb.MaskedString(accessKey)) + fmt.Printf(" %-22s : %s\n", accessKeyProperty, nb.MaskedString(secretKey)) + credsEnv += accessKeyProperty + "=" + string(nb.MaskedString(accessKey)) + " " + credsEnv += secretKeyProperty + "=" + string(nb.MaskedString(secretKey)) + " " + } + + } + + fmt.Printf("Shell commands:\n") + fmt.Printf(" %-22s : alias s3='%saws s3 --no-verify-ssl --endpoint-url %s'\n", "AWS S3 Alias", credsEnv, sysClient.S3URL.String()) + fmt.Printf("\n") + if a != nil { + fmt.Printf("Account status:\n") + fmt.Printf(" %-22s : %s\n", "Name", a.Name) + fmt.Printf(" %-22s : %s\n", "Email", a.Email) + fmt.Printf(" %-22s : %s\n", "DefaultResource", a.DefaultResource) + fmt.Printf(" %-22s : %t\n", "S3Access", a.HasS3Access) + fmt.Printf(" %-22s : %t\n", "AllowBucketCreate", a.CanCreateBuckets) + fmt.Printf("\n") + } + +} + +// RunListBucketAccessClaim runs a CLI command +func RunListBucketAccessClaim(cmd *cobra.Command, args []string) { + list := &nbv1.COSIBucketAccessClaimList{ + TypeMeta: metav1.TypeMeta{Kind: "BucketAccessClaim"}, + } + if !util.KubeList(list) { + return + } + if len(list.Items) == 0 { + fmt.Printf("No COSI bucket access claims found.\n") + return + } + table := (&util.PrintTable{}).AddRow( + "NAMESPACE", + "NAME", + "ACCOUNT-NAME", + "BUCKET-CLAIM", + "BUCKET-ACCESS-CLASS", + "ACCESS-GRANTED", + ) + for i := range list.Items { + cosiBucketAccessClaim := &list.Items[i] + table.AddRow( + cosiBucketAccessClaim.Namespace, + cosiBucketAccessClaim.Name, + cosiBucketAccessClaim.Status.AccountID, + cosiBucketAccessClaim.Spec.BucketClaimName, + cosiBucketAccessClaim.Spec.BucketAccessClassName, + fmt.Sprintf("%t", bool(cosiBucketAccessClaim.Status.AccessGranted)), + ) + } + fmt.Print(table.String()) +} + +// WaitBucketAccessClaimReady waits until the cosi bucket claim status bucket ready changes to true +func WaitBucketAccessClaimReady(cosiBucketAccessClaim *nbv1.COSIBucketAccessClaim) bool { + log := util.Logger() + klient := util.KubeClient() + + intervalSec := time.Duration(3) + maxRetries := 60 + retries := 0 + err := wait.PollImmediateInfinite(intervalSec*time.Second, func() (bool, error) { + if retries == maxRetries { + return false, fmt.Errorf("COSI bucket claim is not ready after max retries - %q", maxRetries) + } + retries++ + err := klient.Get(util.Context(), util.ObjectKey(cosiBucketAccessClaim), cosiBucketAccessClaim) + if err != nil { + log.Printf("⏳ Failed to get COSI bucket access claim: %s", err) + return false, nil + } + CheckBucketAccessClaimPhase(cosiBucketAccessClaim) + if cosiBucketAccessClaim.Status.AccessGranted { + return true, nil + } + return false, nil + }) + return (err == nil) +} + +// CheckBucketAccessClaimPhase prints the phase and reason for it +func CheckBucketAccessClaimPhase(cosiBucketAccessClaim *nbv1.COSIBucketAccessClaim) { + log := util.Logger() + if cosiBucketAccessClaim.Status.AccessGranted { + log.Printf("✅ COSI bucket access claim %q granted", cosiBucketAccessClaim.Name) + } else { + log.Printf("⏳ COSI bucket access claim %q is not yet granted", cosiBucketAccessClaim.Name) + } +} diff --git a/pkg/cosi/cosi_bucket_claim.go b/pkg/cosi/cosi_bucket_claim.go index 611d4c59c..8e296b734 100644 --- a/pkg/cosi/cosi_bucket_claim.go +++ b/pkg/cosi/cosi_bucket_claim.go @@ -36,7 +36,7 @@ func CmdCOSIBucketClaim() *cobra.Command { func CmdCreateBucketClaim() *cobra.Command { cmd := &cobra.Command{ Use: "create ", - Short: "Create an cosi bucket claim", + Short: "Create a COSI bucket claim", Run: RunCreateBucketClaim, } diff --git a/pkg/cosi/cosi_bucket_class.go b/pkg/cosi/cosi_bucket_class.go index 004af2190..c53a79757 100644 --- a/pkg/cosi/cosi_bucket_class.go +++ b/pkg/cosi/cosi_bucket_class.go @@ -35,7 +35,7 @@ func CmdCOSIBucketClass() *cobra.Command { func CmdCreateBucketClass() *cobra.Command { cmd := &cobra.Command{ Use: "create", - Short: "Create an cosi bucket class", + Short: "Create a COSI bucket class", } cmd.AddCommand( @@ -351,7 +351,6 @@ func createCommonCOSIBucketclass(cmd *cobra.Command, args []string, bucketClassT } log.Printf("") - util.PrintThisNoteWhenFinishedApplyingAndStartWaitLoop() log.Printf("") RunStatusBucketClass(cmd, args) } diff --git a/pkg/cosi/cosi_cli_test.go b/pkg/cosi/cosi_cli_test.go index e9302980b..5b1ca89a0 100644 --- a/pkg/cosi/cosi_cli_test.go +++ b/pkg/cosi/cosi_cli_test.go @@ -1,21 +1,21 @@ package cosi import ( - "os" - "os/exec" - "strings" - + "fmt" nbv1 "github.com/noobaa/noobaa-operator/v5/pkg/apis/noobaa/v1alpha1" + "github.com/noobaa/noobaa-operator/v5/pkg/bundle" namespacestore "github.com/noobaa/noobaa-operator/v5/pkg/namespacestore" - "github.com/noobaa/noobaa-operator/v5/pkg/options" "github.com/noobaa/noobaa-operator/v5/pkg/system" "github.com/noobaa/noobaa-operator/v5/pkg/util" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "os" + "os/exec" + "strings" ) -var _ = Describe("COSI driver/provisioner tests", func() { +var _ = Describe("COSI CLI tests", func() { os.Setenv("TEST_ENV", "true") options.Namespace = "test" @@ -24,6 +24,10 @@ var _ = Describe("COSI driver/provisioner tests", func() { firstBucket := "first.bucket" pcBCName := "pc-bucket-class-cli" nsBCName := "ns-bucket-class-cli" + acName1 := "access-class-cli1" + acName2 := "access-class-cli2" + aclaim := "access-claim-cli" + aclaimSecret := "access-claim-secret-cli" invalidNsBCName := "invalid-ns-bucket-class-cli" claimName := "cosi-claim-cli" cosiNSResourceName := "cosi-nsr-cli" @@ -141,4 +145,163 @@ var _ = Describe("COSI driver/provisioner tests", func() { Expect(err).To(BeNil()) }) }) + + Context("Bucket Access class CLI functions", func() { + + It("cosi bucketAccessclass create", func() { + cmd := exec.Command(CLIPath, "cosi", "accessclass", "create", acName1, "-n", options.Namespace) + log.Printf("Running command: create %v args %v ", cmd.Path, cmd.Args) + out, err := cmd.CombinedOutput() + log.Printf("Running command: access class create out %s ", string(out)) + Expect(err).To(BeNil()) + Expect(strings.Contains(string(out), "✅ Created: BucketAccessClass")).To(BeTrue()) + expectedName := fmt.Sprintf("Name:\n %s", acName1) + expectedDriverName := fmt.Sprintf("Driver Name:\n %s", options.COSIDriverName()) + expectedAuthenticationType := fmt.Sprintf("Authentication Type:\n %s", nbv1.COSIKEYAuthenticationType) + Expect(strings.Contains(string(out), expectedName)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedDriverName)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedAuthenticationType)).To(BeTrue()) + }) + + It("cosi bucketAccessclass status", func() { + cmd := exec.Command(CLIPath, "cosi", "accessclass", "status", acName1, "-n", options.Namespace) + log.Printf("Running command: status %v args %v ", cmd.Path, cmd.Args) + out, err := cmd.CombinedOutput() + log.Printf("Running command: access class status out %s ", string(out)) + Expect(err).To(BeNil()) + expectedName := fmt.Sprintf("Name:\n %s", acName1) + expectedDriverName := fmt.Sprintf("Driver Name:\n %s", options.COSIDriverName()) + expectedAuthenticationType := fmt.Sprintf("Authentication Type:\n %s", nbv1.COSIKEYAuthenticationType) + Expect(strings.Contains(string(out), expectedName)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedDriverName)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedAuthenticationType)).To(BeTrue()) + + }) + + It("cosi bucketAccessclass list", func() { + cmd := exec.Command(CLIPath, "cosi", "accessclass", "list", "-n", options.Namespace) + log.Printf("Running command: list %v args %v ", cmd.Path, cmd.Args) + out, err := cmd.CombinedOutput() + log.Printf("Running command: access class list out %s ", string(out)) + Expect(err).To(BeNil()) + }) + + It("cosi bucketAccessclass delete", func() { + cmd := exec.Command(CLIPath, "cosi", "accessclass", "delete", acName1, "-n", options.Namespace) + log.Printf("Running command: status %v args %v ", cmd.Path, cmd.Args) + out, err := cmd.CombinedOutput() + log.Printf("Running command: access class delete out %s ", string(out)) + Expect(err).To(BeNil()) + expectedDeleteMsg := fmt.Sprintf("Deleted : BucketAccessClass \\\"%s\\\"", acName1) + Expect(strings.Contains(string(out), expectedDeleteMsg)).To(BeTrue()) + }) + }) + + Context("Bucket Access claim CLI functions", func() { + + It("cosi bucketAccessclaim create", func() { + + cmd := exec.Command(CLIPath, "cosi", "bucketclass", "create", "placement-bucketclass", pcBCName, "--backingstores", defaultBackingStoreName, "--deletion-policy", "retain", "-n", options.Namespace) + _, err := cmd.CombinedOutput() + Expect(err).To(BeNil()) + cmd = exec.Command(CLIPath, "cosi", "bucketclaim", "create", claimName, "--bucketclass", pcBCName, "-n", options.Namespace) + _, err = cmd.CombinedOutput() + Expect(err).To(BeNil()) + cmd = exec.Command(CLIPath, "cosi", "accessclass", "create", acName2, "-n", options.Namespace) + _, err = cmd.CombinedOutput() + Expect(err).To(BeNil()) + + cmd = exec.Command(CLIPath, "cosi", "accessclaim", "create", aclaim, "--bucket-claim", claimName, "--bucket-access-class", acName2, "--creds-secret-name", aclaimSecret, "-n", options.Namespace) + log.Printf("Running command: create access claim %v args %v ", cmd.Path, cmd.Args) + out, err := cmd.CombinedOutput() + log.Printf("Running command: access claim create out %s ", string(out)) + Expect(err).To(BeNil()) + Expect(strings.Contains(string(out), "✅ Created: BucketAccess")).To(BeTrue()) + + cosiBucketAccessClaim := util.KubeObject(bundle.File_deploy_cosi_bucket_access_claim_yaml).(*nbv1.COSIBucketAccessClaim) + cosiBucketAccessClaim.Name = aclaim + cosiBucketAccessClaim.Namespace = options.Namespace + Expect(util.KubeCheck(cosiBucketAccessClaim)).To(BeTrue()) + Expect(cosiBucketAccessClaim.Status.AccessGranted).To(BeTrue()) + Expect(cosiBucketAccessClaim.Status.AccountID).NotTo(BeNil()) + + expectedCreateBucket := fmt.Sprintf(" %-22s : %s\n", "S3Access", "true") + expectedAccessGranted := fmt.Sprintf(" %-22s : %s\n", "AllowBucketCreate", "false") + expectedDefaultResource := fmt.Sprintf(" %-22s : %s", "DefaultResource", "system-internal-storage-pool") + log.Printf("create status prints expectedCreateBucket=%s \n expectedAccessGranted=%s \n expectedDefaultResource=%s \n", expectedCreateBucket, expectedAccessGranted, expectedDefaultResource) + + expectedName := fmt.Sprintf(" %-22s : %s\n", "Name", cosiBucketAccessClaim.Status.AccountID) + + Expect(strings.Contains(string(out), expectedName)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedAccessGranted)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedDefaultResource)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedCreateBucket)).To(BeTrue()) + }) + + It("cosi bucketAccessclass status", func() { + cmd := exec.Command(CLIPath, "cosi", "accessclaim", "status", aclaim, "-n", options.Namespace) + log.Printf("Running command: status %v args %v ", cmd.Path, cmd.Args) + out, err := cmd.CombinedOutput() + log.Printf("Running command: access claim status out %s ", string(out)) + Expect(err).To(BeNil()) + cosiBucketAccessClaim := util.KubeObject(bundle.File_deploy_cosi_bucket_access_claim_yaml).(*nbv1.COSIBucketAccessClaim) + cosiBucketAccessClaim.Name = aclaim + cosiBucketAccessClaim.Namespace = options.Namespace + Expect(util.KubeCheck(cosiBucketAccessClaim)).To(BeTrue()) + + expectedName := fmt.Sprintf(" %-22s : %s\n", "Name", cosiBucketAccessClaim.Status.AccountID) + expectedCreateBucket := fmt.Sprintf(" %-22s : %s\n", "S3Access", "true") + expectedAccessGranted := fmt.Sprintf(" %-22s : %s\n", "AllowBucketCreate", "false") + expectedDefaultResource := fmt.Sprintf(" %-22s : %s", "DefaultResource", "system-internal-storage-pool") + log.Printf("status prints expectedCreateBucket=%s \n expectedAccessGranted=%s \n expectedDefaultResource=%s \n", expectedCreateBucket, expectedAccessGranted, expectedDefaultResource) + + Expect(strings.Contains(string(out), expectedName)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedAccessGranted)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedDefaultResource)).To(BeTrue()) + Expect(strings.Contains(string(out), expectedCreateBucket)).To(BeTrue()) + }) + + It("cosi bucketAccessclass list", func() { + cmd := exec.Command(CLIPath, "cosi", "accessclaim", "list", "-n", options.Namespace) + log.Printf("Running command: list %v args %v ", cmd.Path, cmd.Args) + out, err := cmd.CombinedOutput() + log.Printf("Running command: access claim list out %s ", string(out)) + Expect(err).To(BeNil()) + Expect(strings.Contains(string(out), aclaim)).To(BeTrue()) + + }) + + It("cosi bucketAccessclass delete", func() { + cmd := exec.Command(CLIPath, "cosi", "accessclaim", "delete", aclaim, "-n", options.Namespace) + log.Printf("Running command: status %v args %v ", cmd.Path, cmd.Args) + out, err := cmd.CombinedOutput() + log.Printf("Running command: access claim delete out %s ", string(out)) + Expect(err).To(BeNil()) + expectedDeleteMsg := fmt.Sprintf("Deleted : BucketAccess \\\"%s\\\"", aclaim) + Expect(strings.Contains(string(out), expectedDeleteMsg)).To(BeTrue()) + cmd = exec.Command("kubectl", "get", "accessclaim", aclaim, "-n", options.Namespace) + log.Printf("Running command: status %v args %v ", cmd.Path, cmd.Args) + out, err = cmd.CombinedOutput() + log.Printf("Running command: kubectl access claim delete out %s ", string(out)) + Expect(strings.Contains(string(out), "error: the server doesn't have a resource type \"accessclaim\"")).To(BeTrue()) + Expect(err).NotTo(BeNil()) + }) + It("Cleanup", func() { + + cmd := exec.Command(CLIPath, "cosi", "bucketclass", "delete", pcBCName, "-n", options.Namespace) + _, err := cmd.CombinedOutput() + log.Printf("Running command: bucketclass access claim delete err %q ", err) + Expect(err).To(BeNil()) + + cmd = exec.Command(CLIPath, "cosi", "bucketclaim", "delete", claimName, "-n", options.Namespace) + log.Printf("Running command: bucketclaim access claim delete err %q ", err) + _, err = cmd.CombinedOutput() + Expect(err).To(BeNil()) + + cmd = exec.Command(CLIPath, "cosi", "accessclass", "delete", acName2, "-n", options.Namespace) + log.Printf("Running command: accessclass access claim delete err %q ", err) + _, err = cmd.CombinedOutput() + Expect(err).To(BeNil()) + }) + }) })