diff --git a/.gitignore b/.gitignore index c45a9fa8e..3e479fac0 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,4 @@ go.work ehthumbs.db Thumbs.db -vendor/ \ No newline at end of file +vendor/.gomodcache/ diff --git a/cmd/nabsl-request/approve.go b/cmd/nabsl-request/approve.go deleted file mode 100644 index 66c6b358c..000000000 --- a/cmd/nabsl-request/approve.go +++ /dev/null @@ -1,135 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 nabsl - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd" -) - -// NewApproveCommand creates the "approve" subcommand under bsl request -func NewApproveCommand(f client.Factory) *cobra.Command { - o := NewApproveOptions() - - c := &cobra.Command{ - Use: "approve REQUEST_NAME", - Short: "Approve a pending backup storage location request", - Long: "Approve a pending backup storage location request to allow the controller to create the corresponding BackupStorageLocation", - Args: cobra.ExactArgs(1), - Example: ` # Approve a request by NABSL name (admin access required) - oc oadp nabsl-request approve user-test-bsl - - # Approve a request by UUID with reason - oc oadp nabsl-request approve nacuser01-user-test-bsl-96dfa8b7-3f6f-4c8d-a168-8527b00fbed8 --reason "Approved for production use"`, - Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(args, f)) - cmd.CheckError(o.Run(c, f)) - }, - } - - o.BindFlags(c.Flags()) - - return c -} - -type ApproveOptions struct { - RequestName string - Reason string - client kbclient.WithWatch -} - -func NewApproveOptions() *ApproveOptions { - return &ApproveOptions{} -} - -func (o *ApproveOptions) BindFlags(flags *pflag.FlagSet) { - flags.StringVar(&o.Reason, "reason", "", "Reason for approval (optional)") -} - -func (o *ApproveOptions) Complete(args []string, f client.Factory) error { - o.RequestName = args[0] - - client, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeVeleroTypes: true, - IncludeNonAdminTypes: true, - }) - if err != nil { - return err - } - - o.client = client - return nil -} - -func (o *ApproveOptions) Run(c *cobra.Command, f client.Factory) error { - - adminNS := f.Namespace() - - requestName, err := shared.FindNABSLRequestByNameOrUUID(context.Background(), o.client, o.RequestName, adminNS) - if err != nil { - return err - } - - var request nacv1alpha1.NonAdminBackupStorageLocationRequest - err = o.client.Get(context.Background(), kbclient.ObjectKey{ - Name: requestName, - Namespace: adminNS, - }, &request) - if err != nil { - return fmt.Errorf("failed to get request %q: %w", requestName, err) - } - - // Check if already approved - if request.Spec.ApprovalDecision == "approve" { - fmt.Printf("Request %q is already approved.\n", o.RequestName) - return nil - } - - // Update the approval decision - request.Spec.ApprovalDecision = "approve" - if o.Reason != "" { - if request.Annotations == nil { - request.Annotations = make(map[string]string) - } - request.Annotations["openshift.io/oadp-approval-reason"] = o.Reason - } - - err = o.client.Update(context.Background(), &request) - if err != nil { - return fmt.Errorf("failed to approve request: %w", err) - } - - // Get the NABSL name for user-friendly output - nabslName := o.RequestName - if request.Status.SourceNonAdminBSL != nil { - nabslName = request.Status.SourceNonAdminBSL.Name - } - - fmt.Printf("Request for NonAdminBackupStorageLocation %q has been approved.\n", nabslName) - fmt.Printf("The controller will now create the corresponding BackupStorageLocation.\n") - - return nil -} diff --git a/cmd/nabsl-request/describe.go b/cmd/nabsl-request/describe.go deleted file mode 100644 index 461aeeabd..000000000 --- a/cmd/nabsl-request/describe.go +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 nabsl - -import ( - "context" - "fmt" - "os" - "sort" - - "github.com/spf13/cobra" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd" -) - -func NewDescribeCommand(f client.Factory) *cobra.Command { - o := NewDescribeOptions() - - c := &cobra.Command{ - Use: "describe NAME", - Short: "Describe a non-admin backup storage location request", - Args: cobra.ExactArgs(1), - Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(args, f)) - cmd.CheckError(o.Validate(c, args, f)) - cmd.CheckError(o.Run(c, f)) - }, - Example: ` # Describe a request by NABSL name - oc oadp nabsl-request describe my-bsl-request - - # Describe a request by UUID - oc oadp nabsl-request describe nacuser01-my-bsl-96dfa8b7-3f6f-4c8d-a168-8527b00fbed8`, - } - - return c -} - -type DescribeOptions struct { - UUID_Name string - client kbclient.WithWatch -} - -func NewDescribeOptions() *DescribeOptions { - return &DescribeOptions{} -} - -func (o *DescribeOptions) Complete(args []string, f client.Factory) error { - o.UUID_Name = args[0] - - client, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeVeleroTypes: true, - IncludeNonAdminTypes: true, - }) - if err != nil { - return err - } - - o.client = client - return nil -} - -func (o *DescribeOptions) Validate(c *cobra.Command, args []string, f client.Factory) error { - return nil -} - -func (o *DescribeOptions) Run(c *cobra.Command, f client.Factory) error { - // Get the admin namespace (from client config) where requests are stored - adminNS := f.Namespace() - - // Get the request from openshift-adp namespace using the UUID - var request nacv1alpha1.NonAdminBackupStorageLocationRequest - requestName, err := shared.FindNABSLRequestByNameOrUUID(context.Background(), o.client, o.UUID_Name, adminNS) - if err != nil { - return err - } - - err = o.client.Get(context.Background(), kbclient.ObjectKey{ - Name: requestName, - Namespace: adminNS, - }, &request) - - if err != nil { - return fmt.Errorf("failed to get request for %q: %w", requestName, err) - } - - return describeRequest(&request) -} - -func describeRequest(request *nacv1alpha1.NonAdminBackupStorageLocationRequest) error { - // Name and Namespace - fmt.Printf("Name: %s\n", request.Name) - fmt.Printf("Namespace: %s\n", request.Namespace) - - // Labels - shared.PrintLabelsOrAnnotations(os.Stdout, "Labels: ", request.Labels) - - // Annotations - shared.PrintLabelsOrAnnotations(os.Stdout, "Annotations: ", request.Annotations) - - fmt.Printf("\n") - - // Phase (with color) - fmt.Printf("Phase: %s\n", shared.ColorizePhase(string(request.Status.Phase))) - - fmt.Printf("\n") - - // Approval Decision - if request.Spec.ApprovalDecision != "" { - fmt.Printf("Approval Decision: %s\n", request.Spec.ApprovalDecision) - fmt.Printf("\n") - } - - // Requested NonAdminBackupStorageLocation - if request.Status.SourceNonAdminBSL != nil { - source := request.Status.SourceNonAdminBSL - fmt.Printf("Requested NonAdminBackupStorageLocation:\n") - fmt.Printf(" Name: %s\n", source.Name) - fmt.Printf(" Namespace: %s\n", source.Namespace) - - if source.NACUUID != "" { - fmt.Printf(" NACUUID: %s\n", source.NACUUID) - } - - fmt.Printf("\n") - - // Requested BackupStorageLocation Spec - if source.RequestedSpec != nil { - spec := source.RequestedSpec - fmt.Printf("Requested BackupStorageLocation Spec:\n") - fmt.Printf(" Provider: %s\n", spec.Provider) - fmt.Printf(" Object Storage Bucket: %s\n", spec.ObjectStorage.Bucket) - - if spec.ObjectStorage.Prefix != "" { - fmt.Printf(" Prefix: %s\n", spec.ObjectStorage.Prefix) - } - - if len(spec.Config) > 0 { - fmt.Printf(" Config:\n") - configKeys := make([]string, 0, len(spec.Config)) - for k := range spec.Config { - configKeys = append(configKeys, k) - } - sort.Strings(configKeys) - for _, k := range configKeys { - fmt.Printf(" %s: %s\n", k, spec.Config[k]) - } - } - - if spec.AccessMode != "" { - fmt.Printf(" Access Mode: %s\n", spec.AccessMode) - } - - if spec.BackupSyncPeriod != nil { - fmt.Printf(" Backup Sync Period: %s\n", spec.BackupSyncPeriod.String()) - } - - if spec.ValidationFrequency != nil { - fmt.Printf(" Validation Frequency: %s\n", spec.ValidationFrequency.String()) - } - - fmt.Printf("\n") - } - } - - // Creation Timestamp - fmt.Printf("Creation Timestamp: %s\n", request.CreationTimestamp.Format("2006-01-02 15:04:05 -0700 MST")) - - return nil -} diff --git a/cmd/nabsl-request/get.go b/cmd/nabsl-request/get.go deleted file mode 100644 index 483c06752..000000000 --- a/cmd/nabsl-request/get.go +++ /dev/null @@ -1,169 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 nabsl - -import ( - "context" - "fmt" - "os" - "text/tabwriter" - - "github.com/spf13/cobra" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewGetCommand(f client.Factory) *cobra.Command { - o := NewGetOptions() - - c := &cobra.Command{ - Use: "get [NAME]", - Short: "Get non-admin backup storage location requests", - Args: cobra.MaximumNArgs(1), - Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(args, f)) - cmd.CheckError(o.Run(c, f)) - }, - Example: ` # Get all backup storage location requests (admin access required) - oc oadp nabsl-request get - - # Get a specific request by NABSL name - oc oadp nabsl-request get my-bsl-request - - # Get a specific request by UUID - oc oadp nabsl-request get nacuser01-my-bsl-96dfa8b7-3f6f-4c8d-a168-8527b00fbed8 - - # Get output in YAML format - oc oadp nabsl-request get my-bsl-request -o yaml`, - } - - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -type GetOptions struct { - Name string - client kbclient.WithWatch -} - -func NewGetOptions() *GetOptions { - return &GetOptions{} -} - -// Complete NABSL request get options -func (o *GetOptions) Complete(args []string, f client.Factory) error { - - if len(args) > 0 { - o.Name = args[0] - } - - client, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeVeleroTypes: true, - IncludeNonAdminTypes: true, - }) - - if err != nil { - return err - } - - o.client = client - return nil -} - -func (o *GetOptions) Run(c *cobra.Command, f client.Factory) error { - // Get the admin namespace (from client config) where requests are stored - adminNS := f.Namespace() - - if o.Name != "" { - // Get specific request by name (UUID) - var request nacv1alpha1.NonAdminBackupStorageLocationRequest - requestName, err := shared.FindNABSLRequestByNameOrUUID(context.Background(), o.client, o.Name, adminNS) - if err != nil { - return err - } - err = o.client.Get(context.Background(), kbclient.ObjectKey{ - Name: requestName, - Namespace: adminNS, - }, &request) - if err != nil { - return fmt.Errorf("failed to get request %q: %w", requestName, err) - } - - if printed, err := output.PrintWithFormat(c, &request); printed || err != nil { - return err - } - - list := &nacv1alpha1.NonAdminBackupStorageLocationRequestList{ - Items: []nacv1alpha1.NonAdminBackupStorageLocationRequest{request}, - } - return printRequestTable(list) - } - - // List all requests in admin namespace - var requestList nacv1alpha1.NonAdminBackupStorageLocationRequestList - var err = o.client.List(context.Background(), &requestList, &kbclient.ListOptions{ - Namespace: adminNS, - }) - - if err != nil { - return fmt.Errorf("failed to list requests: %w", err) - } - - if printed, err := output.PrintWithFormat(c, &requestList); printed || err != nil { - return err - } - - return printRequestTable(&requestList) -} - -func printRequestTable(requestList *nacv1alpha1.NonAdminBackupStorageLocationRequestList) error { - w := tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', 0) - defer w.Flush() - - // Print header - fmt.Fprintln(w, "NAME\tNAMESPACE\tPHASE\tREQUESTED-NABSL\tREQUESTED-NAMESPACE\tAGE") - - for _, request := range requestList.Items { - age := metav1.Now().Sub(request.CreationTimestamp.Time) - - requestedNABSL := "" - requestedNamespace := "" - if request.Status.SourceNonAdminBSL != nil { - requestedNABSL = request.Status.SourceNonAdminBSL.Name - requestedNamespace = request.Status.SourceNonAdminBSL.Namespace - } - - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", - request.Name, - request.Namespace, - request.Status.Phase, - requestedNABSL, - requestedNamespace, - age.Round(1e9).String(), - ) - } - - return nil -} diff --git a/cmd/nabsl-request/nabsl.go b/cmd/nabsl-request/nabsl.go deleted file mode 100644 index 2a1cc472f..000000000 --- a/cmd/nabsl-request/nabsl.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 nabsl - -import ( - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" -) - -// NewNABSLRequestCommand creates the "nabsl-request" command for managing non-admin backup storage location requests -func NewNABSLRequestCommand(f client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "nabsl-request", - Short: "Manage non-admin backup storage location approval requests", - Long: `Manage approval requests for non-admin backup storage locations. - -Non-admin backup storage locations (NABSL) require admin approval before they can be used. -When users create NABSLs, approval requests are automatically generated for admin review. - -Use these commands to view, approve, or reject pending NABSL requests.`, - Example: ` # List all pending NABSL approval requests - oc oadp nabsl-request get - - # Describe a specific NABSL approval request - oc oadp nabsl-request describe my-storage-request - - # Approve a NABSL approval request - oc oadp nabsl-request approve my-storage-request - - # Reject a NABSL approval request - oc oadp nabsl-request reject my-storage-request`, - } - - c.AddCommand( - NewGetCommand(f), - NewDescribeCommand(f), - NewApproveCommand(f), - NewRejectCommand(f), - ) - - return c -} diff --git a/cmd/nabsl-request/nabsl_test.go b/cmd/nabsl-request/nabsl_test.go deleted file mode 100644 index 54f21dbab..000000000 --- a/cmd/nabsl-request/nabsl_test.go +++ /dev/null @@ -1,142 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 nabsl - -import ( - "testing" - - "github.com/migtools/oadp-cli/internal/testutil" -) - -// TestNABSLCommands tests the NABSL command functionality -func TestNABSLCommands(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - tests := []struct { - name string - args []string - expectContains []string - }{ - { - name: "nabsl-request help", - args: []string{"nabsl-request", "--help"}, - expectContains: []string{ - "Manage approval requests for non-admin backup storage locations", - "approve", - "reject", - "describe", - "get", - }, - }, - { - name: "nabsl-request approve help", - args: []string{"nabsl-request", "approve", "--help"}, - expectContains: []string{ - "Approve a pending backup storage location request", - "--reason", - }, - }, - { - name: "nabsl-request reject help", - args: []string{"nabsl-request", "reject", "--help"}, - expectContains: []string{ - "Reject a pending backup storage location request", - "--reason", - }, - }, - { - name: "nabsl-request get help", - args: []string{"nabsl-request", "get", "--help"}, - expectContains: []string{ - "Get non-admin backup storage location requests", - }, - }, - { - name: "nabsl-request describe help", - args: []string{"nabsl-request", "describe", "--help"}, - expectContains: []string{ - "Describe a non-admin backup storage location request", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, tt.args, tt.expectContains) - }) - } -} - -// TestNABSLHelpFlags tests that both --help and -h work for nabsl-request commands -func TestNABSLHelpFlags(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - commands := [][]string{ - {"nabsl-request", "--help"}, - {"nabsl-request", "-h"}, - {"nabsl-request", "approve", "--help"}, - {"nabsl-request", "approve", "-h"}, - {"nabsl-request", "reject", "--help"}, - {"nabsl-request", "reject", "-h"}, - {"nabsl-request", "get", "--help"}, - {"nabsl-request", "get", "-h"}, - {"nabsl-request", "describe", "--help"}, - {"nabsl-request", "describe", "-h"}, - } - - for _, cmd := range commands { - t.Run("help_flags_"+cmd[len(cmd)-1], func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{"Usage:"}) - }) - } -} - -// TestNABSLClientConfigIntegration tests that NABSL request commands respect client config -func TestNABSLClientConfigIntegration(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - _, cleanup := testutil.SetupTempHome(t) - defer cleanup() - - t.Run("nabsl-request commands work with client config", func(t *testing.T) { - // Set a known namespace - _, err := testutil.RunCommand(t, binaryPath, "client", "config", "set", "namespace=admin-namespace") - if err != nil { - t.Fatalf("Failed to set client config: %v", err) - } - - // Test that nabsl-request commands can be invoked (they should respect the namespace) - // We test help commands since they don't require actual K8s resources - commands := [][]string{ - {"nabsl-request", "get", "--help"}, - {"nabsl-request", "approve", "--help"}, - {"nabsl-request", "reject", "--help"}, - {"nabsl-request", "describe", "--help"}, - } - - for _, cmd := range commands { - t.Run("config_test_"+cmd[1], func(t *testing.T) { - output, err := testutil.RunCommand(t, binaryPath, cmd...) - if err != nil { - t.Fatalf("NABSL request command should work with client config: %v", err) - } - if output == "" { - t.Errorf("Expected help output for %v", cmd) - } - }) - } - }) -} diff --git a/cmd/nabsl-request/reject.go b/cmd/nabsl-request/reject.go deleted file mode 100644 index b3e3d9ce2..000000000 --- a/cmd/nabsl-request/reject.go +++ /dev/null @@ -1,139 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 nabsl - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd" -) - -// NewRejectCommand creates the "deny" subcommand under bsl request -func NewRejectCommand(f client.Factory) *cobra.Command { - o := NewRejectOptions() - - c := &cobra.Command{ - Use: "reject REQUEST_NAME", - Short: "Reject a pending backup storage location request", - Long: "Reject a pending backup storage location request to deny the user's request for a backup storage location", - Args: cobra.ExactArgs(1), - Example: ` # Deny a request by NABSL name (admin access required) - oc oadp nabsl-request reject user-test-bsl --reason "Invalid configuration" - - # Deny a request by UUID with detailed reason - oc oadp nabsl-request reject nacuser01-user-test-bsl-96dfa8b7-3f6f-4c8d-a168-8527b00fbed8 --reason "Bucket does not exist in specified region"`, - Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(args, f)) - cmd.CheckError(o.Run(c, f)) - }, - } - - o.BindFlags(c.Flags()) - - return c -} - -type RejectOptions struct { - RequestName string - Reason string - client kbclient.WithWatch -} - -func NewRejectOptions() *RejectOptions { - return &RejectOptions{} -} - -func (o *RejectOptions) BindFlags(flags *pflag.FlagSet) { - flags.StringVar(&o.Reason, "reason", "", "Reason for denial (recommended)") -} - -func (o *RejectOptions) Complete(args []string, f client.Factory) error { - o.RequestName = args[0] - - client, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeVeleroTypes: true, - IncludeNonAdminTypes: true, - }) - if err != nil { - return err - } - - o.client = client - return nil -} - -func (o *RejectOptions) Run(c *cobra.Command, f client.Factory) error { - // Get the admin namespace (from client config) where requests are stored - adminNS := f.Namespace() - - // Find the request either by UUID or by looking up NABSL name - requestName, err := shared.FindNABSLRequestByNameOrUUID(context.Background(), o.client, o.RequestName, adminNS) - if err != nil { - return err - } - - // Get the current request - var request nacv1alpha1.NonAdminBackupStorageLocationRequest - err = o.client.Get(context.Background(), kbclient.ObjectKey{ - Name: requestName, - Namespace: adminNS, - }, &request) - if err != nil { - return fmt.Errorf("failed to get request %q: %w", requestName, err) - } - - // Check if already rejected - if request.Spec.ApprovalDecision == "reject" { - fmt.Printf("Request %q is already rejected.\n", o.RequestName) - return nil - } - - // Update the approval decision - request.Spec.ApprovalDecision = "reject" - if o.Reason != "" { - if request.Annotations == nil { - request.Annotations = make(map[string]string) - } - request.Annotations["openshift.io/oadp-rejection-reason"] = o.Reason - } - - err = o.client.Update(context.Background(), &request) - if err != nil { - return fmt.Errorf("failed to deny request: %w", err) - } - - // Get the NABSL name for user-friendly output - nabslName := o.RequestName - if request.Status.SourceNonAdminBSL != nil { - nabslName = request.Status.SourceNonAdminBSL.Name - } - - fmt.Printf("Request for NonAdminBackupStorageLocation %q has been rejected.\n", nabslName) - if o.Reason != "" { - fmt.Printf("Reason: %s\n", o.Reason) - } - - return nil -} diff --git a/cmd/non-admin/backup/README.md b/cmd/non-admin/backup/README.md deleted file mode 100644 index d2efb38b1..000000000 --- a/cmd/non-admin/backup/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# NonAdminBackup Create Command - -## Overview - -The `nonadmin backup create` command creates backup requests for non-admin users within their authorized namespaces. - -## Minimal MVP Flags - -The following flags represent the minimal viable product for backup creation: - -### Resource Filtering - -| Flag | Type | Default | Description | Status | -|------|------|---------|-------------|--------| -| `--include-resources` | StringArray | `["*"]` | Resources to include | ✅ MVP | -| `--exclude-resources` | StringArray | - | Resources to exclude | ✅ MVP | - -### Label Selection - -| Flag | Type | Default | Description | Status | -|------|------|---------|-------------|--------| -| `--selector`, `-l` | LabelSelector | - | Label selector filter | ✅ MVP | -| `--or-selector` | OrLabelSelector | - | OR label selectors | ✅ MVP | - -### Cluster Resources - -| Flag | Type | Default | Description | Status | -|------|------|---------|-------------|--------| -| `--include-cluster-resources` | OptionalBool | - | Include cluster resources (users can only set to false) | ✅ MVP | - -### Timing & Storage - -| Flag | Type | Default | Description | Status | -|------|------|---------|-------------|--------| -| `--ttl` | Duration | - | Backup retention time | ✅ MVP | -| `--storage-location` | String | - | NABSL reference | ✅ MVP | -| `--csi-snapshot-timeout` | Duration | - | CSI snapshot timeout | ✅ MVP | -| `--item-operation-timeout` | Duration | - | Async operation timeout | ✅ MVP | - -### Snapshot Control - -| Flag | Type | Default | Description | Status | -|------|------|---------|-------------|--------| -| `--snapshot-volumes` | OptionalBool | - | Enable volume snapshots | ✅ MVP | -| `--snapshot-move-data` | OptionalBool | - | Move snapshot data | ✅ MVP | -| `--default-volumes-to-fs-backup` | OptionalBool | - | Use filesystem backup | ✅ MVP | - -## Restricted Flags (Not Available) - -The following flags are **restricted** for non-admin users per the NAB API restrictions: - -| Flag | Reason | Doc Reference | -|------|--------|---------------| -| `--include-namespaces` | Restricted - automatically set to current namespace | NAB API docs | -| `--exclude-namespaces` | Restricted for non-admin users | NAB API docs | -| `--include-cluster-scoped-resources` | Restricted - only empty list acceptable | NAB API docs | -| `--volume-snapshot-locations` | Not supported - defaults used | NAB API docs | - -## Flags Not in MVP (Future Enhancements) - -The following flags are **allowed by the API** but not included in the minimal MVP: - -### Metadata -| Flag | Admin Enforceable | Could Add Later | -|------|------------------|-----------------| -| `--labels` | ✅ Yes | Future | -| `--annotations` | ✅ Yes | Future | - -### Advanced Features -| Flag | Admin Enforceable | Could Add Later | -|------|------------------|-----------------| -| `--from-schedule` | N/A | Future (requires schedule API) | -| `--ordered-resources` | ✅ Yes | Future | -| `--data-mover` | ✅ Yes | Future | -| `--resource-policies-configmap` | ✅ Yes | Future (admin-created only) | -| `--parallel-files-upload` | ✅ Yes | Future | - -### Scoped Resources -| Flag | Admin Enforceable | Could Add Later | -|------|------------------|-----------------| -| `--exclude-cluster-scoped-resources` | ✅ Yes | Future | -| `--include-namespace-scoped-resources` | ✅ Yes | Future | -| `--exclude-namespace-scoped-resources` | ✅ Yes | Future | - -## Examples - -```bash -# Create a simple backup of all resources in the current namespace -oadp nonadmin backup create my-backup - -# Create backup with specific resources -oadp nonadmin backup create my-backup \ - --include-resources deployments,services - -# Create backup with label selector -oadp nonadmin backup create my-backup \ - --selector app=myapp - -# Create backup with snapshots and TTL -oadp nonadmin backup create my-backup \ - --snapshot-volumes \ - --ttl 720h - -# Create backup with specific storage location -oadp nonadmin backup create my-backup \ - --storage-location my-nabsl -``` - -## Architecture Notes - -The backup create command uses **struct embedding** from Velero's backup CreateOptions, matching the pattern used in `nonadmin restore create`. This approach: -- Reduces code duplication -- Ensures compatibility with Velero updates -- Uses BindFlags() as the control gate to expose only MVP features to non-admin users -- Maintains forward compatibility for future enhancements - -## Implementation Details - -### Struct Embedding Pattern - -```go -type CreateOptions struct { - *velerobackup.CreateOptions // Embed Velero's CreateOptions - - // NAB-specific fields - Name string - client kbclient.WithWatch - currentNamespace string -} -``` - -### MVP Flag Control - -The `BindFlags()` method acts as a control gate, exposing only the MVP flags while the embedded struct contains all Velero options. This allows: -- Easy addition of new flags in the future (just bind them in BindFlags) -- Automatic compatibility with Velero struct updates -- Clear separation between what's exposed vs what's available diff --git a/cmd/non-admin/backup/backup.go b/cmd/non-admin/backup/backup.go deleted file mode 100644 index 938004cd7..000000000 --- a/cmd/non-admin/backup/backup.go +++ /dev/null @@ -1,42 +0,0 @@ -package backup - -/* -Copyright 2017 the Velero contributors. - -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. -*/ - -import ( - "github.com/spf13/cobra" - - "github.com/vmware-tanzu/velero/pkg/client" -) - -// NewBackupCommand creates the "backup" subcommand under nonadmin -func NewBackupCommand(f client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "backup", - Short: "Work with non-admin backups", - Long: "Work with non-admin backups", - } - - c.AddCommand( - NewCreateCommand(f, "create"), - NewGetCommand(f, "get"), - NewLogsCommand(f, "logs"), - NewDescribeCommand(f, "describe"), - NewDeleteCommand(f, "delete"), - ) - - return c -} diff --git a/cmd/non-admin/backup/backup_test.go b/cmd/non-admin/backup/backup_test.go deleted file mode 100644 index 728d67f5b..000000000 --- a/cmd/non-admin/backup/backup_test.go +++ /dev/null @@ -1,534 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 backup - -import ( - "testing" - - "github.com/migtools/oadp-cli/internal/testutil" -) - -// TestNonAdminBackupCommands tests the non-admin backup command functionality -func TestNonAdminBackupCommands(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - tests := []struct { - name string - args []string - expectContains []string - }{ - { - name: "nonadmin backup help", - args: []string{"nonadmin", "backup", "--help"}, - expectContains: []string{ - "Work with non-admin backups", - "create", - "describe", - "delete", - "get", - "logs", - }, - }, - { - name: "nonadmin backup create help", - args: []string{"nonadmin", "backup", "create", "--help"}, - expectContains: []string{ - "Create a non-admin backup", - "--storage-location", - "--include-resources", - "--exclude-resources", - }, - }, - { - name: "nonadmin backup describe help", - args: []string{"nonadmin", "backup", "describe", "--help"}, - expectContains: []string{ - "Describe a non-admin backup", - }, - }, - { - name: "nonadmin backup delete help", - args: []string{"nonadmin", "backup", "delete", "--help"}, - expectContains: []string{ - "Delete one or more non-admin backups", - "--confirm", - }, - }, - { - name: "nonadmin backup get help", - args: []string{"nonadmin", "backup", "get", "--help"}, - expectContains: []string{ - "Get one or more non-admin backups", - }, - }, - { - name: "nonadmin backup logs help", - args: []string{"nonadmin", "backup", "logs", "--help"}, - expectContains: []string{ - "Show logs for a non-admin backup", - }, - }, - { - name: "na backup shorthand help", - args: []string{"na", "backup", "--help"}, - expectContains: []string{ - "Work with non-admin backups", - "create", - "describe", - "delete", - "get", - "logs", - }, - }, - // Verb-noun order help command tests - { - name: "nonadmin get backup help", - args: []string{"nonadmin", "get", "backup", "--help"}, - expectContains: []string{ - "Get one or more non-admin backups", - }, - }, - { - name: "nonadmin create backup help", - args: []string{"nonadmin", "create", "backup", "--help"}, - expectContains: []string{ - "Create a non-admin backup", - }, - }, - { - name: "nonadmin delete backup help", - args: []string{"nonadmin", "delete", "backup", "--help"}, - expectContains: []string{ - "Delete one or more non-admin backups", - }, - }, - { - name: "nonadmin describe backup help", - args: []string{"nonadmin", "describe", "backup", "--help"}, - expectContains: []string{ - "Describe a non-admin backup", - }, - }, - { - name: "nonadmin logs backup help", - args: []string{"nonadmin", "logs", "backup", "--help"}, - expectContains: []string{ - "Show logs for a non-admin backup", - }, - }, - // Shorthand verb-noun order tests - { - name: "na get backup help", - args: []string{"na", "get", "backup", "--help"}, - expectContains: []string{ - "Get one or more non-admin backups", - }, - }, - { - name: "na create backup help", - args: []string{"na", "create", "backup", "--help"}, - expectContains: []string{ - "Create a non-admin backup", - }, - }, - { - name: "na delete backup help", - args: []string{"na", "delete", "backup", "--help"}, - expectContains: []string{ - "Delete one or more non-admin backups", - }, - }, - { - name: "na describe backup help", - args: []string{"na", "describe", "backup", "--help"}, - expectContains: []string{ - "Describe a non-admin backup", - }, - }, - { - name: "na logs backup help", - args: []string{"na", "logs", "backup", "--help"}, - expectContains: []string{ - "Show logs for a non-admin backup", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, tt.args, tt.expectContains) - }) - } -} - -// TestNonAdminBackupHelpFlags tests that both --help and -h work for backup commands -func TestNonAdminBackupHelpFlags(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - commands := [][]string{ - {"nonadmin", "backup", "--help"}, - {"nonadmin", "backup", "-h"}, - {"nonadmin", "backup", "create", "--help"}, - {"nonadmin", "backup", "create", "-h"}, - {"nonadmin", "backup", "describe", "--help"}, - {"nonadmin", "backup", "describe", "-h"}, - {"nonadmin", "backup", "delete", "--help"}, - {"nonadmin", "backup", "delete", "-h"}, - {"nonadmin", "backup", "get", "--help"}, - {"nonadmin", "backup", "get", "-h"}, - {"nonadmin", "backup", "logs", "--help"}, - {"nonadmin", "backup", "logs", "-h"}, - {"na", "backup", "--help"}, - {"na", "backup", "-h"}, - // Verb-noun order help flags - {"nonadmin", "get", "backup", "--help"}, - {"nonadmin", "get", "backup", "-h"}, - {"nonadmin", "create", "backup", "--help"}, - {"nonadmin", "create", "backup", "-h"}, - {"nonadmin", "delete", "backup", "--help"}, - {"nonadmin", "delete", "backup", "-h"}, - {"nonadmin", "describe", "backup", "--help"}, - {"nonadmin", "describe", "backup", "-h"}, - {"nonadmin", "logs", "backup", "--help"}, - {"nonadmin", "logs", "backup", "-h"}, - // Shorthand verb-noun order help flags - {"na", "get", "backup", "--help"}, - {"na", "get", "backup", "-h"}, - {"na", "create", "backup", "--help"}, - {"na", "create", "backup", "-h"}, - {"na", "delete", "backup", "--help"}, - {"na", "delete", "backup", "-h"}, - {"na", "describe", "backup", "--help"}, - {"na", "describe", "backup", "-h"}, - {"na", "logs", "backup", "--help"}, - {"na", "logs", "backup", "-h"}, - } - - for _, cmd := range commands { - t.Run("help_flags_"+cmd[len(cmd)-1], func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{"Usage:"}) - }) - } -} - -// TestNonAdminBackupCreateFlags tests create command specific flags -func TestNonAdminBackupCreateFlags(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("create command has all expected MVP flags", func(t *testing.T) { - expectedFlags := []string{ - "--include-resources", - "--exclude-resources", - "--selector", - "--or-selector", - "--include-cluster-resources", - "--ttl", - "--storage-location", - "--csi-snapshot-timeout", - "--item-operation-timeout", - "--snapshot-volumes", - "--snapshot-move-data", - "--default-volumes-to-fs-backup", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "create", "--help"}, - expectedFlags) - }) -} - -// TestNonAdminBackupExamples tests that help text contains proper examples -func TestNonAdminBackupExamples(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("create examples use correct command format", func(t *testing.T) { - expectedExamples := []string{ - "oc oadp nonadmin backup create", - "--storage-location", - "--include-resources", - "--exclude-resources", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "create", "--help"}, - expectedExamples) - }) - - t.Run("main backup help shows subcommands", func(t *testing.T) { - expectedSubcommands := []string{ - "create", - "delete", - "describe", - "get", - "logs", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "--help"}, - expectedSubcommands) - }) -} - -// TestNonAdminBackupClientConfigIntegration tests that backup commands respect client config -func TestNonAdminBackupClientConfigIntegration(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - _, cleanup := testutil.SetupTempHome(t) - defer cleanup() - - t.Run("backup commands work with client config", func(t *testing.T) { - // Set a known namespace - _, err := testutil.RunCommand(t, binaryPath, "client", "config", "set", "namespace=user-namespace") - if err != nil { - t.Fatalf("Failed to set client config: %v", err) - } - - // Test that backup commands can be invoked (they should respect the namespace) - // We test help commands since they don't require actual K8s resources - commands := [][]string{ - {"nonadmin", "backup", "get", "--help"}, - {"nonadmin", "backup", "create", "--help"}, - {"nonadmin", "backup", "describe", "--help"}, - {"nonadmin", "backup", "delete", "--help"}, - {"nonadmin", "backup", "logs", "--help"}, - {"na", "backup", "get", "--help"}, - // Verb-noun order commands - {"nonadmin", "get", "backup", "--help"}, - {"nonadmin", "create", "backup", "--help"}, - {"nonadmin", "describe", "backup", "--help"}, - {"nonadmin", "delete", "backup", "--help"}, - {"nonadmin", "logs", "backup", "--help"}, - {"na", "get", "backup", "--help"}, - {"na", "create", "backup", "--help"}, - } - - for _, cmd := range commands { - t.Run("config_test_"+cmd[len(cmd)-2], func(t *testing.T) { - output, err := testutil.RunCommand(t, binaryPath, cmd...) - if err != nil { - t.Fatalf("Non-admin backup command should work with client config: %v", err) - } - if output == "" { - t.Errorf("Expected help output for %v", cmd) - } - }) - } - }) -} - -// TestNonAdminBackupCommandStructure tests the overall command structure -func TestNonAdminBackupCommandStructure(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("backup commands available under nonadmin", func(t *testing.T) { - _, err := testutil.RunCommand(t, binaryPath, "nonadmin", "--help") - if err != nil { - t.Fatalf("nonadmin command should exist: %v", err) - } - - expectedCommands := []string{"backup"} - for _, cmd := range expectedCommands { - testutil.TestHelpCommand(t, binaryPath, []string{"nonadmin", "--help"}, []string{cmd}) - } - }) - - t.Run("backup commands available under na shorthand", func(t *testing.T) { - _, err := testutil.RunCommand(t, binaryPath, "na", "--help") - if err != nil { - t.Fatalf("na command should exist: %v", err) - } - - expectedCommands := []string{"backup"} - for _, cmd := range expectedCommands { - testutil.TestHelpCommand(t, binaryPath, []string{"na", "--help"}, []string{cmd}) - } - }) -} - -// TestVerbNounOrderExamples tests that verb-noun order commands show proper examples -func TestVerbNounOrderExamples(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("verb commands show proper examples", func(t *testing.T) { - // Test that verb commands show examples with oc oadp prefix - expectedExamples := []string{ - "oc oadp nonadmin get backup", - "oc oadp nonadmin create backup", - "oc oadp nonadmin delete backup", - "oc oadp nonadmin describe backup", - "oc oadp nonadmin logs backup", - } - - commands := [][]string{ - {"nonadmin", "get", "--help"}, - {"nonadmin", "create", "--help"}, - {"nonadmin", "delete", "--help"}, - {"nonadmin", "describe", "--help"}, - {"nonadmin", "logs", "--help"}, - } - - for i, cmd := range commands { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{expectedExamples[i]}) - } - }) - - t.Run("verb commands with specific resources show proper examples", func(t *testing.T) { - // Test that verb commands with specific resources show examples (noun-first format from underlying commands) - expectedExamples := []string{ - "oc oadp nonadmin backup get", - "oc oadp nonadmin backup create backup1", - "oc oadp nonadmin backup delete my-backup", - "oc oadp nonadmin backup describe my-backup", - "oc oadp nonadmin backup logs my-backup", - } - - commands := [][]string{ - {"nonadmin", "get", "backup", "--help"}, - {"nonadmin", "create", "backup", "--help"}, - {"nonadmin", "delete", "backup", "--help"}, - {"nonadmin", "describe", "backup", "--help"}, - {"nonadmin", "logs", "backup", "--help"}, - } - - for i, cmd := range commands { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{expectedExamples[i]}) - } - }) - - t.Run("shorthand verb commands show proper examples", func(t *testing.T) { - // Test that shorthand verb commands show examples - expectedExamples := []string{ - "oc oadp nonadmin get backup", - "oc oadp nonadmin create backup", - "oc oadp nonadmin delete backup", - "oc oadp nonadmin describe backup", - "oc oadp nonadmin logs backup", - } - - commands := [][]string{ - {"na", "get", "--help"}, - {"na", "create", "--help"}, - {"na", "delete", "--help"}, - {"na", "describe", "--help"}, - {"na", "logs", "--help"}, - } - - for i, cmd := range commands { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{expectedExamples[i]}) - } - }) -} - -// TestNonAdminBackupDeleteAllFlag tests the --all flag functionality added in commit 6a112249d91ab5411d199b848cae1c97fccea655 -// AI-generated test -func TestNonAdminBackupDeleteAllFlag(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("delete help shows --all flag", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "delete", "--help"}, - []string{"--all"}) - }) - - t.Run("delete help shows --all in usage", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "delete", "--help"}, - []string{"[NAME...] | --all"}) - }) - - t.Run("delete help describes --all flag purpose", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "delete", "--help"}, - []string{"Delete all backups in the current namespace"}) - }) - - t.Run("delete --all flag description in long help", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "delete", "--help"}, - []string{"Use --all to delete all backups in the current namespace"}) - }) - - t.Run("verb-noun delete help shows --all flag", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "delete", "backup", "--help"}, - []string{"--all"}) - }) - - t.Run("shorthand na delete backup help shows --all flag", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"na", "delete", "backup", "--help"}, - []string{"--all"}) - }) - - t.Run("shorthand na backup delete help shows --all flag", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"na", "backup", "delete", "--help"}, - []string{"--all"}) - }) -} - -// TestNonAdminBackupDeleteAllFlagExamples tests that examples mention the --all flag -// AI-generated test -func TestNonAdminBackupDeleteAllFlagExamples(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("delete help documents both usage patterns", func(t *testing.T) { - // Test that help shows both ways to use the command - expectedPatterns := []string{ - "--all", // The flag itself - "--confirm", // The confirmation skip flag - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "delete", "--help"}, - expectedPatterns) - }) - - t.Run("delete help has examples section", func(t *testing.T) { - // Test that examples section exists and shows various delete patterns - expectedExamples := []string{ - "oc oadp nonadmin backup delete my-backup", - "oc oadp nonadmin backup delete --all", - "oc oadp nonadmin backup delete my-backup --confirm", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "delete", "--help"}, - expectedExamples) - }) -} - -// TestNonAdminBackupDeleteAllFlagBehavior tests the behavioral aspects of the --all flag -// AI-generated test -func TestNonAdminBackupDeleteAllFlagBehavior(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("delete with --all requires no backup names", func(t *testing.T) { - // The --all flag should accept no arguments - // This is a validation that the Args function works correctly - // We expect help output to show: [NAME...] | --all - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "delete", "--help"}, - []string{"[NAME...] | --all"}) - }) - - t.Run("delete help mentions namespace context", func(t *testing.T) { - // The --all flag deletes in current namespace, should be documented - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "backup", "delete", "--help"}, - []string{"current namespace"}) - }) -} diff --git a/cmd/non-admin/backup/create.go b/cmd/non-admin/backup/create.go deleted file mode 100644 index 405879159..000000000 --- a/cmd/non-admin/backup/create.go +++ /dev/null @@ -1,271 +0,0 @@ -package backup - -/* -Copyright The Velero Contributors. - -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. -*/ - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - "github.com/vmware-tanzu/velero/pkg/builder" - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd" - velerobackup "github.com/vmware-tanzu/velero/pkg/cmd/cli/backup" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" -) - -func NewCreateCommand(f client.Factory, use string) *cobra.Command { - o := NewCreateOptions() - - c := &cobra.Command{ - Use: use + " NAME", - Short: "Create a non-admin backup", - Args: cobra.ExactArgs(1), - Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(args, f)) - cmd.CheckError(o.Validate(c, args, f)) - cmd.CheckError(o.Run(c, f)) - }, - Example: ` # Create a simple backup of all resources in the current namespace. - oc oadp nonadmin backup create backup1 - - # Create a backup with specific resource types. - oc oadp nonadmin backup create backup2 --include-resources deployments,services - - # Create a backup with label selector. - oc oadp nonadmin backup create backup3 --selector app=myapp - - # Create a backup with snapshots and TTL. - oc oadp nonadmin backup create backup4 --snapshot-volumes --ttl 720h - - # Create a backup with specific storage location. - oc oadp nonadmin backup create backup5 --storage-location my-nabsl - - # Set default storage location for all backups. - oc oadp client config set default-nabsl=my-nabsl - - # View the YAML for a backup without sending it to the server. - oc oadp nonadmin backup create backup6 -o yaml`, - } - - o.BindFlags(c.Flags()) - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -type CreateOptions struct { - *velerobackup.CreateOptions // Embed Velero's CreateOptions - - // NAB-specific fields - Name string // The NonAdminBackup resource name (maps to Velero's BackupName) - client kbclient.WithWatch - currentNamespace string - storageLocationFromConfig bool // Track if storage location came from config -} - -func NewCreateOptions() *CreateOptions { - return &CreateOptions{ - CreateOptions: velerobackup.NewCreateOptions(), - } -} - -func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { - // Resource filtering (MVP) - flags.Var(&o.IncludeResources, "include-resources", "Resources to include in the backup, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources).") - flags.Var(&o.ExcludeResources, "exclude-resources", "Resources to exclude from the backup, formatted as resource.group, such as storageclasses.storage.k8s.io.") - - // Label selection (MVP) - flags.VarP(&o.Selector, "selector", "l", "Only back up resources matching this label selector.") - flags.Var(&o.OrSelector, "or-selector", "Backup resources matching at least one of the label selector from the list. Label selectors should be separated by ' or '. For example, foo=bar or app=nginx") - - // Cluster resources (MVP) - f := flags.VarPF(&o.IncludeClusterResources, "include-cluster-resources", "", "Include cluster-scoped resources in the backup.") - f.NoOptDefVal = cmd.TRUE - - // Timing/Storage (MVP) - flags.DurationVar(&o.TTL, "ttl", o.TTL, "How long before the backup can be garbage collected.") - flags.StringVar(&o.StorageLocation, "storage-location", "", "Location in which to store the backup. Uses config 'default-nabsl' if not specified.") - flags.DurationVar(&o.CSISnapshotTimeout, "csi-snapshot-timeout", o.CSISnapshotTimeout, "How long to wait for CSI snapshot creation before timeout.") - flags.DurationVar(&o.ItemOperationTimeout, "item-operation-timeout", o.ItemOperationTimeout, "How long to wait for async plugin operations before timeout.") - - // Snapshot control (MVP) - f = flags.VarPF(&o.SnapshotVolumes, "snapshot-volumes", "", "Take snapshots of PersistentVolumes as part of the backup.") - f.NoOptDefVal = cmd.TRUE - - f = flags.VarPF(&o.SnapshotMoveData, "snapshot-move-data", "", "Specify whether snapshot data should be moved.") - f.NoOptDefVal = cmd.TRUE - - f = flags.VarPF(&o.DefaultVolumesToFsBackup, "default-volumes-to-fs-backup", "", "Use pod volume file system backup by default for volumes.") - f.NoOptDefVal = cmd.TRUE -} - -func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Factory) error { - if err := output.ValidateFlags(c); err != nil { - return err - } - - if len(args) != 1 { - return fmt.Errorf("a backup name is required") - } - - if o.Selector.LabelSelector != nil && o.OrSelector.OrLabelSelectors != nil { - return fmt.Errorf("either a 'selector' or an 'or-selector' can be specified, but not both") - } - - // Storage location validation - if o.StorageLocation == "" { - return fmt.Errorf("--storage-location is required\n" + - "To avoid specifying the storage location each time:\n" + - "run `oc oadp client config set default-nabsl=` to set the default storage location") - } - - return nil -} - -func (o *CreateOptions) Complete(args []string, f client.Factory) error { - o.Name = args[0] - - // Load default NABSL from config if not provided via flag - if o.StorageLocation == "" { - defaultNABSL := getNABSLFromConfig() - if defaultNABSL != "" { - o.StorageLocation = defaultNABSL - o.storageLocationFromConfig = true - } - } - - // Create client with NonAdmin scheme - client, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeNonAdminTypes: true, - }) - if err != nil { - return err - } - - // Get the current namespace from kubeconfig instead of using factory namespace - currentNS, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - - o.client = client - o.currentNamespace = currentNS - return nil -} - -func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { - nonAdminBackup, err := o.BuildNonAdminBackup(o.currentNamespace) - if err != nil { - return err - } - - if printed, err := output.PrintWithFormat(c, nonAdminBackup); printed || err != nil { - return err - } - - // Create the backup - if err := o.client.Create(context.TODO(), nonAdminBackup, &kbclient.CreateOptions{}); err != nil { - return err - } - - if o.storageLocationFromConfig { - fmt.Printf("Using default nonadmin backup storage location from config: %s\n", o.StorageLocation) - } - - fmt.Printf("NonAdminBackup request %q submitted successfully.\n", nonAdminBackup.Name) - fmt.Printf("Run `oc oadp nonadmin backup describe %s` or `oc oadp nonadmin backup logs %s` for more details.\n", nonAdminBackup.Name, nonAdminBackup.Name) - fmt.Println() - - return nil -} - -func (o *CreateOptions) BuildNonAdminBackup(namespace string) (*nacv1alpha1.NonAdminBackup, error) { - backupSpec, err := o.buildBackupSpecFromOptions(namespace) - if err != nil { - return nil, err - } - - return o.createNonAdminBackup(namespace, backupSpec), nil -} - -// buildBackupSpecFromOptions creates a BackupSpec from command line options -func (o *CreateOptions) buildBackupSpecFromOptions(namespace string) (*velerov1api.BackupSpec, error) { - backupBuilder := builder.ForBackup(namespace, o.Name). - IncludedNamespaces(namespace). // Automatically include the current namespace - IncludedResources(o.IncludeResources...). - ExcludedResources(o.ExcludeResources...). - LabelSelector(o.Selector.LabelSelector). - OrLabelSelector(o.OrSelector.OrLabelSelectors). - TTL(o.TTL). - StorageLocation(o.StorageLocation). - CSISnapshotTimeout(o.CSISnapshotTimeout). - ItemOperationTimeout(o.ItemOperationTimeout) - - if err := o.applyOptionalBackupOptions(backupBuilder); err != nil { - return nil, err - } - - tempBackup := backupBuilder.Result() - - return &tempBackup.Spec, nil -} - -// applyOptionalBackupOptions applies optional flags to the backup builder -func (o *CreateOptions) applyOptionalBackupOptions(backupBuilder *builder.BackupBuilder) error { - if o.SnapshotVolumes.Value != nil { - backupBuilder.SnapshotVolumes(*o.SnapshotVolumes.Value) - } - if o.SnapshotMoveData.Value != nil { - backupBuilder.SnapshotMoveData(*o.SnapshotMoveData.Value) - } - if o.IncludeClusterResources.Value != nil { - backupBuilder.IncludeClusterResources(*o.IncludeClusterResources.Value) - } - if o.DefaultVolumesToFsBackup.Value != nil { - backupBuilder.DefaultVolumesToFsBackup(*o.DefaultVolumesToFsBackup.Value) - } - - return nil -} - -// createNonAdminBackup creates the NonAdminBackup CR from a BackupSpec -func (o *CreateOptions) createNonAdminBackup(namespace string, backupSpec *velerov1api.BackupSpec) *nacv1alpha1.NonAdminBackup { - return NewNonAdminBackupBuilder(namespace, o.Name). - BackupSpec(nacv1alpha1.NonAdminBackupSpec{ - BackupSpec: backupSpec, - }). - Result() -} - -func getNABSLFromConfig() string { - clientConfig, err := shared.ReadVeleroClientConfig() - if err == nil && clientConfig != nil { - defaultNABSL := clientConfig.GetDefaultNABSL() - if defaultNABSL != "" { - return defaultNABSL - } - } - return "" -} diff --git a/cmd/non-admin/backup/delete.go b/cmd/non-admin/backup/delete.go deleted file mode 100644 index f66a71f06..000000000 --- a/cmd/non-admin/backup/delete.go +++ /dev/null @@ -1,308 +0,0 @@ -package backup - -/* -Copyright The Velero Contributors. - -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. -*/ - -import ( - "bufio" - "context" - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "k8s.io/apimachinery/pkg/api/errors" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" -) - -// NewDeleteCommand creates a cobra command for deleting non-admin backups -func NewDeleteCommand(f client.Factory, use string) *cobra.Command { - o := NewDeleteOptions() - - c := &cobra.Command{ - Use: use + " [NAME...] | --all", - Short: "Delete one or more non-admin backups", - Long: "Delete one or more non-admin backups by setting the deletebackup field to true. Use --all to delete all backups in the current namespace.", - Args: func(cmd *cobra.Command, args []string) error { - // Check if --all flag is set - allFlag, _ := cmd.Flags().GetBool("all") - if allFlag { - return cobra.NoArgs(cmd, args) - } - return cobra.MinimumNArgs(1)(cmd, args) - }, - Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(args, f)) - cmd.CheckError(o.Validate()) - cmd.CheckError(o.Run()) - }, - Example: ` # Delete a specific backup - oc oadp nonadmin backup delete my-backup - - # Delete multiple backups - oc oadp nonadmin backup delete backup1 backup2 backup3 - - # Delete all backups in the current namespace - oc oadp nonadmin backup delete --all - - # Delete without confirmation prompt - oc oadp nonadmin backup delete my-backup --confirm`, - } - - o.BindFlags(c.Flags()) - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -// DeleteOptions holds the options for the delete command -type DeleteOptions struct { - Names []string - Namespace string // Internal field - automatically determined from kubectl context - Confirm bool // Skip confirmation prompt - All bool // Delete all backups in namespace - client kbclient.Client -} - -// NewDeleteOptions creates a new DeleteOptions instance -func NewDeleteOptions() *DeleteOptions { - return &DeleteOptions{} -} - -// BindFlags binds the command line flags to the options -func (o *DeleteOptions) BindFlags(flags *pflag.FlagSet) { - flags.BoolVar(&o.Confirm, "confirm", false, "Skip confirmation prompt and delete immediately") - flags.BoolVar(&o.All, "all", false, "Delete all backups in the current namespace") -} - -// Complete completes the options by setting up the client and determining the namespace -func (o *DeleteOptions) Complete(args []string, f client.Factory) error { - o.Names = args - - // Create client with NonAdmin scheme - kbClient, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeNonAdminTypes: true, - }) - if err != nil { - return err - } - - o.client = kbClient - - // Always use the current namespace from kubectl context - currentNS, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - o.Namespace = currentNS - - // If --all flag is used, list all backups in the namespace - if o.All { - var nabList nacv1alpha1.NonAdminBackupList - err := o.client.List(context.TODO(), &nabList, &kbclient.ListOptions{ - Namespace: o.Namespace, - }) - if err != nil { - return fmt.Errorf("failed to list backups: %w", err) - } - - // Extract backup names - o.Names = make([]string, 0, len(nabList.Items)) - for _, nab := range nabList.Items { - o.Names = append(o.Names, nab.Name) - } - - if len(o.Names) == 0 { - return fmt.Errorf("no backups found in namespace '%s'", o.Namespace) - } - } - - return nil -} - -// Validate validates the options -func (o *DeleteOptions) Validate() error { - if !o.All && len(o.Names) == 0 { - return fmt.Errorf("at least one backup name is required, or use --all to delete all backups") - } - if o.Namespace == "" { - return fmt.Errorf("namespace is required") - } - return nil -} - -// Run executes the delete command -func (o *DeleteOptions) Run() error { - // Show what will be deleted - if o.All { - fmt.Printf("All NonAdminBackup(s) in namespace '%s' will be marked for deletion:\n", o.Namespace) - } else { - fmt.Printf("The following NonAdminBackup(s) will be marked for deletion in namespace '%s':\n", o.Namespace) - } - for _, name := range o.Names { - fmt.Printf(" - %s\n", name) - } - fmt.Println() - - // Prompt for confirmation unless --confirm flag is used - if !o.Confirm { - confirmed, err := o.promptForConfirmation() - if err != nil { - return err - } - if !confirmed { - fmt.Println("Deletion cancelled.") - return nil - } - } - - // Track results - var successful []string - var failed []string - - // Process each backup - for _, name := range o.Names { - err := o.deleteBackup(name) - if err != nil { - fmt.Printf("❌ Failed to mark %s for deletion: %v\n", name, err) - failed = append(failed, name) - } else { - fmt.Printf("✓ %s marked for deletion\n", name) - successful = append(successful, name) - } - } - - // Print summary - fmt.Println() - if len(successful) > 0 { - fmt.Printf("Successfully marked %d backup(s) for deletion:\n", len(successful)) - for _, name := range successful { - fmt.Printf(" - %s\n", name) - } - fmt.Println() - fmt.Println("ℹ️ Note: The actual backup deletion will be performed asynchronously by the OADP controller.") - fmt.Println(" This may take some time to complete. You can monitor progress with:") - fmt.Printf(" oc get nonadminbackup -n %s\n", o.Namespace) - } - - if len(failed) > 0 { - fmt.Printf("Failed to mark %d backup(s) for deletion:\n", len(failed)) - for _, name := range failed { - fmt.Printf(" - %s\n", name) - } - return fmt.Errorf("some operations failed") - } - - return nil -} - -// promptForConfirmation prompts the user for confirmation -func (o *DeleteOptions) promptForConfirmation() (bool, error) { - reader := bufio.NewReader(os.Stdin) - - if o.All { - fmt.Printf("Are you sure you want to delete ALL %d backup(s) in namespace '%s'? (y/N): ", len(o.Names), o.Namespace) - } else if len(o.Names) == 1 { - fmt.Printf("Are you sure you want to delete backup '%s'? (y/N): ", o.Names[0]) - } else { - fmt.Printf("Are you sure you want to delete these %d backups? (y/N): ", len(o.Names)) - } - - response, err := reader.ReadString('\n') - if err != nil { - return false, fmt.Errorf("failed to read user input: %w", err) - } - - response = strings.TrimSpace(strings.ToLower(response)) - return response == "y" || response == "yes", nil -} - -// deleteBackup deletes a single backup -func (o *DeleteOptions) deleteBackup(name string) error { - // Get the NonAdminBackup resource - nab := &nacv1alpha1.NonAdminBackup{} - err := o.client.Get(context.TODO(), kbclient.ObjectKey{ - Name: name, - Namespace: o.Namespace, - }, nab) - if err != nil { - return o.translateError(name, err) - } - - // Set the deletebackup field to true - nab.Spec.DeleteBackup = true - - // Update the resource - err = o.client.Update(context.TODO(), nab) - if err != nil { - return o.translateError(name, err) - } - - return nil -} - -// translateError converts verbose Kubernetes errors into user-friendly messages -func (o *DeleteOptions) translateError(name string, err error) error { - if errors.IsNotFound(err) { - return fmt.Errorf("backup '%s' not found", name) - } - - if errors.IsForbidden(err) { - return fmt.Errorf("permission denied") - } - - if errors.IsUnauthorized(err) { - return fmt.Errorf("authentication required") - } - - if errors.IsConflict(err) { - return fmt.Errorf("backup '%s' was modified, please try again", name) - } - - if errors.IsTimeout(err) { - return fmt.Errorf("request timed out") - } - - if errors.IsServerTimeout(err) { - return fmt.Errorf("server timeout") - } - - if errors.IsServiceUnavailable(err) { - return fmt.Errorf("service unavailable") - } - - // Check for common connection issues - errStr := err.Error() - if strings.Contains(errStr, "connection refused") { - return fmt.Errorf("cannot connect to cluster") - } - - if strings.Contains(errStr, "no such host") { - return fmt.Errorf("cannot reach cluster") - } - - // For any other error, provide a generic message - return fmt.Errorf("operation failed") -} diff --git a/cmd/non-admin/backup/describe.go b/cmd/non-admin/backup/describe.go deleted file mode 100644 index 68cebaed8..000000000 --- a/cmd/non-admin/backup/describe.go +++ /dev/null @@ -1,817 +0,0 @@ -package backup - -import ( - "context" - "encoding/json" - "fmt" - "sort" - "strings" - "time" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" - "gopkg.in/yaml.v2" - corev1 "k8s.io/api/core/v1" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -func NewDescribeCommand(f client.Factory, use string) *cobra.Command { - var ( - requestTimeout time.Duration - details bool - ) - - c := &cobra.Command{ - Use: use + " NAME", - Short: "Describe a non-admin backup", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - backupName := args[0] - - // Get effective timeout (flag takes precedence over env var) - effectiveTimeout := shared.GetHTTPTimeoutWithOverride(requestTimeout) - - // Create context with the effective timeout - ctx, cancel := context.WithTimeout(context.Background(), effectiveTimeout) - defer cancel() - - // Get the current namespace from kubectl context - userNamespace, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - - // Create client with required scheme types and timeout - kbClient, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeNonAdminTypes: true, - IncludeVeleroTypes: true, - IncludeCoreTypes: true, - Timeout: effectiveTimeout, - }) - if err != nil { - return err - } - - // Get the specific backup - var nab nacv1alpha1.NonAdminBackup - if err := kbClient.Get(ctx, kbclient.ObjectKey{ - Namespace: userNamespace, - Name: backupName, - }, &nab); err != nil { - // Check for context cancellation - if ctx.Err() == context.DeadlineExceeded { - return fmt.Errorf("timed out after %v getting NonAdminBackup %q", effectiveTimeout, backupName) - } - if ctx.Err() == context.Canceled { - return fmt.Errorf("operation cancelled: %w", ctx.Err()) - } - return fmt.Errorf("NonAdminBackup %q not found in namespace %q: %w", backupName, userNamespace, err) - } - - // Print in Velero-style format - printNonAdminBackupDetails(cmd, &nab, kbClient, backupName, userNamespace, effectiveTimeout, details) - - // Add detailed output if --details flag is set - if details { - if err := printDetailedBackupInfo(cmd, kbClient, backupName, userNamespace, effectiveTimeout); err != nil { - return fmt.Errorf("failed to fetch detailed backup information: %w", err) - } - } - - return nil - }, - Example: ` oc oadp nonadmin backup describe my-backup - oc oadp nonadmin backup describe my-backup --details - oc oadp nonadmin backup describe my-backup --details --request-timeout=30m`, - } - - c.Flags().DurationVar(&requestTimeout, "request-timeout", 0, fmt.Sprintf("The length of time to wait before giving up on a single server request (e.g., 30s, 5m, 1h). Overrides %s env var. Default: %v", shared.TimeoutEnvVar, shared.DefaultHTTPTimeout)) - c.Flags().BoolVar(&details, "details", false, "Display additional backup details including volume snapshots, resource lists, and item operations") - - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -// printNonAdminBackupDetails prints backup details in Velero admin describe format -func printNonAdminBackupDetails(cmd *cobra.Command, nab *nacv1alpha1.NonAdminBackup, kbClient kbclient.Client, backupName string, userNamespace string, timeout time.Duration, showDetails bool) { - out := cmd.OutOrStdout() - - // Get Velero backup reference if available - var vb *nacv1alpha1.VeleroBackup - if nab.Status.VeleroBackup != nil { - vb = nab.Status.VeleroBackup - } - - // Name and Namespace - fmt.Fprintf(out, "Name: %s\n", nab.Name) - fmt.Fprintf(out, "Namespace: %s\n", nab.Namespace) - - // Labels - fmt.Fprintf(out, "Labels: ") - if len(nab.Labels) == 0 { - fmt.Fprintf(out, "\n") - } else { - labelKeys := make([]string, 0, len(nab.Labels)) - for k := range nab.Labels { - labelKeys = append(labelKeys, k) - } - sort.Strings(labelKeys) - for i, k := range labelKeys { - if i == 0 { - fmt.Fprintf(out, "%s=%s\n", k, nab.Labels[k]) - } else { - fmt.Fprintf(out, " %s=%s\n", k, nab.Labels[k]) - } - } - } - - // Annotations - fmt.Fprintf(out, "Annotations: ") - if len(nab.Annotations) == 0 { - fmt.Fprintf(out, "\n") - } else { - annotationKeys := make([]string, 0, len(nab.Annotations)) - for k := range nab.Annotations { - annotationKeys = append(annotationKeys, k) - } - sort.Strings(annotationKeys) - for i, k := range annotationKeys { - if i == 0 { - fmt.Fprintf(out, "%s=%s\n", k, nab.Annotations[k]) - } else { - fmt.Fprintf(out, " %s=%s\n", k, nab.Annotations[k]) - } - } - } - - fmt.Fprintf(out, "\n") - - // Phase (with color) - phase := string(nab.Status.Phase) - if vb != nil && vb.Status != nil && vb.Status.Phase != "" { - phase = string(vb.Status.Phase) - } - fmt.Fprintf(out, "Phase: %s\n", colorizePhase(phase)) - - fmt.Fprintf(out, "\n") - - // Backup Spec details - if nab.Spec.BackupSpec != nil { - spec := nab.Spec.BackupSpec - - // Namespaces - fmt.Fprintf(out, "Namespaces:\n") - if len(spec.IncludedNamespaces) == 0 { - fmt.Fprintf(out, " Included: *\n") - } else { - fmt.Fprintf(out, " Included: %s\n", strings.Join(spec.IncludedNamespaces, ", ")) - } - if len(spec.ExcludedNamespaces) == 0 { - fmt.Fprintf(out, " Excluded: \n") - } else { - fmt.Fprintf(out, " Excluded: %s\n", strings.Join(spec.ExcludedNamespaces, ", ")) - } - - fmt.Fprintf(out, "\n") - - // Resources - fmt.Fprintf(out, "Resources:\n") - if len(spec.IncludedResources) == 0 { - fmt.Fprintf(out, " Included: *\n") - } else { - fmt.Fprintf(out, " Included: %s\n", strings.Join(spec.IncludedResources, ", ")) - } - if len(spec.ExcludedResources) == 0 { - fmt.Fprintf(out, " Excluded: \n") - } else { - fmt.Fprintf(out, " Excluded: %s\n", strings.Join(spec.ExcludedResources, ", ")) - } - if spec.IncludeClusterResources != nil { - if *spec.IncludeClusterResources { - fmt.Fprintf(out, " Cluster-scoped: included\n") - } else { - fmt.Fprintf(out, " Cluster-scoped: excluded\n") - } - } else { - fmt.Fprintf(out, " Cluster-scoped: auto\n") - } - - fmt.Fprintf(out, "\n") - - // Label selector - if spec.LabelSelector != nil && len(spec.LabelSelector.MatchLabels) > 0 { - var selectorParts []string - for k, v := range spec.LabelSelector.MatchLabels { - selectorParts = append(selectorParts, fmt.Sprintf("%s=%s", k, v)) - } - fmt.Fprintf(out, "Label selector: %s\n", strings.Join(selectorParts, ",")) - } else { - fmt.Fprintf(out, "Label selector: \n") - } - - fmt.Fprintf(out, "\n") - fmt.Fprintf(out, "Or label selector: \n") - fmt.Fprintf(out, "\n") - - // Storage Location - if spec.StorageLocation != "" { - fmt.Fprintf(out, "Storage Location: %s\n", spec.StorageLocation) - } else { - fmt.Fprintf(out, "Storage Location: \n") - } - - fmt.Fprintf(out, "\n") - - // Snapshot settings - if spec.SnapshotVolumes != nil { - if *spec.SnapshotVolumes { - fmt.Fprintf(out, "Velero-Native Snapshot PVs: true\n") - } else { - fmt.Fprintf(out, "Velero-Native Snapshot PVs: false\n") - } - } else { - fmt.Fprintf(out, "Velero-Native Snapshot PVs: auto\n") - } - - if spec.SnapshotMoveData != nil && *spec.SnapshotMoveData { - fmt.Fprintf(out, "Snapshot Move Data: true\n") - } else { - fmt.Fprintf(out, "Snapshot Move Data: false\n") - } - - if spec.DataMover != "" { - fmt.Fprintf(out, "Data Mover: %s\n", spec.DataMover) - } else { - fmt.Fprintf(out, "Data Mover: velero\n") - } - - fmt.Fprintf(out, "\n") - - // TTL - if spec.TTL.Duration > 0 { - fmt.Fprintf(out, "TTL: %s\n", spec.TTL.Duration) - } else { - fmt.Fprintf(out, "TTL: 720h0m0s\n") // default - } - - fmt.Fprintf(out, "\n") - - // Timeouts - if spec.CSISnapshotTimeout.Duration > 0 { - fmt.Fprintf(out, "CSISnapshotTimeout: %s\n", spec.CSISnapshotTimeout.Duration) - } else { - fmt.Fprintf(out, "CSISnapshotTimeout: 10m0s\n") - } - - if spec.ItemOperationTimeout.Duration > 0 { - fmt.Fprintf(out, "ItemOperationTimeout: %s\n", spec.ItemOperationTimeout.Duration) - } else { - fmt.Fprintf(out, "ItemOperationTimeout: 4h0m0s\n") - } - - fmt.Fprintf(out, "\n") - - // Hooks - if len(spec.Hooks.Resources) > 0 { - fmt.Fprintf(out, "Hooks: %d resources with hooks\n", len(spec.Hooks.Resources)) - } else { - fmt.Fprintf(out, "Hooks: \n") - } - - fmt.Fprintf(out, "\n") - } - - // Velero backup status information - if vb != nil && vb.Status != nil { - status := vb.Status - - // Backup Format Version - if status.FormatVersion != "" { - fmt.Fprintf(out, "Backup Format Version: %s\n", status.FormatVersion) - } - - fmt.Fprintf(out, "\n") - - // Started and Completed times - if !status.StartTimestamp.IsZero() { - fmt.Fprintf(out, "Started: %s\n", status.StartTimestamp.Format("2006-01-02 15:04:05 -0700 MST")) - } - if !status.CompletionTimestamp.IsZero() { - fmt.Fprintf(out, "Completed: %s\n", status.CompletionTimestamp.Format("2006-01-02 15:04:05 -0700 MST")) - } - - fmt.Fprintf(out, "\n") - - // Expiration - if status.Expiration != nil { - fmt.Fprintf(out, "Expiration: %s\n", status.Expiration.Format("2006-01-02 15:04:05 -0700 MST")) - } - - fmt.Fprintf(out, "\n") - - // Progress - if status.Progress != nil { - fmt.Fprintf(out, "Total items to be backed up: %d\n", status.Progress.TotalItems) - fmt.Fprintf(out, "Items backed up: %d\n", status.Progress.ItemsBackedUp) - } - - fmt.Fprintf(out, "\n") - - // Fetch and display Resource List (only if showDetails is true) - if showDetails { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - resourceList, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: backupName, - DataType: "BackupResourceList", - Namespace: userNamespace, - HTTPTimeout: timeout, - }) - - if err == nil && resourceList != "" { - if formattedList := formatResourceList(resourceList); formattedList != "" { - fmt.Fprintf(out, "Resource List:\n") - fmt.Fprintf(out, "%s\n", formattedList) - fmt.Fprintf(out, "\n") - } - } - } - - // Backup Volumes - fmt.Fprintf(out, "Backup Volumes:\n") - - hasVeleroSnapshots := status.VolumeSnapshotsAttempted > 0 - if hasVeleroSnapshots { - fmt.Fprintf(out, " Velero-Native Snapshots: %d of %d snapshots completed successfully (specify --details for more information)\n", - status.VolumeSnapshotsCompleted, status.VolumeSnapshotsAttempted) - } else { - fmt.Fprintf(out, " Velero-Native Snapshots: \n") - } - - fmt.Fprintf(out, "\n") - - hasCSISnapshots := status.CSIVolumeSnapshotsAttempted > 0 - if hasCSISnapshots { - fmt.Fprintf(out, " CSI Snapshots: %d of %d snapshots completed successfully\n", - status.CSIVolumeSnapshotsCompleted, status.CSIVolumeSnapshotsAttempted) - } else { - fmt.Fprintf(out, " CSI Snapshots: \n") - } - - fmt.Fprintf(out, "\n") - - // Pod Volume Backups - fmt.Fprintf(out, " Pod Volume Backups: \n") - - fmt.Fprintf(out, "\n") - - // Hooks - if status.HookStatus != nil { - fmt.Fprintf(out, "HooksAttempted: %d\n", status.HookStatus.HooksAttempted) - fmt.Fprintf(out, "HooksFailed: %d\n", status.HookStatus.HooksFailed) - } else { - fmt.Fprintf(out, "HooksAttempted: \n") - fmt.Fprintf(out, "HooksFailed: \n") - } - } else { - // Velero backup not available yet - fmt.Fprintf(out, "Velero backup information not yet available.\n") - fmt.Fprintf(out, "Request Phase: %s\n", nab.Status.Phase) - } -} - -// printDetailedBackupInfo fetches and displays additional backup details when --details flag is used. -// It uses NonAdminDownloadRequest to fetch: -// - BackupVolumeInfos (snapshot details) -// - BackupResults (errors, warnings) -// - BackupItemOperations (plugin operations) -func printDetailedBackupInfo(cmd *cobra.Command, kbClient kbclient.Client, backupName string, userNamespace string, timeout time.Duration) error { - out := cmd.OutOrStdout() - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - hasOutput := false - - // 1. Fetch BackupVolumeInfos - volumeInfo, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: backupName, - DataType: "BackupVolumeInfos", - Namespace: userNamespace, - HTTPTimeout: timeout, - }) - - if err == nil && volumeInfo != "" { - if formattedInfo := formatVolumeInfo(volumeInfo); formattedInfo != "" { - if !hasOutput { - fmt.Fprintf(out, "\n") - hasOutput = true - } - fmt.Fprintf(out, "Volume Snapshot Details:\n") - fmt.Fprintf(out, "%s\n", formattedInfo) - fmt.Fprintf(out, "\n") - } - } - - // 2. Fetch BackupResults - results, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: backupName, - DataType: "BackupResults", - Namespace: userNamespace, - HTTPTimeout: timeout, - }) - - if err == nil && results != "" { - if formattedResults := formatBackupResults(results); formattedResults != "" { - if !hasOutput { - fmt.Fprintf(out, "\n") - hasOutput = true - } - fmt.Fprintf(out, "Backup Results:\n") - fmt.Fprintf(out, "%s\n", formattedResults) - fmt.Fprintf(out, "\n") - } - } - - // 3. Fetch BackupItemOperations - itemOps, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: backupName, - DataType: "BackupItemOperations", - Namespace: userNamespace, - HTTPTimeout: timeout, - }) - - if err == nil && itemOps != "" { - if formattedOps := formatItemOperations(itemOps); formattedOps != "" { - if !hasOutput { - fmt.Fprintf(out, "\n") - } - fmt.Fprintf(out, "Backup Item Operations:\n") - fmt.Fprintf(out, "%s\n", formattedOps) - fmt.Fprintf(out, "\n") - } - } - - return nil -} - -// formatVolumeInfo formats volume snapshot information for display -func formatVolumeInfo(volumeInfo string) string { - if strings.TrimSpace(volumeInfo) == "" { - return "" - } - - // Try to parse as JSON array - var snapshots []interface{} - if err := json.Unmarshal([]byte(volumeInfo), &snapshots); err != nil { - // If parsing fails, fall back to indented output - return indent(volumeInfo, " ") - } - - // If empty array, return empty string (will show "") - if len(snapshots) == 0 { - return "" - } - - // Format as indented JSON for readability - formatted, err := json.MarshalIndent(snapshots, " ", " ") - if err != nil { - return indent(volumeInfo, " ") - } - return indent(string(formatted), " ") -} - -// formatResourceList formats the resource list for display -func formatResourceList(resourceList string) string { - if strings.TrimSpace(resourceList) == "" { - return "" - } - - // Try to parse as JSON map - var resources map[string][]string - if err := json.Unmarshal([]byte(resourceList), &resources); err != nil { - // If parsing fails, fall back to indented output - return indent(resourceList, " ") - } - - // Sort the keys (GroupVersionKind) - keys := make([]string, 0, len(resources)) - for k := range resources { - keys = append(keys, k) - } - sort.Strings(keys) - - // Build formatted output - var output strings.Builder - for _, gvk := range keys { - items := resources[gvk] - fmt.Fprintf(&output, " %s:\n", gvk) - for _, item := range items { - fmt.Fprintf(&output, " - %s\n", item) - } - } - - return strings.TrimSuffix(output.String(), "\n") -} - -// formatBackupResults formats backup results (errors/warnings) for display -func formatBackupResults(results string) string { - if strings.TrimSpace(results) == "" { - return "" - } - - // Try to parse as JSON object with errors and warnings - var resultsObj struct { - Errors map[string]interface{} `json:"errors"` - Warnings map[string]interface{} `json:"warnings"` - } - if err := json.Unmarshal([]byte(results), &resultsObj); err != nil { - // If parsing fails, fall back to indented output - return indent(results, " ") - } - - // If both are empty, return empty string so section won't be printed - if len(resultsObj.Errors) == 0 && len(resultsObj.Warnings) == 0 { - return "" - } - - // Format nicely - var output strings.Builder - - // Show errors - output.WriteString(" Errors:\n") - if len(resultsObj.Errors) > 0 { - formatted, _ := json.MarshalIndent(resultsObj.Errors, " ", " ") - output.WriteString(indent(string(formatted), " ")) - } else { - output.WriteString(" ") - } - output.WriteString("\n\n") - - // Show warnings - output.WriteString(" Warnings:\n") - if len(resultsObj.Warnings) > 0 { - formatted, _ := json.MarshalIndent(resultsObj.Warnings, " ", " ") - output.WriteString(indent(string(formatted), " ")) - } else { - output.WriteString(" ") - } - - return strings.TrimSuffix(output.String(), "\n") -} - -// formatItemOperations formats backup item operations for display -func formatItemOperations(itemOps string) string { - if strings.TrimSpace(itemOps) == "" { - return "" - } - - // Try to parse as JSON array - var operations []interface{} - if err := json.Unmarshal([]byte(itemOps), &operations); err != nil { - // If parsing fails, fall back to indented output - return indent(itemOps, " ") - } - - // If empty array, return empty string (will show "") - if len(operations) == 0 { - return "" - } - - // Format as indented JSON for readability - formatted, err := json.MarshalIndent(operations, " ", " ") - if err != nil { - return indent(itemOps, " ") - } - return indent(string(formatted), " ") -} - -// colorizePhase returns the phase string with ANSI color codes -func colorizePhase(phase string) string { - const ( - colorGreen = "\033[32m" - colorYellow = "\033[33m" - colorRed = "\033[31m" - colorReset = "\033[0m" - ) - - switch phase { - case "Completed": - return colorGreen + phase + colorReset - case "InProgress", "New": - return colorYellow + phase + colorReset - case "Failed", "FailedValidation", "PartiallyFailed": - return colorRed + phase + colorReset - default: - return phase - } -} - -// NonAdminDescribeBackup mirrors Velero's output.DescribeBackup functionality -// but works within non-admin RBAC boundaries using NonAdminDownloadRequest. -// The timeout parameter controls how long to wait for download requests to complete. -// If timeout is 0, DefaultOperationTimeout is used. -func NonAdminDescribeBackup(cmd *cobra.Command, kbClient kbclient.Client, nab *nacv1alpha1.NonAdminBackup, userNamespace string, timeout time.Duration) error { - // Use provided timeout or fall back to default - if timeout == 0 { - timeout = shared.DefaultOperationTimeout - } - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - // Print basic backup information - fmt.Fprintf(cmd.OutOrStdout(), "Name: %s\n", nab.Name) - fmt.Fprintf(cmd.OutOrStdout(), "Namespace: %s\n", nab.Namespace) - - // Print labels - fmt.Fprintf(cmd.OutOrStdout(), "Labels:\n") - if len(nab.Labels) == 0 { - fmt.Fprintf(cmd.OutOrStdout(), " \n") - } else { - labelKeys := make([]string, 0, len(nab.Labels)) - for k := range nab.Labels { - labelKeys = append(labelKeys, k) - } - sort.Strings(labelKeys) - for _, k := range labelKeys { - fmt.Fprintf(cmd.OutOrStdout(), " %s=%s\n", k, nab.Labels[k]) - } - } - - // Print annotations - fmt.Fprintf(cmd.OutOrStdout(), "Annotations:\n") - if len(nab.Annotations) == 0 { - fmt.Fprintf(cmd.OutOrStdout(), " \n") - } else { - annotationKeys := make([]string, 0, len(nab.Annotations)) - for k := range nab.Annotations { - annotationKeys = append(annotationKeys, k) - } - sort.Strings(annotationKeys) - for _, k := range annotationKeys { - fmt.Fprintf(cmd.OutOrStdout(), " %s=%s\n", k, nab.Annotations[k]) - } - } - - // Print timestamps and status from NonAdminBackup - fmt.Fprintf(cmd.OutOrStdout(), "Creation Timestamp: %s\n", nab.CreationTimestamp.Format(time.RFC3339)) - fmt.Fprintf(cmd.OutOrStdout(), "Phase: %s\n", nab.Status.Phase) - - // If there's a referenced Velero backup, get more details - if nab.Status.VeleroBackup != nil && nab.Status.VeleroBackup.Name != "" { - veleroBackupName := nab.Status.VeleroBackup.Name - - // Try to get additional backup details, but don't block if they're not available - fmt.Fprintf(cmd.OutOrStdout(), "\nFetching additional backup details...") - - // Get backup results using NonAdminDownloadRequest (most important data) - if results, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: veleroBackupName, - DataType: "BackupResults", - Namespace: userNamespace, - HTTPTimeout: timeout, - }); err == nil { - fmt.Fprintf(cmd.OutOrStdout(), "\nBackup Results:\n") - fmt.Fprintf(cmd.OutOrStdout(), "%s", indent(results, " ")) - } - - // Get backup details using NonAdminDownloadRequest for BackupResourceList - if resourceList, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: veleroBackupName, - DataType: "BackupResourceList", - Namespace: userNamespace, - HTTPTimeout: timeout, - }); err == nil { - fmt.Fprintf(cmd.OutOrStdout(), "\nBackup Resource List:\n") - fmt.Fprintf(cmd.OutOrStdout(), "%s", indent(resourceList, " ")) - } - - // Get backup volume info using NonAdminDownloadRequest - if volumeInfo, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: veleroBackupName, - DataType: "BackupVolumeInfos", - Namespace: userNamespace, - HTTPTimeout: timeout, - }); err == nil { - fmt.Fprintf(cmd.OutOrStdout(), "\nBackup Volume Info:\n") - fmt.Fprintf(cmd.OutOrStdout(), "%s", indent(volumeInfo, " ")) - } - - // Get backup item operations using NonAdminDownloadRequest - if itemOps, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: veleroBackupName, - DataType: "BackupItemOperations", - Namespace: userNamespace, - HTTPTimeout: timeout, - }); err == nil { - fmt.Fprintf(cmd.OutOrStdout(), "\nBackup Item Operations:\n") - fmt.Fprintf(cmd.OutOrStdout(), "%s", indent(itemOps, " ")) - } - - fmt.Fprintf(cmd.OutOrStdout(), "\nDone fetching additional details.") - } - - // Print NonAdminBackup Spec (excluding sensitive information) - if nab.Spec.BackupSpec != nil { - specYaml, err := yaml.Marshal(nab.Spec.BackupSpec) - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), "\nSpec: \n", err) - } else { - filteredSpec := filterIncludedNamespaces(string(specYaml)) - fmt.Fprintf(cmd.OutOrStdout(), "\nSpec:\n%s", indent(filteredSpec, " ")) - } - } - - // Print NonAdminBackup Status (excluding sensitive information) - statusYaml, err := yaml.Marshal(nab.Status) - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), "\nStatus: \n", err) - } else { - // Filter out includednamespaces from status output as well - filteredStatus := filterIncludedNamespaces(string(statusYaml)) - fmt.Fprintf(cmd.OutOrStdout(), "\nStatus:\n%s", indent(filteredStatus, " ")) - } - - // Print Events for NonAdminBackup - fmt.Fprintf(cmd.OutOrStdout(), "\nEvents:\n") - var eventList corev1.EventList - if err := kbClient.List(ctx, &eventList, kbclient.InNamespace(userNamespace)); err != nil { - fmt.Fprintf(cmd.OutOrStdout(), " \n", err) - } else { - // Filter events related to this NonAdminBackup - var relatedEvents []corev1.Event - for _, event := range eventList.Items { - if event.InvolvedObject.Kind == "NonAdminBackup" && event.InvolvedObject.Name == nab.Name { - relatedEvents = append(relatedEvents, event) - } - } - - if len(relatedEvents) == 0 { - fmt.Fprintf(cmd.OutOrStdout(), " \n") - } else { - for _, e := range relatedEvents { - fmt.Fprintf(cmd.OutOrStdout(), " %s: %s\n", e.Reason, e.Message) - } - } - } - - return nil -} - -// Helper to filter out includednamespaces from YAML output -func filterIncludedNamespaces(yamlContent string) string { - lines := strings.Split(yamlContent, "\n") - var filtered []string - skip := false - var skipIndentLevel int - - for i := 0; i < len(lines); i++ { - line := lines[i] - trimmed := strings.TrimSpace(line) - - // Calculate indentation level - indentLevel := len(line) - len(strings.TrimLeft(line, " \t")) - - // Check if this line starts the includednamespaces field - if !skip && (trimmed == "includednamespaces:" || trimmed == "includedNamespaces:" || - strings.HasPrefix(trimmed, "includednamespaces: ") || strings.HasPrefix(trimmed, "includedNamespaces: ")) { - skip = true - skipIndentLevel = indentLevel - continue - } - - if skip { - // Stop skipping if we found a line at the same or lesser indentation level - // and it's not an empty line and it's not a list item belonging to the skipped field - if trimmed != "" && indentLevel <= skipIndentLevel && !strings.HasPrefix(trimmed, "- ") { - skip = false - // Process this line since we're no longer skipping - filtered = append(filtered, line) - } - // If we're still skipping, don't add the line - continue - } - - // Add the line if we're not skipping - filtered = append(filtered, line) - } - return strings.Join(filtered, "\n") -} - -// Helper to indent YAML blocks -func indent(s, prefix string) string { - lines := strings.Split(s, "\n") - for i, line := range lines { - if len(line) > 0 { - lines[i] = prefix + line - } - } - return strings.Join(lines, "\n") -} diff --git a/cmd/non-admin/backup/get.go b/cmd/non-admin/backup/get.go deleted file mode 100644 index 1ef9e0475..000000000 --- a/cmd/non-admin/backup/get.go +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright The Velero Contributors. - -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 backup - -import ( - "context" - "fmt" - "time" - - "github.com/migtools/oadp-cli/cmd/non-admin/output" - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -func NewGetCommand(f client.Factory, use string) *cobra.Command { - c := &cobra.Command{ - Use: use + " [NAME]", - Short: "Get non-admin backup(s)", - Long: "Get one or more non-admin backups", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // Get the current namespace from kubectl context - userNamespace, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - - // Create client with full scheme - kbClient, err := shared.NewClientWithFullScheme(f) - if err != nil { - return err - } - - if len(args) == 1 { - // Get specific backup - backupName := args[0] - var nab nacv1alpha1.NonAdminBackup - err := kbClient.Get(context.Background(), kbclient.ObjectKey{ - Namespace: userNamespace, - Name: backupName, - }, &nab) - if err != nil { - return fmt.Errorf("failed to get NonAdminBackup %q: %w", backupName, err) - } - - if printed, err := output.PrintWithFormat(cmd, &nab); printed || err != nil { - return err - } - - // If no output format specified, print table format for single item - list := &nacv1alpha1.NonAdminBackupList{ - Items: []nacv1alpha1.NonAdminBackup{nab}, - } - return printNonAdminBackupTable(list) - } else { - // List all backups in namespace - var nabList nacv1alpha1.NonAdminBackupList - err := kbClient.List(context.Background(), &nabList, &kbclient.ListOptions{ - Namespace: userNamespace, - }) - if err != nil { - return fmt.Errorf("failed to list NonAdminBackups: %w", err) - } - - if printed, err := output.PrintWithFormat(cmd, &nabList); printed || err != nil { - return err - } - - // Print table format - return printNonAdminBackupTable(&nabList) - } - }, - Example: ` # Get all non-admin backups in the current namespace - oc oadp nonadmin backup get - - # Get a specific non-admin backup - oc oadp nonadmin backup get my-backup - - # Get backups in YAML format - oc oadp nonadmin backup get -o yaml - - # Get a specific backup in JSON format - oc oadp nonadmin backup get my-backup -o json`, - } - - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -func printNonAdminBackupTable(nabList *nacv1alpha1.NonAdminBackupList) error { - if len(nabList.Items) == 0 { - fmt.Println("No non-admin backups found.") - return nil - } - - // Print header - fmt.Printf("%-30s %-15s %-15s %-20s %-10s %-10s\n", "NAME", "REQUEST PHASE", "VELERO PHASE", "CREATED", "AGE", "DURATION") - - // Print each backup - for _, nab := range nabList.Items { - status := getBackupStatus(&nab) - veleroPhase := getVeleroPhase(&nab) - created := nab.CreationTimestamp.Format("2006-01-02 15:04:05") - age := formatAge(nab.CreationTimestamp.Time) - duration := getBackupDuration(&nab) - - fmt.Printf("%-30s %-15s %-15s %-20s %-10s %-10s\n", nab.Name, status, veleroPhase, created, age, duration) - } - - return nil -} - -func getBackupStatus(nab *nacv1alpha1.NonAdminBackup) string { - if nab.Status.Phase != "" { - return string(nab.Status.Phase) - } - return "Unknown" -} - -func getVeleroPhase(nab *nacv1alpha1.NonAdminBackup) string { - if nab.Status.VeleroBackup != nil && nab.Status.VeleroBackup.Status != nil { - if nab.Status.VeleroBackup.Status.Phase != "" { - return string(nab.Status.VeleroBackup.Status.Phase) - } - } - return "N/A" -} - -func getBackupDuration(nab *nacv1alpha1.NonAdminBackup) string { - // Check if we have completion timestamp - if nab.Status.VeleroBackup != nil && nab.Status.VeleroBackup.Status != nil { - if !nab.Status.VeleroBackup.Status.CompletionTimestamp.IsZero() { - // Calculate duration from request creation to completion - duration := nab.Status.VeleroBackup.Status.CompletionTimestamp.Sub(nab.CreationTimestamp.Time) - return formatDuration(duration) - } - } - return "N/A" -} - -func formatDuration(d time.Duration) string { - if d < time.Minute { - return fmt.Sprintf("%ds", int(d.Seconds())) - } else if d < time.Hour { - return fmt.Sprintf("%dm%ds", int(d.Minutes()), int(d.Seconds())%60) - } else { - hours := int(d.Hours()) - minutes := int(d.Minutes()) % 60 - return fmt.Sprintf("%dh%dm", hours, minutes) - } -} - -func formatAge(t time.Time) string { - duration := time.Since(t) - - days := int(duration.Hours() / 24) - hours := int(duration.Hours()) % 24 - minutes := int(duration.Minutes()) % 60 - - if days > 0 { - return fmt.Sprintf("%dd", days) - } else if hours > 0 { - return fmt.Sprintf("%dh", hours) - } else if minutes > 0 { - return fmt.Sprintf("%dm", minutes) - } else { - return "1m" - } -} diff --git a/cmd/non-admin/backup/logs.go b/cmd/non-admin/backup/logs.go deleted file mode 100644 index 625e2d1f1..000000000 --- a/cmd/non-admin/backup/logs.go +++ /dev/null @@ -1,145 +0,0 @@ -package backup - -/* -Copyright The Velero Contributors. - -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. -*/ - -import ( - "context" - "fmt" - "net" - "time" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -func NewLogsCommand(f client.Factory, use string) *cobra.Command { - var requestTimeout time.Duration - - c := &cobra.Command{ - Use: use + " NAME", - Short: "Show logs for a non-admin backup", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // Get effective timeout (flag takes precedence over env var) - effectiveTimeout := shared.GetHTTPTimeoutWithOverride(requestTimeout) - - // Create context with the effective timeout for the entire operation - ctx, cancel := context.WithTimeout(context.Background(), effectiveTimeout) - defer cancel() - - // Get the current namespace from kubectl context - userNamespace, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - backupName := args[0] - - // Create scheme with required types - scheme, err := shared.NewSchemeWithTypes(shared.ClientOptions{ - IncludeNonAdminTypes: true, - IncludeVeleroTypes: true, - }) - if err != nil { - return err - } - - restConfig, err := f.ClientConfig() - if err != nil { - return fmt.Errorf("failed to get rest config: %w", err) - } - // Set timeout on REST config to prevent hanging when cluster is unreachable - restConfig.Timeout = effectiveTimeout - - // Set a custom dial function with timeout to ensure TCP connection attempts - // also respect the timeout (the default TCP dial timeout is ~30s) - dialer := &net.Dialer{ - Timeout: effectiveTimeout, - } - restConfig.Dial = func(ctx context.Context, network, address string) (net.Conn, error) { - return dialer.DialContext(ctx, network, address) - } - - kbClient, err := kbclient.New(restConfig, kbclient.Options{Scheme: scheme}) - if err != nil { - return fmt.Errorf("failed to create controller-runtime client: %w", err) - } - - // Verify the NonAdminBackup exists before creating download request - var nab nacv1alpha1.NonAdminBackup - if err := kbClient.Get(ctx, kbclient.ObjectKey{ - Namespace: userNamespace, - Name: backupName, - }, &nab); err != nil { - return fmt.Errorf("failed to get NonAdminBackup %q: %w", backupName, err) - } - - fmt.Fprintf(cmd.OutOrStdout(), "Waiting for backup logs to be processed (timeout: %v)...\n", effectiveTimeout) - - // Create download request and wait for signed URL - req, signedURL, err := shared.CreateAndWaitForDownloadURL(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: backupName, - DataType: "BackupLog", - Namespace: userNamespace, - Timeout: effectiveTimeout, - PollInterval: 2 * time.Second, - HTTPTimeout: effectiveTimeout, - OnProgress: func() { - fmt.Fprintf(cmd.OutOrStdout(), ".") - }, - }) - - if err != nil { - if req != nil { - // Clean up on error - if ctx.Err() == context.DeadlineExceeded { - return shared.FormatDownloadRequestTimeoutError(kbClient, req, effectiveTimeout) - } - deleteCtx, cancelDelete := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelDelete() - _ = kbClient.Delete(deleteCtx, req) - } - return err - } - - // Clean up the download request when done - defer func() { - deleteCtx, cancelDelete := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelDelete() - _ = kbClient.Delete(deleteCtx, req) - }() - - fmt.Fprintf(cmd.OutOrStdout(), "\nDownload URL received, fetching logs...\n") - - // Use the shared StreamDownloadContent function to download and stream logs - // Note: We use the same effective timeout for the HTTP download - if err := shared.StreamDownloadContentWithTimeout(signedURL, cmd.OutOrStdout(), effectiveTimeout); err != nil { - return fmt.Errorf("failed to download and stream logs: %w", err) - } - - return nil - }, - Example: ` oc oadp nonadmin backup logs my-backup - oc oadp nonadmin backup logs my-backup --request-timeout=30m`, - } - - c.Flags().DurationVar(&requestTimeout, "request-timeout", 0, fmt.Sprintf("The length of time to wait before giving up on a single server request (e.g., 30s, 5m, 1h). Overrides %s env var. Default: %v", shared.TimeoutEnvVar, shared.DefaultHTTPTimeout)) - - return c -} diff --git a/cmd/non-admin/backup/nonadminbackup_builder.go b/cmd/non-admin/backup/nonadminbackup_builder.go deleted file mode 100644 index cef8fdfec..000000000 --- a/cmd/non-admin/backup/nonadminbackup_builder.go +++ /dev/null @@ -1,164 +0,0 @@ -/* -Copyright The Velero Contributors. - -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 backup - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" -) - -/* - -Example usage: - -var nonAdminBackup = builder.ForNonAdminBackup("user-namespace", "backup-1"). - ObjectMeta( - builder.WithLabels("foo", "bar"), - ). - BackupSpec(nacv1alpha1.NonAdminBackupSpec{ - BackupSpec: &velerov1api.BackupSpec{ - IncludedNamespaces: []string{"app-namespace"}, - }, - }). - Result() - -*/ - -// NonAdminBackupBuilder builds NonAdminBackup objects. -type NonAdminBackupBuilder struct { - object *nacv1alpha1.NonAdminBackup -} - -// NewNonAdminBackupBuilder is the constructor for a NonAdminBackupBuilder. -func NewNonAdminBackupBuilder(namespace, name string) *NonAdminBackupBuilder { - return &NonAdminBackupBuilder{ - object: &nacv1alpha1.NonAdminBackup{ - TypeMeta: metav1.TypeMeta{ - APIVersion: nacv1alpha1.GroupVersion.String(), - Kind: "NonAdminBackup", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - }, - } -} - -// Result returns the built NonAdminBackup. -func (b *NonAdminBackupBuilder) Result() *nacv1alpha1.NonAdminBackup { - return b.object -} - -// ObjectMeta applies functional options to the NonAdminBackup's ObjectMeta. -func (b *NonAdminBackupBuilder) ObjectMeta(opts ...ObjectMetaOpt) *NonAdminBackupBuilder { - for _, opt := range opts { - opt(b.object) - } - - return b -} - -// BackupSpec sets the NonAdminBackup's backup spec. -func (b *NonAdminBackupBuilder) BackupSpec(spec nacv1alpha1.NonAdminBackupSpec) *NonAdminBackupBuilder { - b.object.Spec = spec - return b -} - -// Phase sets the NonAdminBackup's phase. -func (b *NonAdminBackupBuilder) Phase(phase nacv1alpha1.NonAdminPhase) *NonAdminBackupBuilder { - b.object.Status.Phase = phase - return b -} - -// VeleroBackup sets the reference to the created Velero backup. -func (b *NonAdminBackupBuilder) VeleroBackup(backupName, backupNamespace string) *NonAdminBackupBuilder { - if b.object.Status.VeleroBackup == nil { - b.object.Status.VeleroBackup = &nacv1alpha1.VeleroBackup{} - } - b.object.Status.VeleroBackup.Name = backupName - b.object.Status.VeleroBackup.Namespace = backupNamespace - return b -} - -// Conditions sets the NonAdminBackup's conditions. -func (b *NonAdminBackupBuilder) Conditions(conditions []metav1.Condition) *NonAdminBackupBuilder { - b.object.Status.Conditions = conditions - return b -} - -// WithStatus sets the NonAdminBackup's status. -func (b *NonAdminBackupBuilder) WithStatus(status nacv1alpha1.NonAdminBackupStatus) *NonAdminBackupBuilder { - b.object.Status = status - return b -} - -// ObjectMetaOpt is a functional option for setting ObjectMeta properties. -type ObjectMetaOpt func(obj metav1.Object) - -// WithLabels returns a functional option that sets labels on an object. -func WithLabels(key, value string) ObjectMetaOpt { - return func(obj metav1.Object) { - labels := obj.GetLabels() - if labels == nil { - labels = make(map[string]string) - } - labels[key] = value - obj.SetLabels(labels) - } -} - -// WithLabelsMap returns a functional option that sets labels from a map on an object. -func WithLabelsMap(labels map[string]string) ObjectMetaOpt { - return func(obj metav1.Object) { - existingLabels := obj.GetLabels() - if existingLabels == nil { - existingLabels = make(map[string]string) - } - for k, v := range labels { - existingLabels[k] = v - } - obj.SetLabels(existingLabels) - } -} - -// WithAnnotations returns a functional option that sets annotations on an object. -func WithAnnotations(key, value string) ObjectMetaOpt { - return func(obj metav1.Object) { - annotations := obj.GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } - annotations[key] = value - obj.SetAnnotations(annotations) - } -} - -// WithAnnotationsMap returns a functional option that sets annotations from a map on an object. -func WithAnnotationsMap(annotations map[string]string) ObjectMetaOpt { - return func(obj metav1.Object) { - existingAnnotations := obj.GetAnnotations() - if existingAnnotations == nil { - existingAnnotations = make(map[string]string) - } - for k, v := range annotations { - existingAnnotations[k] = v - } - obj.SetAnnotations(existingAnnotations) - } -} diff --git a/cmd/non-admin/bsl/bsl.go b/cmd/non-admin/bsl/bsl.go deleted file mode 100644 index cd6a21502..000000000 --- a/cmd/non-admin/bsl/bsl.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 bsl - -import ( - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" -) - -// NewBSLCommand creates the "bsl" subcommand under nonadmin -func NewBSLCommand(f client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "bsl", - Short: "Create and manage backup storage locations", - Long: "Create and manage non-admin backup storage locations", - } - - c.AddCommand( - NewCreateCommand(f, "create"), - NewGetCommand(f, "get"), - ) - - return c -} diff --git a/cmd/non-admin/bsl/bsl_test.go b/cmd/non-admin/bsl/bsl_test.go deleted file mode 100644 index d7b721e53..000000000 --- a/cmd/non-admin/bsl/bsl_test.go +++ /dev/null @@ -1,348 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 bsl - -import ( - "testing" - - "github.com/migtools/oadp-cli/internal/testutil" -) - -// TestNonAdminBSLCommands tests the non-admin BSL command functionality -func TestNonAdminBSLCommands(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - tests := []struct { - name string - args []string - expectContains []string - }{ - { - name: "nonadmin bsl help", - args: []string{"nonadmin", "bsl", "--help"}, - expectContains: []string{ - "Create and manage non-admin backup storage locations", - "create", - "get", - }, - }, - { - name: "nonadmin bsl create help", - args: []string{"nonadmin", "bsl", "create", "--help"}, - expectContains: []string{ - "Create a non-admin backup storage location", - "--provider", - "--bucket", - "--credential", - "--region", - "--prefix", - }, - }, - { - name: "nonadmin bsl get help", - args: []string{"nonadmin", "bsl", "get", "--help"}, - expectContains: []string{ - "Get one or more non-admin backup storage locations", - }, - }, - { - name: "na bsl shorthand help", - args: []string{"na", "bsl", "--help"}, - expectContains: []string{ - "Create and manage non-admin backup storage locations", - "create", - "get", - }, - }, - // Verb-noun order help command tests - { - name: "nonadmin get bsl help", - args: []string{"nonadmin", "get", "bsl", "--help"}, - expectContains: []string{ - "Get one or more non-admin backup storage locations", - }, - }, - { - name: "nonadmin create bsl help", - args: []string{"nonadmin", "create", "bsl", "--help"}, - expectContains: []string{ - "Create a non-admin backup storage location", - }, - }, - // Shorthand verb-noun order tests - { - name: "na get bsl help", - args: []string{"na", "get", "bsl", "--help"}, - expectContains: []string{ - "Get one or more non-admin backup storage locations", - }, - }, - { - name: "na create bsl help", - args: []string{"na", "create", "bsl", "--help"}, - expectContains: []string{ - "Create a non-admin backup storage location", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, tt.args, tt.expectContains) - }) - } -} - -// TestNonAdminBSLHelpFlags tests that both --help and -h work for BSL commands -func TestNonAdminBSLHelpFlags(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - commands := [][]string{ - {"nonadmin", "bsl", "--help"}, - {"nonadmin", "bsl", "-h"}, - {"nonadmin", "bsl", "create", "--help"}, - {"nonadmin", "bsl", "create", "-h"}, - {"nonadmin", "bsl", "get", "--help"}, - {"nonadmin", "bsl", "get", "-h"}, - {"na", "bsl", "--help"}, - {"na", "bsl", "-h"}, - // Verb-noun order help flags - {"nonadmin", "get", "bsl", "--help"}, - {"nonadmin", "get", "bsl", "-h"}, - {"nonadmin", "create", "bsl", "--help"}, - {"nonadmin", "create", "bsl", "-h"}, - // Shorthand verb-noun order help flags - {"na", "get", "bsl", "--help"}, - {"na", "get", "bsl", "-h"}, - {"na", "create", "bsl", "--help"}, - {"na", "create", "bsl", "-h"}, - } - - for _, cmd := range commands { - t.Run("help_flags_"+cmd[len(cmd)-1], func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{"Usage:"}) - }) - } -} - -// TestNonAdminBSLCreateFlags tests create command specific flags -func TestNonAdminBSLCreateFlags(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("create command has all expected flags", func(t *testing.T) { - expectedFlags := []string{ - "--provider", - "--bucket", - "--credential", - "--region", - "--prefix", - "--config", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "bsl", "create", "--help"}, - expectedFlags) - }) -} - -// TestNonAdminBSLExamples tests that help text contains proper examples -func TestNonAdminBSLExamples(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("create examples use correct command format", func(t *testing.T) { - expectedExamples := []string{ - "oc oadp nonadmin bsl create", - "--provider", - "--bucket", - "--credential", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "bsl", "create", "--help"}, - expectedExamples) - }) - - t.Run("get examples use correct command format", func(t *testing.T) { - expectedExamples := []string{ - "oc oadp nonadmin bsl get", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "bsl", "get", "--help"}, - expectedExamples) - }) - - t.Run("main bsl help shows subcommands", func(t *testing.T) { - expectedSubcommands := []string{ - "create", - "get", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "bsl", "--help"}, - expectedSubcommands) - }) -} - -// TestNonAdminBSLClientConfigIntegration tests that BSL commands respect client config -func TestNonAdminBSLClientConfigIntegration(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - _, cleanup := testutil.SetupTempHome(t) - defer cleanup() - - t.Run("bsl commands work with client config", func(t *testing.T) { - // Set a known namespace - _, err := testutil.RunCommand(t, binaryPath, "client", "config", "set", "namespace=user-namespace") - if err != nil { - t.Fatalf("Failed to set client config: %v", err) - } - - // Test that BSL commands can be invoked (they should respect the namespace) - // We test help commands since they don't require actual K8s resources - commands := [][]string{ - {"nonadmin", "bsl", "get", "--help"}, - {"nonadmin", "bsl", "create", "--help"}, - {"na", "bsl", "get", "--help"}, - // Verb-noun order commands - {"nonadmin", "get", "bsl", "--help"}, - {"nonadmin", "create", "bsl", "--help"}, - {"na", "get", "bsl", "--help"}, - {"na", "create", "bsl", "--help"}, - } - - for _, cmd := range commands { - t.Run("config_test_"+cmd[len(cmd)-2], func(t *testing.T) { - output, err := testutil.RunCommand(t, binaryPath, cmd...) - if err != nil { - t.Fatalf("Non-admin BSL command should work with client config: %v", err) - } - if output == "" { - t.Errorf("Expected help output for %v", cmd) - } - }) - } - }) -} - -// TestNonAdminBSLCommandStructure tests the overall command structure -func TestNonAdminBSLCommandStructure(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("bsl commands available under nonadmin", func(t *testing.T) { - _, err := testutil.RunCommand(t, binaryPath, "nonadmin", "--help") - if err != nil { - t.Fatalf("nonadmin command should exist: %v", err) - } - - expectedCommands := []string{"bsl"} - for _, cmd := range expectedCommands { - testutil.TestHelpCommand(t, binaryPath, []string{"nonadmin", "--help"}, []string{cmd}) - } - }) - - t.Run("bsl commands available under na shorthand", func(t *testing.T) { - _, err := testutil.RunCommand(t, binaryPath, "na", "--help") - if err != nil { - t.Fatalf("na command should exist: %v", err) - } - - expectedCommands := []string{"bsl"} - for _, cmd := range expectedCommands { - testutil.TestHelpCommand(t, binaryPath, []string{"na", "--help"}, []string{cmd}) - } - }) -} - -// TestVerbNounOrderBSLExamples tests that verb-noun order commands show proper BSL examples -func TestVerbNounOrderBSLExamples(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("get verb command shows bsl examples", func(t *testing.T) { - expectedExamples := []string{ - "oc oadp nonadmin get bsl", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "get", "--help"}, - expectedExamples) - }) - - t.Run("create verb command shows bsl examples", func(t *testing.T) { - expectedExamples := []string{ - "oc oadp nonadmin create bsl", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "create", "--help"}, - expectedExamples) - }) - - t.Run("get bsl with specific resource shows proper examples", func(t *testing.T) { - expectedExamples := []string{ - "oc oadp nonadmin bsl get", // Shows noun-first format from underlying command - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "get", "bsl", "--help"}, - expectedExamples) - }) - - t.Run("create bsl with specific resource shows proper examples", func(t *testing.T) { - expectedExamples := []string{ - "oc oadp nonadmin bsl create", // Shows noun-first format from underlying command - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "create", "bsl", "--help"}, - expectedExamples) - }) -} - -// TestNonAdminBSLOutputFormat tests that help text uses correct command format -func TestNonAdminBSLOutputFormat(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("usage shows oc oadp prefix", func(t *testing.T) { - expectedStrings := []string{ - "oc oadp nonadmin bsl", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "bsl", "--help"}, - expectedStrings) - }) - - t.Run("create usage shows oc oadp prefix", func(t *testing.T) { - expectedStrings := []string{ - "oc oadp nonadmin bsl create", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "bsl", "create", "--help"}, - expectedStrings) - }) - - t.Run("get usage shows oc oadp prefix", func(t *testing.T) { - expectedStrings := []string{ - "oc oadp nonadmin bsl get", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "bsl", "get", "--help"}, - expectedStrings) - }) -} diff --git a/cmd/non-admin/bsl/create.go b/cmd/non-admin/bsl/create.go deleted file mode 100644 index 50eba6ec8..000000000 --- a/cmd/non-admin/bsl/create.go +++ /dev/null @@ -1,207 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 bsl - -import ( - "context" - "fmt" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/migtools/oadp-cli/cmd/non-admin/output" - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - "github.com/vmware-tanzu/velero/pkg/builder" - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd" - "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func NewCreateCommand(f client.Factory, use string) *cobra.Command { - o := NewCreateOptions() - - c := &cobra.Command{ - Use: use + " NAME", - Short: "Create a non-admin backup storage location", - Args: cobra.ExactArgs(1), - Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(args, f)) - cmd.CheckError(o.Validate(c, args, f)) - cmd.CheckError(o.Run(c, f)) - }, - Example: ` # Create a non-admin backup storage location for AWS - oc oadp nonadmin bsl create my-storage \ - --provider aws \ - --bucket my-velero-bucket \ - --credential cloud-credentials=cloud \ - --region us-east-1 - - # Create with prefix for organizing backups - oc oadp nonadmin bsl create my-storage \ - --provider aws \ - --bucket my-velero-bucket \ - --prefix velero-backups \ - --credential cloud-credentials=cloud \ - --region us-east-1 - - # Create with custom credential key - oc oadp nonadmin bsl create my-storage \ - --provider aws \ - --bucket my-velero-bucket \ - --credential my-secret=service-account-key \ - --region us-east-1 - - # View the YAML without creating the resource - oc oadp nonadmin bsl create my-storage \ - --provider aws \ - --bucket my-bucket \ - --credential cloud-credentials=cloud \ - --region us-east-1 \ - -o yaml`, - } - - o.BindFlags(c.Flags()) - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -type CreateOptions struct { - Name string - Namespace string - Provider string - Bucket string - Prefix string - Credential flag.Map - Region string - Config map[string]string - client kbclient.WithWatch -} - -func NewCreateOptions() *CreateOptions { - return &CreateOptions{ - Credential: flag.NewMap(), - Config: make(map[string]string), - } -} - -func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { - flags.StringVar(&o.Provider, "provider", "", "Storage provider (required). Examples: aws, azure, gcp") - flags.StringVar(&o.Bucket, "bucket", "", "Object storage bucket name (required)") - flags.StringVar(&o.Prefix, "prefix", "", "Prefix for backup objects in the bucket") - flags.Var(&o.Credential, "credential", "The credential to be used by this location as a key-value pair, where the key is the Kubernetes Secret name, and the value is the data key name within the Secret. Required, one value only.") - flags.StringVar(&o.Region, "region", "", "Storage region (required for some providers like AWS)") - flags.StringToStringVar(&o.Config, "config", nil, "Additional provider-specific configuration (key=value pairs)") -} - -func (o *CreateOptions) Complete(args []string, f client.Factory) error { - o.Name = args[0] - - // Create client with full scheme including NonAdmin and Velero types - client, err := shared.NewClientWithFullScheme(f) - if err != nil { - return err - } - - o.client = client - - // Get the current namespace - currentNS, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - o.Namespace = currentNS - - return nil -} - -func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Factory) error { - if o.Provider == "" { - return fmt.Errorf("--provider is required") - } - if o.Bucket == "" { - return fmt.Errorf("--bucket is required") - } - if len(o.Credential.Data()) == 0 { - return errors.New("--credential is required") - } - if len(o.Credential.Data()) > 1 { - return errors.New("--credential can only contain 1 key/value pair") - } - - return nil -} - -func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { - // Build config map - config := make(map[string]string) - if o.Region != "" { - config["region"] = o.Region - } - // Add any additional config provided via --config flag - for k, v := range o.Config { - config[k] = v - } - - // Create the NABSL - nabsl := &nacv1alpha1.NonAdminBackupStorageLocation{ - ObjectMeta: metav1.ObjectMeta{ - Name: o.Name, - Namespace: o.Namespace, - }, - Spec: nacv1alpha1.NonAdminBackupStorageLocationSpec{ - BackupStorageLocationSpec: &velerov1.BackupStorageLocationSpec{ - Provider: o.Provider, - Config: config, - StorageType: velerov1.StorageType{ - ObjectStorage: &velerov1.ObjectStorageLocation{ - Bucket: o.Bucket, - Prefix: o.Prefix, - }, - }, - }, - }, - } - - // Set credential from user-provided key-value pair - for secretName, secretKey := range o.Credential.Data() { - nabsl.Spec.BackupStorageLocationSpec.Credential = builder.ForSecretKeySelector(secretName, secretKey).Result() - break - } - - if printed, err := output.PrintWithFormat(c, nabsl); printed || err != nil { - return err - } - - err := o.client.Create(context.Background(), nabsl) - if err != nil { - return err - } - - fmt.Printf("NonAdminBackupStorageLocation %q created successfully.\n", nabsl.Name) - fmt.Println("The controller will create a request for admin approval.") - fmt.Println("Use 'oc oadp nonadmin bsl request get' to view created requests.") - fmt.Printf("Use `oc oadp client config set default-nabsl=%s` to set this BSL as the default to avoid specifying the BSL name each time.\n", nabsl.Name) - fmt.Println() - return nil -} diff --git a/cmd/non-admin/bsl/get.go b/cmd/non-admin/bsl/get.go deleted file mode 100644 index 2bffbce03..000000000 --- a/cmd/non-admin/bsl/get.go +++ /dev/null @@ -1,183 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 bsl - -import ( - "context" - "fmt" - "time" - - "github.com/migtools/oadp-cli/cmd/non-admin/output" - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -func NewGetCommand(f client.Factory, use string) *cobra.Command { - c := &cobra.Command{ - Use: use + " [NAME]", - Short: "Get non-admin backup storage location(s)", - Long: "Get one or more non-admin backup storage locations", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // Get the current namespace from kubectl context - userNamespace, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - - // Create client with full scheme - kbClient, err := shared.NewClientWithFullScheme(f) - if err != nil { - return err - } - - if len(args) == 1 { - // Get specific BSL - bslName := args[0] - var nabsl nacv1alpha1.NonAdminBackupStorageLocation - err := kbClient.Get(context.Background(), kbclient.ObjectKey{ - Namespace: userNamespace, - Name: bslName, - }, &nabsl) - if err != nil { - return fmt.Errorf("failed to get NonAdminBackupStorageLocation %q: %w", bslName, err) - } - - if printed, err := output.PrintWithFormat(cmd, &nabsl); printed || err != nil { - return err - } - - // If no output format specified, print table format for single item - list := &nacv1alpha1.NonAdminBackupStorageLocationList{ - Items: []nacv1alpha1.NonAdminBackupStorageLocation{nabsl}, - } - return printNonAdminBSLTable(list) - } else { - // List all BSLs in namespace - var nabslList nacv1alpha1.NonAdminBackupStorageLocationList - err := kbClient.List(context.Background(), &nabslList, &kbclient.ListOptions{ - Namespace: userNamespace, - }) - if err != nil { - return fmt.Errorf("failed to list NonAdminBackupStorageLocations: %w", err) - } - - if printed, err := output.PrintWithFormat(cmd, &nabslList); printed || err != nil { - return err - } - - // Print table format - return printNonAdminBSLTable(&nabslList) - } - }, - Example: ` # Get all non-admin backup storage locations in the current namespace - oc oadp nonadmin bsl get - - # Get a specific non-admin backup storage location - oc oadp nonadmin bsl get my-storage - - # Get backup storage locations in YAML format - oc oadp nonadmin bsl get -o yaml - - # Get a specific backup storage location in JSON format - oc oadp nonadmin bsl get my-storage -o json`, - } - - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -func printNonAdminBSLTable(nabslList *nacv1alpha1.NonAdminBackupStorageLocationList) error { - if len(nabslList.Items) == 0 { - fmt.Println("No non-admin backup storage locations found.") - return nil - } - - // Print header - fmt.Printf("%-30s %-15s %-15s %-15s %-20s %-10s\n", "NAME", "REQUEST PHASE", "VELERO PHASE", "PROVIDER", "BUCKET/PREFIX", "AGE") - - // Print each BSL - for _, nabsl := range nabslList.Items { - status := getBSLStatus(&nabsl) - veleroPhase := getBSLVeleroPhase(&nabsl) - provider := getProvider(&nabsl) - bucketPrefix := getBucketPrefix(&nabsl) - age := formatAge(nabsl.CreationTimestamp.Time) - - fmt.Printf("%-30s %-15s %-15s %-15s %-20s %-10s\n", nabsl.Name, status, veleroPhase, provider, bucketPrefix, age) - } - - return nil -} - -func getBSLStatus(nabsl *nacv1alpha1.NonAdminBackupStorageLocation) string { - if nabsl.Status.Phase != "" { - return string(nabsl.Status.Phase) - } - return "Unknown" -} - -func getBSLVeleroPhase(nabsl *nacv1alpha1.NonAdminBackupStorageLocation) string { - if nabsl.Status.VeleroBackupStorageLocation != nil && nabsl.Status.VeleroBackupStorageLocation.Status != nil { - if nabsl.Status.VeleroBackupStorageLocation.Status.Phase != "" { - return string(nabsl.Status.VeleroBackupStorageLocation.Status.Phase) - } - } - return "N/A" -} - -func getProvider(nabsl *nacv1alpha1.NonAdminBackupStorageLocation) string { - if nabsl.Spec.BackupStorageLocationSpec != nil && nabsl.Spec.BackupStorageLocationSpec.Provider != "" { - return nabsl.Spec.BackupStorageLocationSpec.Provider - } - return "N/A" -} - -func getBucketPrefix(nabsl *nacv1alpha1.NonAdminBackupStorageLocation) string { - if nabsl.Spec.BackupStorageLocationSpec != nil && nabsl.Spec.BackupStorageLocationSpec.ObjectStorage != nil { - bucket := nabsl.Spec.BackupStorageLocationSpec.ObjectStorage.Bucket - prefix := nabsl.Spec.BackupStorageLocationSpec.ObjectStorage.Prefix - if prefix != "" { - return fmt.Sprintf("%s/%s", bucket, prefix) - } - return bucket - } - return "N/A" -} - -func formatAge(t time.Time) string { - duration := time.Since(t) - - days := int(duration.Hours() / 24) - hours := int(duration.Hours()) % 24 - minutes := int(duration.Minutes()) % 60 - - if days > 0 { - return fmt.Sprintf("%dd", days) - } else if hours > 0 { - return fmt.Sprintf("%dh", hours) - } else if minutes > 0 { - return fmt.Sprintf("%dm", minutes) - } else { - return "1m" - } -} diff --git a/cmd/non-admin/nonadmin.go b/cmd/non-admin/nonadmin.go deleted file mode 100644 index dd6c08e5f..000000000 --- a/cmd/non-admin/nonadmin.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 nonadmin - -import ( - "github.com/migtools/oadp-cli/cmd/non-admin/backup" - "github.com/migtools/oadp-cli/cmd/non-admin/bsl" - "github.com/migtools/oadp-cli/cmd/non-admin/restore" - "github.com/migtools/oadp-cli/cmd/non-admin/verbs" - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" -) - -// NewNonAdminCommand creates the top-level "nonadmin" subcommand -func NewNonAdminCommand(f client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "nonadmin", - Short: "Work with non-admin resources", - Long: "Work with non-admin resources like backups, restores and backup storage locations", - Aliases: []string{"na"}, - } - - // Add backup subcommand - c.AddCommand(backup.NewBackupCommand(f)) - - // Add restore subcommand - c.AddCommand(restore.NewRestoreCommand(f)) - - // Add backup storage location subcommand - c.AddCommand(bsl.NewBSLCommand(f)) - - // Add verb-based commands for compatibility with Velero CLI pattern - c.AddCommand(verbs.NewGetCommand(f)) - c.AddCommand(verbs.NewCreateCommand(f)) - c.AddCommand(verbs.NewDeleteCommand(f)) - c.AddCommand(verbs.NewDescribeCommand(f)) - c.AddCommand(verbs.NewLogsCommand(f)) - - return c -} diff --git a/cmd/non-admin/nonadmin_test.go b/cmd/non-admin/nonadmin_test.go deleted file mode 100644 index 5c647702e..000000000 --- a/cmd/non-admin/nonadmin_test.go +++ /dev/null @@ -1,216 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 nonadmin - -import ( - "testing" - - "github.com/migtools/oadp-cli/internal/testutil" -) - -// TestNonAdminCommands tests the non-admin command functionality -func TestNonAdminCommands(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - tests := []struct { - name string - args []string - expectContains []string - }{ - { - name: "nonadmin help", - args: []string{"nonadmin", "--help"}, - expectContains: []string{ - "Work with non-admin resources", - "Work with non-admin resources like backups", - "backup", - "bsl", - }, - }, - { - name: "nonadmin backup help", - args: []string{"nonadmin", "backup", "--help"}, - expectContains: []string{ - "Work with non-admin backups", - "create", - }, - }, - { - name: "nonadmin backup create help", - args: []string{"nonadmin", "backup", "create", "--help"}, - expectContains: []string{ - "Create a non-admin backup", - }, - }, - { - name: "nonadmin backup create help", - args: []string{"nonadmin", "create", "backup", "--help"}, - expectContains: []string{ - "Create a non-admin backup", - }, - }, - // Verb-noun order help command tests - { - name: "nonadmin get help", - args: []string{"nonadmin", "get", "--help"}, - expectContains: []string{ - "Get one or more non-admin resources", - "backup", - }, - }, - { - name: "nonadmin create help", - args: []string{"nonadmin", "create", "--help"}, - expectContains: []string{ - "Create non-admin resources", - "backup", - "bsl", - }, - }, - { - name: "nonadmin delete help", - args: []string{"nonadmin", "delete", "--help"}, - expectContains: []string{ - "Delete non-admin resources", - "backup", - }, - }, - { - name: "nonadmin describe help", - args: []string{"nonadmin", "describe", "--help"}, - expectContains: []string{ - "Describe non-admin resources", - "backup", - }, - }, - { - name: "nonadmin logs help", - args: []string{"nonadmin", "logs", "--help"}, - expectContains: []string{ - "Get logs for non-admin resources", - "backup", - }, - }, - // Verb-noun order with specific resources - { - name: "nonadmin get backup help", - args: []string{"nonadmin", "get", "backup", "--help"}, - expectContains: []string{ - "Get one or more non-admin backups", - }, - }, - { - name: "nonadmin create backup help", - args: []string{"nonadmin", "create", "backup", "--help"}, - expectContains: []string{ - "Create a non-admin backup", - }, - }, - { - name: "nonadmin delete backup help", - args: []string{"nonadmin", "delete", "backup", "--help"}, - expectContains: []string{ - "Delete one or more non-admin backups", - }, - }, - { - name: "nonadmin describe backup help", - args: []string{"nonadmin", "describe", "backup", "--help"}, - expectContains: []string{ - "Describe a non-admin backup", - }, - }, - { - name: "nonadmin logs backup help", - args: []string{"nonadmin", "logs", "backup", "--help"}, - expectContains: []string{ - "Show logs for a non-admin backup", - }, - }, - { - name: "nonadmin create bsl help", - args: []string{"nonadmin", "create", "bsl", "--help"}, - expectContains: []string{ - "Create a non-admin backup storage location", - }, - }, - // Shorthand tests for verb-noun order - { - name: "na get help", - args: []string{"na", "get", "--help"}, - expectContains: []string{ - "Get one or more non-admin resources", - "backup", - }, - }, - { - name: "na create backup help", - args: []string{"na", "create", "backup", "--help"}, - expectContains: []string{ - "Create a non-admin backup", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, tt.args, tt.expectContains) - }) - } -} - -// TestNonAdminHelpFlags tests that both --help and -h work for non-admin commands -func TestNonAdminHelpFlags(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - commands := [][]string{ - {"nonadmin", "--help"}, - {"nonadmin", "-h"}, - {"nonadmin", "backup", "--help"}, - {"nonadmin", "backup", "-h"}, - {"nonadmin", "bsl", "--help"}, - {"nonadmin", "bsl", "-h"}, - // Verb-noun order help flags - {"nonadmin", "get", "--help"}, - {"nonadmin", "get", "-h"}, - {"nonadmin", "create", "--help"}, - {"nonadmin", "create", "-h"}, - {"nonadmin", "delete", "--help"}, - {"nonadmin", "delete", "-h"}, - {"nonadmin", "describe", "--help"}, - {"nonadmin", "describe", "-h"}, - {"nonadmin", "logs", "--help"}, - {"nonadmin", "logs", "-h"}, - {"nonadmin", "get", "backup", "--help"}, - {"nonadmin", "get", "backup", "-h"}, - {"nonadmin", "create", "backup", "--help"}, - {"nonadmin", "create", "backup", "-h"}, - {"nonadmin", "create", "bsl", "--help"}, - {"nonadmin", "create", "bsl", "-h"}, - // Shorthand verb-noun order help flags - {"na", "get", "--help"}, - {"na", "get", "-h"}, - {"na", "create", "--help"}, - {"na", "create", "-h"}, - } - - for _, cmd := range commands { - t.Run("help_flags_"+cmd[len(cmd)-1], func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{"Usage:"}) - }) - } -} diff --git a/cmd/non-admin/output/output.go b/cmd/non-admin/output/output.go deleted file mode 100644 index a27eca01e..000000000 --- a/cmd/non-admin/output/output.go +++ /dev/null @@ -1,148 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 output - -import ( - "bytes" - "fmt" - "io" - - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - velerooutput "github.com/vmware-tanzu/velero/pkg/cmd/util/output" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" -) - -// NonAdminScheme returns a runtime.Scheme with NonAdmin types registered -func NonAdminScheme() *runtime.Scheme { - scheme := runtime.NewScheme() - - // Add NonAdmin types - if err := nacv1alpha1.AddToScheme(scheme); err != nil { - panic(fmt.Sprintf("failed to add NonAdmin types to scheme: %v", err)) - } - - // Add Velero types for compatibility - if err := velerov1api.AddToScheme(scheme); err != nil { - panic(fmt.Sprintf("failed to add Velero types to scheme: %v", err)) - } - - return scheme -} - -// BindFlags wraps Velero's BindFlags to add output flags -func BindFlags(flags *pflag.FlagSet) { - velerooutput.BindFlags(flags) -} - -// ClearOutputFlagDefault wraps Velero's ClearOutputFlagDefault -func ClearOutputFlagDefault(cmd *cobra.Command) { - velerooutput.ClearOutputFlagDefault(cmd) -} - -// PrintWithFormat prints the provided object in the format specified by -// the command's flags. This is a custom implementation for nonadmin commands -// that supports NonAdmin CRD types (NonAdminBackup, NonAdminRestore, etc.) -func PrintWithFormat(c *cobra.Command, obj runtime.Object) (bool, error) { - format := velerooutput.GetOutputFlagValue(c) - if format == "" { - return false, nil - } - - switch format { - case "json", "yaml": - return printEncoded(obj, format) - case "table": - // Table format is not supported by this function - // The caller should handle table printing - return false, nil - } - - return false, errors.Errorf("unsupported output format %q; valid values are 'table', 'json', and 'yaml'", format) -} - -func printEncoded(obj runtime.Object, format string) (bool, error) { - // assume we're printing obj - toPrint := obj - - if meta.IsListType(obj) { - list, _ := meta.ExtractList(obj) - if len(list) == 1 { - // if obj was a list and there was only 1 item, just print that 1 instead of a list - toPrint = list[0] - } - } - - encoded, err := encode(toPrint, format) - if err != nil { - return false, err - } - - fmt.Println(string(encoded)) - - return true, nil -} - -func encode(obj runtime.Object, format string) ([]byte, error) { - buf := new(bytes.Buffer) - - if err := encodeTo(obj, format, buf); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func encodeTo(obj runtime.Object, format string, w io.Writer) error { - encoder, err := encoderFor(format, obj) - if err != nil { - return err - } - - return errors.WithStack(encoder.Encode(obj, w)) -} - -func encoderFor(format string, obj runtime.Object) (runtime.Encoder, error) { - var encoder runtime.Encoder - - // Use NonAdminScheme instead of Velero's scheme - codecFactory := serializer.NewCodecFactory(NonAdminScheme()) - - desiredMediaType := fmt.Sprintf("application/%s", format) - serializerInfo, found := runtime.SerializerInfoForMediaType(codecFactory.SupportedMediaTypes(), desiredMediaType) - if !found { - return nil, errors.Errorf("unable to locate an encoder for %q", desiredMediaType) - } - if serializerInfo.PrettySerializer != nil { - encoder = serializerInfo.PrettySerializer - } else { - encoder = serializerInfo.Serializer - } - - if !obj.GetObjectKind().GroupVersionKind().Empty() { - return encoder, nil - } - - // Use the appropriate GroupVersion for encoding - // For NonAdmin types, use nacv1alpha1.GroupVersion - encoder = codecFactory.EncoderForVersion(encoder, nacv1alpha1.GroupVersion) - return encoder, nil -} diff --git a/cmd/non-admin/output/output_test.go b/cmd/non-admin/output/output_test.go deleted file mode 100644 index 4a130fa69..000000000 --- a/cmd/non-admin/output/output_test.go +++ /dev/null @@ -1,520 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 output - -import ( - "bytes" - "io" - "os" - "strings" - "testing" - - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/spf13/cobra" - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestNonAdminScheme(t *testing.T) { - scheme := NonAdminScheme() - - tests := []struct { - name string - gvk schema.GroupVersionKind - objType runtime.Object - }{ - { - name: "NonAdminBackup is registered", - gvk: schema.GroupVersionKind{ - Group: "oadp.openshift.io", - Version: "v1alpha1", - Kind: "NonAdminBackup", - }, - objType: &nacv1alpha1.NonAdminBackup{}, - }, - { - name: "NonAdminBackupList is registered", - gvk: schema.GroupVersionKind{ - Group: "oadp.openshift.io", - Version: "v1alpha1", - Kind: "NonAdminBackupList", - }, - objType: &nacv1alpha1.NonAdminBackupList{}, - }, - { - name: "NonAdminRestore is registered", - gvk: schema.GroupVersionKind{ - Group: "oadp.openshift.io", - Version: "v1alpha1", - Kind: "NonAdminRestore", - }, - objType: &nacv1alpha1.NonAdminRestore{}, - }, - { - name: "NonAdminRestoreList is registered", - gvk: schema.GroupVersionKind{ - Group: "oadp.openshift.io", - Version: "v1alpha1", - Kind: "NonAdminRestoreList", - }, - objType: &nacv1alpha1.NonAdminRestoreList{}, - }, - { - name: "NonAdminBackupStorageLocation is registered", - gvk: schema.GroupVersionKind{ - Group: "oadp.openshift.io", - Version: "v1alpha1", - Kind: "NonAdminBackupStorageLocation", - }, - objType: &nacv1alpha1.NonAdminBackupStorageLocation{}, - }, - { - name: "Velero Backup is registered", - gvk: schema.GroupVersionKind{ - Group: "velero.io", - Version: "v1", - Kind: "Backup", - }, - objType: &velerov1api.Backup{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Check if the type is recognized by the scheme - gvks, _, err := scheme.ObjectKinds(tt.objType) - if err != nil { - t.Fatalf("Failed to get ObjectKinds: %v", err) - } - - found := false - for _, gvk := range gvks { - if gvk.Group == tt.gvk.Group && gvk.Version == tt.gvk.Version && gvk.Kind == tt.gvk.Kind { - found = true - break - } - } - - if !found { - t.Errorf("Expected GVK %v to be registered in scheme, but it was not found", tt.gvk) - } - }) - } -} - -func TestBindFlags(t *testing.T) { - cmd := &cobra.Command{} - BindFlags(cmd.Flags()) - - // Check that the output flag is bound - outputFlag := cmd.Flags().Lookup("output") - if outputFlag == nil { - t.Fatal("Expected 'output' flag to be bound, but it was not found") - } - - // Check that the label-columns flag is bound - labelColumnsFlag := cmd.Flags().Lookup("label-columns") - if labelColumnsFlag == nil { - t.Fatal("Expected 'label-columns' flag to be bound, but it was not found") - } - - // Check that the show-labels flag is bound - showLabelsFlag := cmd.Flags().Lookup("show-labels") - if showLabelsFlag == nil { - t.Fatal("Expected 'show-labels' flag to be bound, but it was not found") - } -} - -func TestClearOutputFlagDefault(t *testing.T) { - cmd := &cobra.Command{} - BindFlags(cmd.Flags()) - - // Initially, the default should be "table" - outputFlag := cmd.Flags().Lookup("output") - if outputFlag.DefValue != "table" { - t.Errorf("Expected default value to be 'table', got %q", outputFlag.DefValue) - } - - // Clear the default - ClearOutputFlagDefault(cmd) - - // After clearing, the default should be empty - if outputFlag.DefValue != "" { - t.Errorf("Expected default value to be empty after clearing, got %q", outputFlag.DefValue) - } -} - -func TestPrintWithFormat(t *testing.T) { - tests := []struct { - name string - outputFormat string - obj runtime.Object - expectPrinted bool - expectError bool - validateOutput func(t *testing.T, output string) - }{ - { - name: "empty format returns false", - outputFormat: "", - obj: createTestBackup("test-backup"), - expectPrinted: false, - expectError: false, - }, - { - name: "yaml format", - outputFormat: "yaml", - obj: createTestBackup("test-backup"), - expectPrinted: true, - expectError: false, - validateOutput: func(t *testing.T, output string) { - if !strings.Contains(output, "apiVersion: oadp.openshift.io/v1alpha1") { - t.Error("Expected YAML output to contain apiVersion") - } - if !strings.Contains(output, "kind: NonAdminBackup") { - t.Error("Expected YAML output to contain kind") - } - if !strings.Contains(output, "name: test-backup") { - t.Error("Expected YAML output to contain name") - } - }, - }, - { - name: "json format", - outputFormat: "json", - obj: createTestBackup("test-backup"), - expectPrinted: true, - expectError: false, - validateOutput: func(t *testing.T, output string) { - if !strings.Contains(output, `"apiVersion": "oadp.openshift.io/v1alpha1"`) { - t.Error("Expected JSON output to contain apiVersion") - } - if !strings.Contains(output, `"kind": "NonAdminBackup"`) { - t.Error("Expected JSON output to contain kind") - } - if !strings.Contains(output, `"name": "test-backup"`) { - t.Error("Expected JSON output to contain name") - } - }, - }, - { - name: "table format returns false", - outputFormat: "table", - obj: createTestBackup("test-backup"), - expectPrinted: false, - expectError: false, - }, - { - name: "invalid format returns error", - outputFormat: "invalid", - obj: createTestBackup("test-backup"), - expectPrinted: false, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a command with the output flag - cmd := &cobra.Command{} - BindFlags(cmd.Flags()) - if tt.outputFormat != "" { - if err := cmd.Flags().Set("output", tt.outputFormat); err != nil { - t.Fatalf("Failed to set output flag: %v", err) - } - } - - // Capture stdout - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - printed, err := PrintWithFormat(cmd, tt.obj) - - // Restore stdout and read captured output - w.Close() - os.Stdout = oldStdout - var buf bytes.Buffer - if _, err := io.Copy(&buf, r); err != nil { - t.Fatalf("Failed to read output: %v", err) - } - output := buf.String() - - // Check error expectation - if tt.expectError && err == nil { - t.Error("Expected error but got none") - } - if !tt.expectError && err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Check printed expectation - if printed != tt.expectPrinted { - t.Errorf("Expected printed=%v, got %v", tt.expectPrinted, printed) - } - - // Validate output if provided - if tt.validateOutput != nil { - tt.validateOutput(t, output) - } - }) - } -} - -func TestPrintWithFormatList(t *testing.T) { - tests := []struct { - name string - outputFormat string - obj runtime.Object - expectSingle bool // Should single item list be printed as single object? - validateCount func(t *testing.T, output string) - }{ - { - name: "single item list printed as single object in yaml", - outputFormat: "yaml", - obj: &nacv1alpha1.NonAdminBackupList{ - Items: []nacv1alpha1.NonAdminBackup{ - *createTestBackup("backup-1"), - }, - }, - expectSingle: true, - validateCount: func(t *testing.T, output string) { - // Single object should not have "items:" field - if strings.Contains(output, "items:") { - t.Error("Single item from list should not contain 'items:' field") - } - if !strings.Contains(output, "name: backup-1") { - t.Error("Expected output to contain backup name") - } - }, - }, - { - name: "multiple item list printed as list in yaml", - outputFormat: "yaml", - obj: &nacv1alpha1.NonAdminBackupList{ - Items: []nacv1alpha1.NonAdminBackup{ - *createTestBackup("backup-1"), - *createTestBackup("backup-2"), - }, - }, - expectSingle: false, - validateCount: func(t *testing.T, output string) { - // Multiple objects should have "items:" field - if !strings.Contains(output, "items:") { - t.Error("Multiple items should contain 'items:' field") - } - if !strings.Contains(output, "name: backup-1") { - t.Error("Expected output to contain first backup name") - } - if !strings.Contains(output, "name: backup-2") { - t.Error("Expected output to contain second backup name") - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmd := &cobra.Command{} - BindFlags(cmd.Flags()) - if err := cmd.Flags().Set("output", tt.outputFormat); err != nil { - t.Fatalf("Failed to set output flag: %v", err) - } - - // Capture stdout - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - _, err := PrintWithFormat(cmd, tt.obj) - - // Restore stdout and read captured output - w.Close() - os.Stdout = oldStdout - var buf bytes.Buffer - if _, copyErr := io.Copy(&buf, r); copyErr != nil { - t.Fatalf("Failed to read output: %v", copyErr) - } - output := buf.String() - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - if tt.validateCount != nil { - tt.validateCount(t, output) - } - }) - } -} - -func TestEncode(t *testing.T) { - tests := []struct { - name string - obj runtime.Object - format string - validate func(t *testing.T, data []byte) - }{ - { - name: "encode NonAdminBackup to yaml", - obj: createTestBackup("test-backup"), - format: "yaml", - validate: func(t *testing.T, data []byte) { - output := string(data) - if !strings.Contains(output, "apiVersion: oadp.openshift.io/v1alpha1") { - t.Error("Expected YAML to contain apiVersion") - } - if !strings.Contains(output, "kind: NonAdminBackup") { - t.Error("Expected YAML to contain kind") - } - }, - }, - { - name: "encode NonAdminBackup to json", - obj: createTestBackup("test-backup"), - format: "json", - validate: func(t *testing.T, data []byte) { - output := string(data) - if !strings.Contains(output, `"apiVersion"`) { - t.Error("Expected JSON to contain apiVersion") - } - if !strings.Contains(output, `"kind"`) { - t.Error("Expected JSON to contain kind") - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data, err := encode(tt.obj, tt.format) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - if tt.validate != nil { - tt.validate(t, data) - } - }) - } -} - -func TestEncoderFor(t *testing.T) { - tests := []struct { - name string - format string - obj runtime.Object - expectError bool - }{ - { - name: "yaml encoder", - format: "yaml", - obj: createTestBackup("test"), - expectError: false, - }, - { - name: "json encoder", - format: "json", - obj: createTestBackup("test"), - expectError: false, - }, - { - name: "invalid format", - format: "xml", - obj: createTestBackup("test"), - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - encoder, err := encoderFor(tt.format, tt.obj) - - if tt.expectError { - if err == nil { - t.Error("Expected error but got none") - } - } else { - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - if encoder == nil { - t.Error("Expected encoder but got nil") - } - } - }) - } -} - -func TestIsListType(t *testing.T) { - tests := []struct { - name string - obj runtime.Object - isList bool - }{ - { - name: "NonAdminBackupList is a list", - obj: &nacv1alpha1.NonAdminBackupList{}, - isList: true, - }, - { - name: "NonAdminBackup is not a list", - obj: &nacv1alpha1.NonAdminBackup{}, - isList: false, - }, - { - name: "NonAdminRestoreList is a list", - obj: &nacv1alpha1.NonAdminRestoreList{}, - isList: true, - }, - { - name: "NonAdminRestore is not a list", - obj: &nacv1alpha1.NonAdminRestore{}, - isList: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - isList := meta.IsListType(tt.obj) - if isList != tt.isList { - t.Errorf("Expected IsListType=%v, got %v", tt.isList, isList) - } - }) - } -} - -// Helper function to create a test NonAdminBackup -func createTestBackup(name string) *nacv1alpha1.NonAdminBackup { - return &nacv1alpha1.NonAdminBackup{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "oadp.openshift.io/v1alpha1", - Kind: "NonAdminBackup", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "test-namespace", - }, - Spec: nacv1alpha1.NonAdminBackupSpec{ - BackupSpec: &velerov1api.BackupSpec{ - IncludedNamespaces: []string{"test-namespace"}, - }, - }, - } -} diff --git a/cmd/non-admin/restore/README.md b/cmd/non-admin/restore/README.md deleted file mode 100644 index 699087643..000000000 --- a/cmd/non-admin/restore/README.md +++ /dev/null @@ -1,135 +0,0 @@ -# NonAdminRestore Create Command - -## Overview - -The `nonadmin restore create` command creates restore requests for non-admin users within their authorized namespaces. - -## Minimal MVP Flags - -The following flags represent the minimal viable product for restore creation (7 total): - -### Core Flags - -| Flag | Type | Default | Description | Status | -|------|------|---------|-------------|--------| -| `--backup-name` | String | - | Source backup (required) | ✅ MVP | -| `--include-resources` | StringArray | `["*"]` | Resources to include | ✅ MVP | -| `--exclude-resources` | StringArray | - | Resources to exclude | ✅ MVP | - -### Label Selection - -| Flag | Type | Default | Description | Status | -|------|------|---------|-------------|--------| -| `--selector`, `-l` | LabelSelector | - | Label selector | ✅ MVP | -| `--or-selector` | OrLabelSelector | - | OR label selectors | ✅ MVP | - -### Cluster Resources - -| Flag | Type | Default | Description | Status | -|------|------|---------|-------------|--------| -| `--include-cluster-resources` | OptionalBool | - | Include cluster resources | ✅ MVP | - -### Timing - -| Flag | Type | Default | Description | Status | -|------|------|---------|-------------|--------| -| `--item-operation-timeout` | Duration | - | Operation timeout | ✅ MVP | - -## Restricted Flags (Not Available) - -The following flags are **restricted** for non-admin users per the NAR API restrictions: - -| Flag | Reason | Doc Reference | -|------|--------|---------------| -| `--from-schedule` | Not supported for non-admin | NAR API docs | -| `--include-namespaces` | Restricted - automatically set | NAR API docs | -| `--exclude-namespaces` | Restricted for non-admin users | NAR API docs | -| `--namespace-mappings` | Restricted for non-admin users | NAR API docs | - -## Flags Not in MVP (Future Enhancements) - -The following flags are **allowed by the API** but not included in the minimal MVP: - -### Metadata -| Flag | Admin Enforceable | Could Add Later | -|------|------------------|-----------------| -| `--labels` | ✅ Yes | Future | -| `--annotations` | ✅ Yes | Future | - -### Restore Behavior -| Flag | Admin Enforceable | Could Add Later | -|------|------------------|-----------------| -| `--restore-volumes` | ✅ Yes | Future | -| `--preserve-nodeports` | ✅ Yes | Future | -| `--existing-resource-policy` | ✅ Yes | Future | - -### Advanced Features -| Flag | Admin Enforceable | Could Add Later | -|------|------------------|-----------------| -| `--resource-modifier-configmap` | ✅ Yes | Future | -| `--status-include-resources` | ✅ Yes | Future | -| `--status-exclude-resources` | ✅ Yes | Future | -| `--write-sparse-files` | ✅ Yes | Future | -| `--parallel-files-download` | ✅ Yes | Future | - -### UX Flags -| Flag | Purpose | Could Add Later | -|------|---------|-----------------| -| `--wait` | Wait for restore completion | Future | - -## Examples - -```bash -# Create a simple restore from a backup -oadp nonadmin restore create my-restore --backup-name my-backup - -# Create restore with specific resources -oadp nonadmin restore create my-restore \ - --backup-name my-backup \ - --include-resources deployments,services - -# Create restore excluding certain resources -oadp nonadmin restore create my-restore \ - --backup-name my-backup \ - --exclude-resources secrets - -# Create restore with label selector -oadp nonadmin restore create my-restore \ - --backup-name my-backup \ - --selector app=myapp - -# View the YAML without creating it -oadp nonadmin restore create my-restore \ - --backup-name my-backup \ - -o yaml -``` - -## Architecture Notes - -The restore create command uses **struct embedding** from Velero's restore CreateOptions. This approach: -- Reduces code duplication -- Ensures compatibility with Velero updates -- Uses BindFlags() as the control gate to expose only MVP features to non-admin users -- Maintains forward compatibility for future enhancements - -## Implementation Details - -### Struct Embedding Pattern - -```go -type CreateOptions struct { - *velerorestore.CreateOptions // Embed Velero's CreateOptions - - // NAR-specific fields - Name string - client kbclient.WithWatch - currentNamespace string -} -``` - -### MVP Flag Control - -The `BindFlags()` method acts as a control gate, exposing only the MVP flags while the embedded struct contains all Velero options. This allows: -- Easy addition of new flags in the future (just bind them in BindFlags) -- Automatic compatibility with Velero struct updates -- Clear separation between what's exposed vs what's available diff --git a/cmd/non-admin/restore/create.go b/cmd/non-admin/restore/create.go deleted file mode 100644 index 8d3e67a46..000000000 --- a/cmd/non-admin/restore/create.go +++ /dev/null @@ -1,195 +0,0 @@ -package restore - -/* -Copyright The Velero Contributors. - -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. -*/ - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/vmware-tanzu/velero/pkg/builder" - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd" - velerorestore "github.com/vmware-tanzu/velero/pkg/cmd/cli/restore" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" -) - -func NewCreateCommand(f client.Factory, use string) *cobra.Command { - o := NewCreateOptions() - - c := &cobra.Command{ - Use: use + " [NAME]", - Short: "Create a non-admin restore", - Args: cobra.MaximumNArgs(1), - Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(args, f)) - cmd.CheckError(o.Validate(c, args, f)) - cmd.CheckError(o.Run(c, f)) - }, - Example: ` # Create a non-admin restore from a backup (auto-generated name). - oc oadp nonadmin restore create --backup-name backup1 - - # Create a non-admin restore with a specific name. - oc oadp nonadmin restore create restore1 --backup-name backup1 - - # Create a non-admin restore with specific resource types. - oc oadp nonadmin restore create restore2 --backup-name backup1 --include-resources deployments,services - - # Create a non-admin restore excluding certain resources. - oc oadp nonadmin restore create restore3 --backup-name backup1 --exclude-resources secrets - - # Create a non-admin restore with label selector. - oc oadp nonadmin restore create restore4 --backup-name backup1 --selector app=myapp - - # View the YAML for a non-admin restore without sending it to the server. - oc oadp nonadmin restore create restore5 --backup-name backup1 -o yaml`, - } - - o.BindFlags(c.Flags()) - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -type CreateOptions struct { - *velerorestore.CreateOptions - - // NAR-specific fields - Name string // The NonAdminRestore resource name (maps to Velero's RestoreName) - client kbclient.WithWatch - currentNamespace string -} - -func NewCreateOptions() *CreateOptions { - return &CreateOptions{ - CreateOptions: velerorestore.NewCreateOptions(), - } -} - -func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { - - flags.StringVar(&o.BackupName, "backup-name", "", "The backup to restore from.") - - // Label selection - flags.VarP(&o.Selector, "selector", "l", "Only restore resources matching this label selector.") - flags.Var(&o.OrSelector, "or-selector", "Restore resources matching at least one of the label selector from the list. Label selectors should be separated by ' or '. For example, foo=bar or app=nginx") - - flags.DurationVar(&o.ItemOperationTimeout, "item-operation-timeout", o.ItemOperationTimeout, "How long to wait for async plugin operations before timeout.") - - flags.Var(&o.IncludeResources, "include-resources", "Resources to include in the restore, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources).") - flags.Var(&o.ExcludeResources, "exclude-resources", "Resources to exclude from the restore, formatted as resource.group, such as storageclasses.storage.k8s.io.") - - f := flags.VarPF(&o.IncludeClusterResources, "include-cluster-resources", "", "Include cluster-scoped resources in the restore.") - f.NoOptDefVal = cmd.TRUE -} - -func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Factory) error { - if err := output.ValidateFlags(c); err != nil { - return err - } - - // Must specify backup-name - if o.BackupName == "" { - return fmt.Errorf("--backup-name is required") - } - - if o.Selector.LabelSelector != nil && o.OrSelector.OrLabelSelectors != nil { - return fmt.Errorf("either a 'selector' or an 'or-selector' can be specified, but not both") - } - - return nil -} - -func (o *CreateOptions) Complete(args []string, f client.Factory) error { - // Name is optional - if not provided, will use GenerateName in the builder - if len(args) > 0 { - o.Name = args[0] - } else { - o.Name = "" - } - - // Create client with NonAdmin scheme - client, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeNonAdminTypes: true, - }) - if err != nil { - return err - } - - // Get the current namespace from kubeconfig instead of using factory namespace - currentNS, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - - o.client = client - o.currentNamespace = currentNS - return nil -} - -func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { - nonAdminRestore, err := o.BuildNonAdminRestore(o.currentNamespace) - if err != nil { - return err - } - - if printed, err := output.PrintWithFormat(c, nonAdminRestore); printed || err != nil { - return err - } - - // Create the restore - if err := o.client.Create(context.TODO(), nonAdminRestore, &kbclient.CreateOptions{}); err != nil { - return err - } - - // Use the actual name (either provided or auto-generated by the API server) - actualName := nonAdminRestore.Name - fmt.Printf("NonAdminRestore request %q submitted successfully.\n", actualName) - fmt.Printf("Run `oc oadp nonadmin restore describe %s` or `oc oadp nonadmin restore logs %s` for more details.\n", actualName, actualName) - return nil -} - -func (o *CreateOptions) BuildNonAdminRestore(namespace string) (*nacv1alpha1.NonAdminRestore, error) { - // Use Velero's builder for RestoreSpec - restoreBuilder := builder.ForRestore(namespace, o.Name). - Backup(o.BackupName). - IncludedResources(o.IncludeResources...). - ExcludedResources(o.ExcludeResources...). - LabelSelector(o.Selector.LabelSelector). - OrLabelSelector(o.OrSelector.OrLabelSelectors). - ItemOperationTimeout(o.ItemOperationTimeout) - - // Apply optional include-cluster-resources flag - if o.IncludeClusterResources.Value != nil { - restoreBuilder.IncludeClusterResources(*o.IncludeClusterResources.Value) - } - - tempRestore := restoreBuilder.Result() - - // Wrap in NonAdminRestore - return NewNonAdminRestoreBuilder(namespace, o.Name). - RestoreSpec(nacv1alpha1.NonAdminRestoreSpec{ - RestoreSpec: &tempRestore.Spec, - }). - Result(), nil -} diff --git a/cmd/non-admin/restore/delete.go b/cmd/non-admin/restore/delete.go deleted file mode 100644 index 3923999e4..000000000 --- a/cmd/non-admin/restore/delete.go +++ /dev/null @@ -1,298 +0,0 @@ -package restore - -/* -Copyright The Velero Contributors. - -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. -*/ - -import ( - "bufio" - "context" - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "k8s.io/apimachinery/pkg/api/errors" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" -) - -// NewDeleteCommand creates a cobra command for deleting non-admin restores -func NewDeleteCommand(f client.Factory, use string) *cobra.Command { - o := NewDeleteOptions() - - c := &cobra.Command{ - Use: use + " [NAME...] | --all", - Short: "Delete one or more non-admin restores", - Long: "Delete one or more non-admin restores. Use --all to delete all restores in the current namespace.", - Args: func(cmd *cobra.Command, args []string) error { - // Check if --all flag is set - allFlag, _ := cmd.Flags().GetBool("all") - if allFlag { - return cobra.NoArgs(cmd, args) - } - return cobra.MinimumNArgs(1)(cmd, args) - }, - Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(args, f)) - cmd.CheckError(o.Validate()) - cmd.CheckError(o.Run()) - }, - Example: ` # Delete a specific restore - oc oadp nonadmin restore delete my-restore - - # Delete multiple restores - oc oadp nonadmin restore delete restore1 restore2 restore3 - - # Delete all restores in the current namespace - oc oadp nonadmin restore delete --all - - # Delete without confirmation prompt - oc oadp nonadmin restore delete my-restore --confirm`, - } - - o.BindFlags(c.Flags()) - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -// DeleteOptions holds the options for the delete command -type DeleteOptions struct { - Names []string - Namespace string // Internal field - automatically determined from kubectl context - Confirm bool // Skip confirmation prompt - All bool // Delete all restores in namespace - client kbclient.Client -} - -// NewDeleteOptions creates a new DeleteOptions instance -func NewDeleteOptions() *DeleteOptions { - return &DeleteOptions{} -} - -// BindFlags binds the command line flags to the options -func (o *DeleteOptions) BindFlags(flags *pflag.FlagSet) { - flags.BoolVar(&o.Confirm, "confirm", false, "Skip confirmation prompt and delete immediately") - flags.BoolVar(&o.All, "all", false, "Delete all restores in the current namespace") -} - -// Complete completes the options by setting up the client and determining the namespace -func (o *DeleteOptions) Complete(args []string, f client.Factory) error { - o.Names = args - - // Create client with NonAdmin scheme - kbClient, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeNonAdminTypes: true, - }) - if err != nil { - return err - } - - o.client = kbClient - - // Always use the current namespace from kubectl context - currentNS, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - o.Namespace = currentNS - - // If --all flag is used, list all restores in the namespace - if o.All { - var narList nacv1alpha1.NonAdminRestoreList - err := o.client.List(context.TODO(), &narList, &kbclient.ListOptions{ - Namespace: o.Namespace, - }) - if err != nil { - return fmt.Errorf("failed to list restores: %w", err) - } - - // Extract restore names - o.Names = make([]string, 0, len(narList.Items)) - for _, nar := range narList.Items { - o.Names = append(o.Names, nar.Name) - } - - if len(o.Names) == 0 { - return fmt.Errorf("no restores found in namespace '%s'", o.Namespace) - } - } - - return nil -} - -// Validate validates the options -func (o *DeleteOptions) Validate() error { - if !o.All && len(o.Names) == 0 { - return fmt.Errorf("at least one restore name is required, or use --all to delete all restores") - } - if o.Namespace == "" { - return fmt.Errorf("namespace is required") - } - return nil -} - -// Run executes the delete command -func (o *DeleteOptions) Run() error { - // Show what will be deleted - if o.All { - fmt.Printf("All NonAdminRestore(s) in namespace '%s' will be deleted:\n", o.Namespace) - } else { - fmt.Printf("The following NonAdminRestore(s) will be deleted in namespace '%s':\n", o.Namespace) - } - for _, name := range o.Names { - fmt.Printf(" - %s\n", name) - } - fmt.Println() - - // Prompt for confirmation unless --confirm flag is used - if !o.Confirm { - confirmed, err := o.promptForConfirmation() - if err != nil { - return err - } - if !confirmed { - fmt.Println("Deletion cancelled.") - return nil - } - } - - // Track results - var successful []string - var failed []string - - // Process each restore - for _, name := range o.Names { - err := o.deleteRestore(name) - if err != nil { - fmt.Printf("❌ Failed to delete %s: %v\n", name, err) - failed = append(failed, name) - } else { - fmt.Printf("✓ %s deleted successfully\n", name) - successful = append(successful, name) - } - } - - // Print summary - fmt.Println() - if len(successful) > 0 { - fmt.Printf("Successfully deleted %d restore(s)\n", len(successful)) - } - - if len(failed) > 0 { - fmt.Printf("Failed to delete %d restore(s):\n", len(failed)) - for _, name := range failed { - fmt.Printf(" - %s\n", name) - } - return fmt.Errorf("some operations failed") - } - - return nil -} - -// promptForConfirmation prompts the user for confirmation -func (o *DeleteOptions) promptForConfirmation() (bool, error) { - reader := bufio.NewReader(os.Stdin) - - if o.All { - fmt.Printf("Are you sure you want to delete ALL %d restore(s) in namespace '%s'? (y/N): ", len(o.Names), o.Namespace) - } else if len(o.Names) == 1 { - fmt.Printf("Are you sure you want to delete restore '%s'? (y/N): ", o.Names[0]) - } else { - fmt.Printf("Are you sure you want to delete these %d restores? (y/N): ", len(o.Names)) - } - - response, err := reader.ReadString('\n') - if err != nil { - return false, fmt.Errorf("failed to read user input: %w", err) - } - - response = strings.TrimSpace(strings.ToLower(response)) - return response == "y" || response == "yes", nil -} - -// deleteRestore deletes a single restore -func (o *DeleteOptions) deleteRestore(name string) error { - // Get the NonAdminRestore resource - nar := &nacv1alpha1.NonAdminRestore{} - err := o.client.Get(context.TODO(), kbclient.ObjectKey{ - Name: name, - Namespace: o.Namespace, - }, nar) - if err != nil { - return o.translateError(name, err) - } - - // Delete the resource - err = o.client.Delete(context.TODO(), nar) - if err != nil { - return o.translateError(name, err) - } - - return nil -} - -// translateError converts verbose Kubernetes errors into user-friendly messages -func (o *DeleteOptions) translateError(name string, err error) error { - if errors.IsNotFound(err) { - return fmt.Errorf("restore '%s' not found", name) - } - - if errors.IsForbidden(err) { - return fmt.Errorf("permission denied") - } - - if errors.IsUnauthorized(err) { - return fmt.Errorf("authentication required") - } - - if errors.IsConflict(err) { - return fmt.Errorf("restore '%s' was modified, please try again", name) - } - - if errors.IsTimeout(err) { - return fmt.Errorf("request timed out") - } - - if errors.IsServerTimeout(err) { - return fmt.Errorf("server timeout") - } - - if errors.IsServiceUnavailable(err) { - return fmt.Errorf("service unavailable") - } - - // Check for common connection issues - errStr := err.Error() - if strings.Contains(errStr, "connection refused") { - return fmt.Errorf("cannot connect to cluster") - } - - if strings.Contains(errStr, "no such host") { - return fmt.Errorf("cannot reach cluster") - } - - // For any other error, provide a generic message - return fmt.Errorf("operation failed") -} diff --git a/cmd/non-admin/restore/describe.go b/cmd/non-admin/restore/describe.go deleted file mode 100644 index 9c7604d70..000000000 --- a/cmd/non-admin/restore/describe.go +++ /dev/null @@ -1,544 +0,0 @@ -package restore - -import ( - "context" - "encoding/json" - "fmt" - "sort" - "strings" - "time" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -func NewDescribeCommand(f client.Factory, use string) *cobra.Command { - var ( - requestTimeout time.Duration - details bool - ) - - c := &cobra.Command{ - Use: use + " NAME", - Short: "Describe a non-admin restore", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - restoreName := args[0] - - // Get effective timeout (flag takes precedence over env var) - effectiveTimeout := shared.GetHTTPTimeoutWithOverride(requestTimeout) - - // Create context with the effective timeout - ctx, cancel := context.WithTimeout(context.Background(), effectiveTimeout) - defer cancel() - - // Get the current namespace from kubectl context - userNamespace, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - - // Create client with required scheme types and timeout - kbClient, err := shared.NewClientWithScheme(f, shared.ClientOptions{ - IncludeNonAdminTypes: true, - IncludeVeleroTypes: true, - IncludeCoreTypes: true, - Timeout: effectiveTimeout, - }) - if err != nil { - return err - } - - // Get the specific restore - var nar nacv1alpha1.NonAdminRestore - if err := kbClient.Get(ctx, kbclient.ObjectKey{ - Namespace: userNamespace, - Name: restoreName, - }, &nar); err != nil { - // Check for context cancellation - if ctx.Err() == context.DeadlineExceeded { - return fmt.Errorf("timed out after %v getting NonAdminRestore %q", effectiveTimeout, restoreName) - } - if ctx.Err() == context.Canceled { - return fmt.Errorf("operation cancelled: %w", ctx.Err()) - } - return fmt.Errorf("NonAdminRestore %q not found in namespace %q: %w", restoreName, userNamespace, err) - } - - // Print in Velero-style format - printNonAdminRestoreDetails(cmd, &nar, kbClient, restoreName, userNamespace, effectiveTimeout) - - // Add detailed output if --details flag is set - if details { - if err := printDetailedRestoreInfo(cmd, kbClient, restoreName, userNamespace, effectiveTimeout); err != nil { - return fmt.Errorf("failed to fetch detailed restore information: %w", err) - } - } - - return nil - }, - Example: ` oc oadp nonadmin restore describe my-restore - oc oadp nonadmin restore describe my-restore --details - oc oadp nonadmin restore describe my-restore --details --request-timeout=30m`, - } - - c.Flags().DurationVar(&requestTimeout, "request-timeout", 0, fmt.Sprintf("The length of time to wait before giving up on a single server request (e.g., 30s, 5m, 1h). Overrides %s env var. Default: %v", shared.TimeoutEnvVar, shared.DefaultHTTPTimeout)) - c.Flags().BoolVar(&details, "details", false, "Display additional restore details including resource lists and item operations") - - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -// printNonAdminRestoreDetails prints restore details in Velero admin describe format -func printNonAdminRestoreDetails(cmd *cobra.Command, nar *nacv1alpha1.NonAdminRestore, kbClient kbclient.Client, restoreName string, userNamespace string, timeout time.Duration) { - out := cmd.OutOrStdout() - - // Get Velero restore reference if available - var vr *nacv1alpha1.VeleroRestore - if nar.Status.VeleroRestore != nil { - vr = nar.Status.VeleroRestore - } - - // Name and Namespace - fmt.Fprintf(out, "Name: %s\n", nar.Name) - fmt.Fprintf(out, "Namespace: %s\n", nar.Namespace) - - // Labels - fmt.Fprintf(out, "Labels: ") - if len(nar.Labels) == 0 { - fmt.Fprintf(out, "\n") - } else { - labelKeys := make([]string, 0, len(nar.Labels)) - for k := range nar.Labels { - labelKeys = append(labelKeys, k) - } - sort.Strings(labelKeys) - for i, k := range labelKeys { - if i == 0 { - fmt.Fprintf(out, "%s=%s\n", k, nar.Labels[k]) - } else { - fmt.Fprintf(out, " %s=%s\n", k, nar.Labels[k]) - } - } - } - - // Annotations - fmt.Fprintf(out, "Annotations: ") - if len(nar.Annotations) == 0 { - fmt.Fprintf(out, "\n") - } else { - annotationKeys := make([]string, 0, len(nar.Annotations)) - for k := range nar.Annotations { - annotationKeys = append(annotationKeys, k) - } - sort.Strings(annotationKeys) - for i, k := range annotationKeys { - if i == 0 { - fmt.Fprintf(out, "%s=%s\n", k, nar.Annotations[k]) - } else { - fmt.Fprintf(out, " %s=%s\n", k, nar.Annotations[k]) - } - } - } - - fmt.Fprintf(out, "\n") - - // Phase (with color) - phase := string(nar.Status.Phase) - if vr != nil && vr.Status != nil && vr.Status.Phase != "" { - phase = string(vr.Status.Phase) - } - fmt.Fprintf(out, "Phase: %s\n", colorizePhase(phase)) - - fmt.Fprintf(out, "\n") - - // Restore Spec details - if nar.Spec.RestoreSpec != nil { - spec := nar.Spec.RestoreSpec - - // Source Backup - if spec.BackupName != "" { - fmt.Fprintf(out, "Backup: %s\n", spec.BackupName) - } else { - fmt.Fprintf(out, "Backup: \n") - } - - fmt.Fprintf(out, "\n") - - // Namespaces - fmt.Fprintf(out, "Namespaces:\n") - if len(spec.IncludedNamespaces) == 0 { - fmt.Fprintf(out, " Included: *\n") - } else { - fmt.Fprintf(out, " Included: %s\n", strings.Join(spec.IncludedNamespaces, ", ")) - } - if len(spec.ExcludedNamespaces) == 0 { - fmt.Fprintf(out, " Excluded: \n") - } else { - fmt.Fprintf(out, " Excluded: %s\n", strings.Join(spec.ExcludedNamespaces, ", ")) - } - - fmt.Fprintf(out, "\n") - - // Namespace Mappings - if len(spec.NamespaceMapping) == 0 { - fmt.Fprintf(out, "Namespace mappings: \n") - } else { - fmt.Fprintf(out, "Namespace mappings:\n") - // Sort the mappings for consistent output - mappingKeys := make([]string, 0, len(spec.NamespaceMapping)) - for k := range spec.NamespaceMapping { - mappingKeys = append(mappingKeys, k) - } - sort.Strings(mappingKeys) - for _, from := range mappingKeys { - fmt.Fprintf(out, " %s: %s\n", from, spec.NamespaceMapping[from]) - } - } - - fmt.Fprintf(out, "\n") - - // Resources - fmt.Fprintf(out, "Resources:\n") - if len(spec.IncludedResources) == 0 { - fmt.Fprintf(out, " Included: *\n") - } else { - fmt.Fprintf(out, " Included: %s\n", strings.Join(spec.IncludedResources, ", ")) - } - if len(spec.ExcludedResources) == 0 { - fmt.Fprintf(out, " Excluded: \n") - } else { - fmt.Fprintf(out, " Excluded: %s\n", strings.Join(spec.ExcludedResources, ", ")) - } - if spec.IncludeClusterResources != nil { - if *spec.IncludeClusterResources { - fmt.Fprintf(out, " Cluster-scoped: included\n") - } else { - fmt.Fprintf(out, " Cluster-scoped: excluded\n") - } - } else { - fmt.Fprintf(out, " Cluster-scoped: auto\n") - } - - fmt.Fprintf(out, "\n") - - // Label selector - if spec.LabelSelector != nil && len(spec.LabelSelector.MatchLabels) > 0 { - var selectorParts []string - for k, v := range spec.LabelSelector.MatchLabels { - selectorParts = append(selectorParts, fmt.Sprintf("%s=%s", k, v)) - } - fmt.Fprintf(out, "Label selector: %s\n", strings.Join(selectorParts, ",")) - } else { - fmt.Fprintf(out, "Label selector: \n") - } - - fmt.Fprintf(out, "\n") - fmt.Fprintf(out, "Or label selector: \n") - fmt.Fprintf(out, "\n") - - // Restore PVs setting - if spec.RestorePVs != nil { - if *spec.RestorePVs { - fmt.Fprintf(out, "Restore PVs: true\n") - } else { - fmt.Fprintf(out, "Restore PVs: false\n") - } - } else { - fmt.Fprintf(out, "Restore PVs: auto\n") - } - - fmt.Fprintf(out, "\n") - - // Existing Resource Policy - if spec.ExistingResourcePolicy != "" { - fmt.Fprintf(out, "Existing Resource Policy: %s\n", spec.ExistingResourcePolicy) - } else { - fmt.Fprintf(out, "Existing Resource Policy: \n") - } - - fmt.Fprintf(out, "\n") - - // Item Operation Timeout - if spec.ItemOperationTimeout.Duration > 0 { - fmt.Fprintf(out, "ItemOperationTimeout: %s\n", spec.ItemOperationTimeout.Duration) - } else { - fmt.Fprintf(out, "ItemOperationTimeout: 4h0m0s\n") - } - - fmt.Fprintf(out, "\n") - - // Hooks - if len(spec.Hooks.Resources) > 0 { - fmt.Fprintf(out, "Hooks: %d resources with hooks\n", len(spec.Hooks.Resources)) - } else { - fmt.Fprintf(out, "Hooks: \n") - } - - fmt.Fprintf(out, "\n") - } - - // Velero restore status information - if vr != nil && vr.Status != nil { - status := vr.Status - - // Started and Completed times - if !status.StartTimestamp.IsZero() { - fmt.Fprintf(out, "Started: %s\n", status.StartTimestamp.Format("2006-01-02 15:04:05 -0700 MST")) - } - if !status.CompletionTimestamp.IsZero() { - fmt.Fprintf(out, "Completed: %s\n", status.CompletionTimestamp.Format("2006-01-02 15:04:05 -0700 MST")) - } - - fmt.Fprintf(out, "\n") - - // Progress - if status.Progress != nil { - fmt.Fprintf(out, "Total items to be restored: %d\n", status.Progress.TotalItems) - fmt.Fprintf(out, "Items restored: %d\n", status.Progress.ItemsRestored) - } - - fmt.Fprintf(out, "\n") - - // Warnings and Errors - if status.Warnings > 0 { - fmt.Fprintf(out, "Warnings: %d\n", status.Warnings) - } - if status.Errors > 0 { - fmt.Fprintf(out, "Errors: %d\n", status.Errors) - } - - fmt.Fprintf(out, "\n") - - // Hooks - if status.HookStatus != nil { - fmt.Fprintf(out, "HooksAttempted: %d\n", status.HookStatus.HooksAttempted) - fmt.Fprintf(out, "HooksFailed: %d\n", status.HookStatus.HooksFailed) - } else { - fmt.Fprintf(out, "HooksAttempted: \n") - fmt.Fprintf(out, "HooksFailed: \n") - } - } else { - // Velero restore not available yet - fmt.Fprintf(out, "Velero restore information not yet available.\n") - fmt.Fprintf(out, "Request Phase: %s\n", nar.Status.Phase) - } -} - -// printDetailedRestoreInfo fetches and displays additional restore details when --details flag is used. -// It uses NonAdminDownloadRequest to fetch: -// - RestoreResourceList (list of restored resources) -// - RestoreResults (errors, warnings) -// - RestoreItemOperations (plugin operations) -func printDetailedRestoreInfo(cmd *cobra.Command, kbClient kbclient.Client, restoreName string, userNamespace string, timeout time.Duration) error { - out := cmd.OutOrStdout() - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - hasOutput := false - - // 1. Fetch RestoreResourceList - resourceList, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: restoreName, - DataType: "RestoreResourceList", - Namespace: userNamespace, - HTTPTimeout: timeout, - }) - - if err == nil && resourceList != "" { - if formattedList := formatRestoreResourceList(resourceList); formattedList != "" { - if !hasOutput { - fmt.Fprintf(out, "\n") - hasOutput = true - } - fmt.Fprintf(out, "Resource List:\n") - fmt.Fprintf(out, "%s\n", formattedList) - fmt.Fprintf(out, "\n") - } - } - - // 2. Fetch RestoreResults - results, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: restoreName, - DataType: "RestoreResults", - Namespace: userNamespace, - HTTPTimeout: timeout, - }) - - if err == nil && results != "" { - if formattedResults := formatRestoreResults(results); formattedResults != "" { - if !hasOutput { - fmt.Fprintf(out, "\n") - hasOutput = true - } - fmt.Fprintf(out, "Restore Results:\n") - fmt.Fprintf(out, "%s\n", formattedResults) - fmt.Fprintf(out, "\n") - } - } - - // 3. Fetch RestoreItemOperations - itemOps, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: restoreName, - DataType: "RestoreItemOperations", - Namespace: userNamespace, - HTTPTimeout: timeout, - }) - - if err == nil && itemOps != "" { - if formattedOps := formatRestoreItemOperations(itemOps); formattedOps != "" { - if !hasOutput { - fmt.Fprintf(out, "\n") - } - fmt.Fprintf(out, "Restore Item Operations:\n") - fmt.Fprintf(out, "%s\n", formattedOps) - fmt.Fprintf(out, "\n") - } - } - - return nil -} - -// formatRestoreResourceList formats the resource list for display -func formatRestoreResourceList(resourceList string) string { - if strings.TrimSpace(resourceList) == "" { - return "" - } - - // Try to parse as JSON map - var resources map[string][]string - if err := json.Unmarshal([]byte(resourceList), &resources); err != nil { - // If parsing fails, fall back to indented output - return indent(resourceList, " ") - } - - // Sort the keys (GroupVersionKind) - keys := make([]string, 0, len(resources)) - for k := range resources { - keys = append(keys, k) - } - sort.Strings(keys) - - // Build formatted output - var output strings.Builder - for _, gvk := range keys { - items := resources[gvk] - fmt.Fprintf(&output, " %s:\n", gvk) - for _, item := range items { - fmt.Fprintf(&output, " - %s\n", item) - } - } - - return strings.TrimSuffix(output.String(), "\n") -} - -// formatRestoreResults formats restore results (errors/warnings) for display -func formatRestoreResults(results string) string { - if strings.TrimSpace(results) == "" { - return "" - } - - // Try to parse as JSON object with errors and warnings - var resultsObj struct { - Errors map[string]interface{} `json:"errors"` - Warnings map[string]interface{} `json:"warnings"` - } - if err := json.Unmarshal([]byte(results), &resultsObj); err != nil { - // If parsing fails, fall back to indented output - return indent(results, " ") - } - - // If both are empty, return empty string so section won't be printed - if len(resultsObj.Errors) == 0 && len(resultsObj.Warnings) == 0 { - return "" - } - - // Format nicely - var output strings.Builder - - // Show errors - output.WriteString(" Errors:\n") - if len(resultsObj.Errors) > 0 { - formatted, _ := json.MarshalIndent(resultsObj.Errors, " ", " ") - output.WriteString(indent(string(formatted), " ")) - } else { - output.WriteString(" ") - } - output.WriteString("\n\n") - - // Show warnings - output.WriteString(" Warnings:\n") - if len(resultsObj.Warnings) > 0 { - formatted, _ := json.MarshalIndent(resultsObj.Warnings, " ", " ") - output.WriteString(indent(string(formatted), " ")) - } else { - output.WriteString(" ") - } - - return strings.TrimSuffix(output.String(), "\n") -} - -// formatRestoreItemOperations formats restore item operations for display -func formatRestoreItemOperations(itemOps string) string { - if strings.TrimSpace(itemOps) == "" { - return "" - } - - // Try to parse as JSON array - var operations []interface{} - if err := json.Unmarshal([]byte(itemOps), &operations); err != nil { - // If parsing fails, fall back to indented output - return indent(itemOps, " ") - } - - // If empty array, return empty string (will show "") - if len(operations) == 0 { - return "" - } - - // Format as indented JSON for readability - formatted, err := json.MarshalIndent(operations, " ", " ") - if err != nil { - return indent(itemOps, " ") - } - return indent(string(formatted), " ") -} - -// colorizePhase returns the phase string with ANSI color codes -func colorizePhase(phase string) string { - const ( - colorGreen = "\033[32m" - colorYellow = "\033[33m" - colorRed = "\033[31m" - colorReset = "\033[0m" - ) - - switch phase { - case "Completed": - return colorGreen + phase + colorReset - case "InProgress", "New": - return colorYellow + phase + colorReset - case "Failed", "FailedValidation", "PartiallyFailed": - return colorRed + phase + colorReset - default: - return phase - } -} - -// Helper to indent YAML blocks -func indent(s, prefix string) string { - lines := strings.Split(s, "\n") - for i, line := range lines { - if len(line) > 0 { - lines[i] = prefix + line - } - } - return strings.Join(lines, "\n") -} diff --git a/cmd/non-admin/restore/get.go b/cmd/non-admin/restore/get.go deleted file mode 100644 index ebc85422e..000000000 --- a/cmd/non-admin/restore/get.go +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright The Velero Contributors. - -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 restore - -import ( - "context" - "fmt" - "time" - - "github.com/migtools/oadp-cli/cmd/non-admin/output" - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -func NewGetCommand(f client.Factory, use string) *cobra.Command { - c := &cobra.Command{ - Use: use + " [NAME]", - Short: "Get non-admin restore(s)", - Long: "Get one or more non-admin restores", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // Get the current namespace from kubectl context - userNamespace, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - - // Create client with full scheme - kbClient, err := shared.NewClientWithFullScheme(f) - if err != nil { - return err - } - - if len(args) == 1 { - // Get specific restore - restoreName := args[0] - var nar nacv1alpha1.NonAdminRestore - err := kbClient.Get(context.Background(), kbclient.ObjectKey{ - Namespace: userNamespace, - Name: restoreName, - }, &nar) - if err != nil { - return fmt.Errorf("failed to get NonAdminRestore %q: %w", restoreName, err) - } - - if printed, err := output.PrintWithFormat(cmd, &nar); printed || err != nil { - return err - } - - // If no output format specified, print table format for single item - list := &nacv1alpha1.NonAdminRestoreList{ - Items: []nacv1alpha1.NonAdminRestore{nar}, - } - return printNonAdminRestoreTable(list) - } else { - // List all restores in namespace - var narList nacv1alpha1.NonAdminRestoreList - err := kbClient.List(context.Background(), &narList, &kbclient.ListOptions{ - Namespace: userNamespace, - }) - if err != nil { - return fmt.Errorf("failed to list NonAdminRestores: %w", err) - } - - if printed, err := output.PrintWithFormat(cmd, &narList); printed || err != nil { - return err - } - - // Print table format - return printNonAdminRestoreTable(&narList) - } - }, - Example: ` # Get all non-admin restores in the current namespace - oc oadp nonadmin restore get - - # Get a specific non-admin restore - oc oadp nonadmin restore get my-restore - - # Get restores in YAML format - oc oadp nonadmin restore get -o yaml - - # Get a specific restore in JSON format - oc oadp nonadmin restore get my-restore -o json`, - } - - output.BindFlags(c.Flags()) - output.ClearOutputFlagDefault(c) - - return c -} - -func printNonAdminRestoreTable(narList *nacv1alpha1.NonAdminRestoreList) error { - if len(narList.Items) == 0 { - fmt.Println("No non-admin restores found.") - return nil - } - - // Print header - fmt.Printf("%-30s %-15s %-15s %-20s %-10s %-10s\n", "NAME", "REQUEST PHASE", "VELERO PHASE", "CREATED", "AGE", "DURATION") - - // Print each restore - for _, nar := range narList.Items { - status := getRestoreStatus(&nar) - veleroPhase := getVeleroRestorePhase(&nar) - created := nar.CreationTimestamp.Format("2006-01-02 15:04:05") - age := formatAge(nar.CreationTimestamp.Time) - duration := getRestoreDuration(&nar) - - fmt.Printf("%-30s %-15s %-15s %-20s %-10s %-10s\n", nar.Name, status, veleroPhase, created, age, duration) - } - - return nil -} - -func getRestoreStatus(nar *nacv1alpha1.NonAdminRestore) string { - if nar.Status.Phase != "" { - return string(nar.Status.Phase) - } - return "Unknown" -} - -func getVeleroRestorePhase(nar *nacv1alpha1.NonAdminRestore) string { - if nar.Status.VeleroRestore != nil && nar.Status.VeleroRestore.Status != nil { - if nar.Status.VeleroRestore.Status.Phase != "" { - return string(nar.Status.VeleroRestore.Status.Phase) - } - } - return "N/A" -} - -func getRestoreDuration(nar *nacv1alpha1.NonAdminRestore) string { - // Check if we have completion timestamp - if nar.Status.VeleroRestore != nil && nar.Status.VeleroRestore.Status != nil { - if !nar.Status.VeleroRestore.Status.CompletionTimestamp.IsZero() { - // Calculate duration from request creation to completion - duration := nar.Status.VeleroRestore.Status.CompletionTimestamp.Sub(nar.CreationTimestamp.Time) - return formatDuration(duration) - } - } - return "N/A" -} - -func formatDuration(d time.Duration) string { - if d < time.Minute { - return fmt.Sprintf("%ds", int(d.Seconds())) - } else if d < time.Hour { - return fmt.Sprintf("%dm%ds", int(d.Minutes()), int(d.Seconds())%60) - } else { - hours := int(d.Hours()) - minutes := int(d.Minutes()) % 60 - return fmt.Sprintf("%dh%dm", hours, minutes) - } -} - -func formatAge(t time.Time) string { - duration := time.Since(t) - - days := int(duration.Hours() / 24) - hours := int(duration.Hours()) % 24 - minutes := int(duration.Minutes()) % 60 - - if days > 0 { - return fmt.Sprintf("%dd", days) - } else if hours > 0 { - return fmt.Sprintf("%dh", hours) - } else if minutes > 0 { - return fmt.Sprintf("%dm", minutes) - } else { - return "1m" - } -} diff --git a/cmd/non-admin/restore/logs.go b/cmd/non-admin/restore/logs.go deleted file mode 100644 index 81bb1b354..000000000 --- a/cmd/non-admin/restore/logs.go +++ /dev/null @@ -1,145 +0,0 @@ -package restore - -/* -Copyright The Velero Contributors. - -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. -*/ - -import ( - "context" - "fmt" - "net" - "time" - - "github.com/migtools/oadp-cli/cmd/shared" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -func NewLogsCommand(f client.Factory, use string) *cobra.Command { - var requestTimeout time.Duration - - c := &cobra.Command{ - Use: use + " NAME", - Short: "Show logs for a non-admin restore", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // Get effective timeout (flag takes precedence over env var) - effectiveTimeout := shared.GetHTTPTimeoutWithOverride(requestTimeout) - - // Create context with the effective timeout for the entire operation - ctx, cancel := context.WithTimeout(context.Background(), effectiveTimeout) - defer cancel() - - // Get the current namespace from kubectl context - userNamespace, err := shared.GetCurrentNamespace() - if err != nil { - return fmt.Errorf("failed to determine current namespace: %w", err) - } - restoreName := args[0] - - // Create scheme with required types - scheme, err := shared.NewSchemeWithTypes(shared.ClientOptions{ - IncludeNonAdminTypes: true, - IncludeVeleroTypes: true, - }) - if err != nil { - return err - } - - restConfig, err := f.ClientConfig() - if err != nil { - return fmt.Errorf("failed to get rest config: %w", err) - } - // Set timeout on REST config to prevent hanging when cluster is unreachable - restConfig.Timeout = effectiveTimeout - - // Set a custom dial function with timeout to ensure TCP connection attempts - // also respect the timeout (the default TCP dial timeout is ~30s) - dialer := &net.Dialer{ - Timeout: effectiveTimeout, - } - restConfig.Dial = func(ctx context.Context, network, address string) (net.Conn, error) { - return dialer.DialContext(ctx, network, address) - } - - kbClient, err := kbclient.New(restConfig, kbclient.Options{Scheme: scheme}) - if err != nil { - return fmt.Errorf("failed to create controller-runtime client: %w", err) - } - - // Verify the NonAdminRestore exists before creating download request - var nar nacv1alpha1.NonAdminRestore - if err := kbClient.Get(ctx, kbclient.ObjectKey{ - Namespace: userNamespace, - Name: restoreName, - }, &nar); err != nil { - return fmt.Errorf("failed to get NonAdminRestore %q: %w", restoreName, err) - } - - fmt.Fprintf(cmd.OutOrStdout(), "Waiting for restore logs to be processed (timeout: %v)...\n", effectiveTimeout) - - // Create download request and wait for signed URL - req, signedURL, err := shared.CreateAndWaitForDownloadURL(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: restoreName, - DataType: "RestoreLog", - Namespace: userNamespace, - Timeout: effectiveTimeout, - PollInterval: 2 * time.Second, - HTTPTimeout: effectiveTimeout, - OnProgress: func() { - fmt.Fprintf(cmd.OutOrStdout(), ".") - }, - }) - - if err != nil { - if req != nil { - // Clean up on error - if ctx.Err() == context.DeadlineExceeded { - return shared.FormatDownloadRequestTimeoutError(kbClient, req, effectiveTimeout) - } - deleteCtx, cancelDelete := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelDelete() - _ = kbClient.Delete(deleteCtx, req) - } - return err - } - - // Clean up the download request when done - defer func() { - deleteCtx, cancelDelete := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelDelete() - _ = kbClient.Delete(deleteCtx, req) - }() - - fmt.Fprintf(cmd.OutOrStdout(), "\nDownload URL received, fetching logs...\n") - - // Use the shared StreamDownloadContent function to download and stream logs - // Note: We use the same effective timeout for the HTTP download - if err := shared.StreamDownloadContentWithTimeout(signedURL, cmd.OutOrStdout(), effectiveTimeout); err != nil { - return fmt.Errorf("failed to download and stream logs: %w", err) - } - - return nil - }, - Example: ` oc oadp nonadmin restore logs my-restore - oc oadp nonadmin restore logs my-restore --request-timeout=30m`, - } - - c.Flags().DurationVar(&requestTimeout, "request-timeout", 0, fmt.Sprintf("The length of time to wait before giving up on a single server request (e.g., 30s, 5m, 1h). Overrides %s env var. Default: %v", shared.TimeoutEnvVar, shared.DefaultHTTPTimeout)) - - return c -} diff --git a/cmd/non-admin/restore/nonadminrestore_builder.go b/cmd/non-admin/restore/nonadminrestore_builder.go deleted file mode 100644 index 23daa4454..000000000 --- a/cmd/non-admin/restore/nonadminrestore_builder.go +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright The Velero Contributors. - -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 restore - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" -) - -/* - -Example usage: - -var nonAdminRestore = builder.NewNonAdminRestoreBuilder("user-namespace", "restore-1"). - ObjectMeta( - builder.WithLabels("foo", "bar"), - ). - RestoreSpec(nacv1alpha1.NonAdminRestoreSpec{ - RestoreSpec: &velerov1api.RestoreSpec{ - BackupName: "backup-1", - }, - }). - Result() - -*/ - -// NonAdminRestoreBuilder builds NonAdminRestore objects. -type NonAdminRestoreBuilder struct { - object *nacv1alpha1.NonAdminRestore -} - -// NewNonAdminRestoreBuilder is the constructor for a NonAdminRestoreBuilder. -func NewNonAdminRestoreBuilder(namespace, name string) *NonAdminRestoreBuilder { - objMeta := metav1.ObjectMeta{ - Namespace: namespace, - } - - // If name is empty, use GenerateName for auto-generation - if name == "" { - objMeta.GenerateName = "restore-" - } else { - objMeta.Name = name - } - - return &NonAdminRestoreBuilder{ - object: &nacv1alpha1.NonAdminRestore{ - TypeMeta: metav1.TypeMeta{ - APIVersion: nacv1alpha1.GroupVersion.String(), - Kind: "NonAdminRestore", - }, - ObjectMeta: objMeta, - }, - } -} - -// Result returns the built NonAdminRestore. -func (b *NonAdminRestoreBuilder) Result() *nacv1alpha1.NonAdminRestore { - return b.object -} - -// ObjectMeta applies functional options to the NonAdminRestore's ObjectMeta. -func (b *NonAdminRestoreBuilder) ObjectMeta(opts ...ObjectMetaOpt) *NonAdminRestoreBuilder { - for _, opt := range opts { - opt(b.object) - } - - return b -} - -// RestoreSpec sets the NonAdminRestore's restore spec. -func (b *NonAdminRestoreBuilder) RestoreSpec(spec nacv1alpha1.NonAdminRestoreSpec) *NonAdminRestoreBuilder { - b.object.Spec = spec - return b -} - -// Phase sets the NonAdminRestore's phase. -func (b *NonAdminRestoreBuilder) Phase(phase nacv1alpha1.NonAdminPhase) *NonAdminRestoreBuilder { - b.object.Status.Phase = phase - return b -} - -// VeleroRestore sets the reference to the created Velero restore. -func (b *NonAdminRestoreBuilder) VeleroRestore(restoreName, restoreNamespace string) *NonAdminRestoreBuilder { - if b.object.Status.VeleroRestore == nil { - b.object.Status.VeleroRestore = &nacv1alpha1.VeleroRestore{} - } - b.object.Status.VeleroRestore.Name = restoreName - b.object.Status.VeleroRestore.Namespace = restoreNamespace - return b -} - -// Conditions sets the NonAdminRestore's conditions. -func (b *NonAdminRestoreBuilder) Conditions(conditions []metav1.Condition) *NonAdminRestoreBuilder { - b.object.Status.Conditions = conditions - return b -} - -// WithStatus sets the NonAdminRestore's status. -func (b *NonAdminRestoreBuilder) WithStatus(status nacv1alpha1.NonAdminRestoreStatus) *NonAdminRestoreBuilder { - b.object.Status = status - return b -} - -// ObjectMetaOpt is a functional option for setting ObjectMeta properties. -type ObjectMetaOpt func(obj metav1.Object) - -// WithLabels returns a functional option that sets labels on an object. -func WithLabels(key, value string) ObjectMetaOpt { - return func(obj metav1.Object) { - labels := obj.GetLabels() - if labels == nil { - labels = make(map[string]string) - } - labels[key] = value - obj.SetLabels(labels) - } -} - -// WithLabelsMap returns a functional option that sets labels from a map on an object. -func WithLabelsMap(labels map[string]string) ObjectMetaOpt { - return func(obj metav1.Object) { - existingLabels := obj.GetLabels() - if existingLabels == nil { - existingLabels = make(map[string]string) - } - for k, v := range labels { - existingLabels[k] = v - } - obj.SetLabels(existingLabels) - } -} - -// WithAnnotations returns a functional option that sets annotations on an object. -func WithAnnotations(key, value string) ObjectMetaOpt { - return func(obj metav1.Object) { - annotations := obj.GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } - annotations[key] = value - obj.SetAnnotations(annotations) - } -} - -// WithAnnotationsMap returns a functional option that sets annotations from a map on an object. -func WithAnnotationsMap(annotations map[string]string) ObjectMetaOpt { - return func(obj metav1.Object) { - existingAnnotations := obj.GetAnnotations() - if existingAnnotations == nil { - existingAnnotations = make(map[string]string) - } - for k, v := range annotations { - existingAnnotations[k] = v - } - obj.SetAnnotations(existingAnnotations) - } -} diff --git a/cmd/non-admin/restore/restore.go b/cmd/non-admin/restore/restore.go deleted file mode 100644 index bababb2dc..000000000 --- a/cmd/non-admin/restore/restore.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright The Velero Contributors. - -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 restore - -import ( - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" -) - -func NewRestoreCommand(f client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "restore", - Short: "Work with non-admin restores", - Long: "Work with non-admin restores", - } - - c.AddCommand( - NewCreateCommand(f, "create"), - NewGetCommand(f, "get"), - NewDescribeCommand(f, "describe"), - NewLogsCommand(f, "logs"), - NewDeleteCommand(f, "delete"), - ) - - return c -} diff --git a/cmd/non-admin/restore/restore_test.go b/cmd/non-admin/restore/restore_test.go deleted file mode 100644 index da95c8b76..000000000 --- a/cmd/non-admin/restore/restore_test.go +++ /dev/null @@ -1,524 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 restore - -import ( - "testing" - - "github.com/migtools/oadp-cli/internal/testutil" -) - -// TestNonAdminRestoreCommands tests the non-admin restore command functionality -func TestNonAdminRestoreCommands(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - tests := []struct { - name string - args []string - expectContains []string - }{ - { - name: "nonadmin restore help", - args: []string{"nonadmin", "restore", "--help"}, - expectContains: []string{ - "Work with non-admin restores", - "create", - "get", - "describe", - "logs", - "delete", - }, - }, - { - name: "nonadmin restore create help", - args: []string{"nonadmin", "restore", "create", "--help"}, - expectContains: []string{ - "Create a non-admin restore", - "--backup-name", - "--include-resources", - "--exclude-resources", - "--selector", - "--or-selector", - }, - }, - { - name: "nonadmin restore get help", - args: []string{"nonadmin", "restore", "get", "--help"}, - expectContains: []string{ - "Get one or more non-admin restores", - }, - }, - { - name: "na restore shorthand help", - args: []string{"na", "restore", "--help"}, - expectContains: []string{ - "Work with non-admin restores", - "create", - "get", - "describe", - "logs", - "delete", - }, - }, - // Verb-noun order help command tests - { - name: "nonadmin get restore help", - args: []string{"nonadmin", "get", "restore", "--help"}, - expectContains: []string{ - "Get one or more non-admin restores", - }, - }, - { - name: "nonadmin create restore help", - args: []string{"nonadmin", "create", "restore", "--help"}, - expectContains: []string{ - "Create a non-admin restore", - }, - }, - // Shorthand verb-noun order tests - { - name: "na get restore help", - args: []string{"na", "get", "restore", "--help"}, - expectContains: []string{ - "Get one or more non-admin restores", - }, - }, - { - name: "na create restore help", - args: []string{"na", "create", "restore", "--help"}, - expectContains: []string{ - "Create a non-admin restore", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, tt.args, tt.expectContains) - }) - } -} - -// TestNonAdminRestoreHelpFlags tests that both --help and -h work for restore commands -func TestNonAdminRestoreHelpFlags(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - commands := [][]string{ - {"nonadmin", "restore", "--help"}, - {"nonadmin", "restore", "-h"}, - {"nonadmin", "restore", "create", "--help"}, - {"nonadmin", "restore", "create", "-h"}, - {"nonadmin", "restore", "get", "--help"}, - {"nonadmin", "restore", "get", "-h"}, - {"nonadmin", "restore", "describe", "--help"}, - {"nonadmin", "restore", "describe", "-h"}, - {"nonadmin", "restore", "logs", "--help"}, - {"nonadmin", "restore", "logs", "-h"}, - {"nonadmin", "restore", "delete", "--help"}, - {"nonadmin", "restore", "delete", "-h"}, - {"na", "restore", "--help"}, - {"na", "restore", "-h"}, - // Verb-noun order help flags - {"nonadmin", "get", "restore", "--help"}, - {"nonadmin", "get", "restore", "-h"}, - {"nonadmin", "create", "restore", "--help"}, - {"nonadmin", "create", "restore", "-h"}, - {"nonadmin", "describe", "restore", "--help"}, - {"nonadmin", "describe", "restore", "-h"}, - {"nonadmin", "logs", "restore", "--help"}, - {"nonadmin", "logs", "restore", "-h"}, - {"nonadmin", "delete", "restore", "--help"}, - {"nonadmin", "delete", "restore", "-h"}, - // Shorthand verb-noun order help flags - {"na", "get", "restore", "--help"}, - {"na", "get", "restore", "-h"}, - {"na", "create", "restore", "--help"}, - {"na", "create", "restore", "-h"}, - } - - for _, cmd := range commands { - t.Run("help_flags_"+cmd[len(cmd)-1], func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{"Usage:"}) - }) - } -} - -// TestNonAdminRestoreCreateFlags tests create command specific flags -func TestNonAdminRestoreCreateFlags(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("create command has all expected flags", func(t *testing.T) { - // Minimal MVP flags only (based on NAR restrictions for non-admin users) - expectedFlags := []string{ - "--backup-name", - "--include-resources", - "--exclude-resources", - "--selector", - "--or-selector", - "--include-cluster-resources", - "--item-operation-timeout", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "restore", "create", "--help"}, - expectedFlags) - }) -} - -// TestNonAdminRestoreExamples tests that help text contains proper examples -func TestNonAdminRestoreExamples(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("create examples use correct command format", func(t *testing.T) { - expectedExamples := []string{ - "oc oadp nonadmin restore create", - "--backup-name", - "--include-resources", - "--selector", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "restore", "create", "--help"}, - expectedExamples) - }) - - t.Run("main restore help shows subcommands", func(t *testing.T) { - expectedSubcommands := []string{ - "create", - "get", - "describe", - "logs", - "delete", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "restore", "--help"}, - expectedSubcommands) - }) -} - -// TestNonAdminRestoreClientConfigIntegration tests that restore commands respect client config -func TestNonAdminRestoreClientConfigIntegration(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - _, cleanup := testutil.SetupTempHome(t) - defer cleanup() - - t.Run("restore commands work with client config", func(t *testing.T) { - // Set a known namespace - _, err := testutil.RunCommand(t, binaryPath, "client", "config", "set", "namespace=user-namespace") - if err != nil { - t.Fatalf("Failed to set client config: %v", err) - } - - // Test that restore commands can be invoked (they should respect the namespace) - // We test help commands since they don't require actual K8s resources - commands := [][]string{ - {"nonadmin", "restore", "get", "--help"}, - {"nonadmin", "restore", "create", "--help"}, - {"nonadmin", "restore", "describe", "--help"}, - {"nonadmin", "restore", "logs", "--help"}, - {"nonadmin", "restore", "delete", "--help"}, - {"na", "restore", "get", "--help"}, - // Verb-noun order commands - {"nonadmin", "get", "restore", "--help"}, - {"nonadmin", "create", "restore", "--help"}, - {"nonadmin", "describe", "restore", "--help"}, - {"nonadmin", "logs", "restore", "--help"}, - {"nonadmin", "delete", "restore", "--help"}, - {"na", "get", "restore", "--help"}, - {"na", "create", "restore", "--help"}, - } - - for _, cmd := range commands { - t.Run("config_test_"+cmd[len(cmd)-2], func(t *testing.T) { - output, err := testutil.RunCommand(t, binaryPath, cmd...) - if err != nil { - t.Fatalf("Non-admin restore command should work with client config: %v", err) - } - if output == "" { - t.Errorf("Expected help output for %v", cmd) - } - }) - } - }) -} - -// TestNonAdminRestoreCommandStructure tests the overall command structure -func TestNonAdminRestoreCommandStructure(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("restore commands available under nonadmin", func(t *testing.T) { - _, err := testutil.RunCommand(t, binaryPath, "nonadmin", "--help") - if err != nil { - t.Fatalf("nonadmin command should exist: %v", err) - } - - expectedCommands := []string{"restore"} - for _, cmd := range expectedCommands { - testutil.TestHelpCommand(t, binaryPath, []string{"nonadmin", "--help"}, []string{cmd}) - } - }) - - t.Run("restore commands available under na shorthand", func(t *testing.T) { - _, err := testutil.RunCommand(t, binaryPath, "na", "--help") - if err != nil { - t.Fatalf("na command should exist: %v", err) - } - - expectedCommands := []string{"restore"} - for _, cmd := range expectedCommands { - testutil.TestHelpCommand(t, binaryPath, []string{"na", "--help"}, []string{cmd}) - } - }) -} - -// TestVerbNounOrderRestoreExamples tests that verb-noun order commands show proper examples -func TestVerbNounOrderRestoreExamples(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("verb commands show proper examples", func(t *testing.T) { - // Test that verb commands show examples with oc oadp prefix - expectedExamples := []string{ - "oc oadp nonadmin get restore", - "oc oadp nonadmin create restore", - "oc oadp nonadmin describe restore", - "oc oadp nonadmin logs restore", - "oc oadp nonadmin delete restore", - } - - commands := [][]string{ - {"nonadmin", "get", "--help"}, - {"nonadmin", "create", "--help"}, - {"nonadmin", "describe", "--help"}, - {"nonadmin", "logs", "--help"}, - {"nonadmin", "delete", "--help"}, - } - - for i, cmd := range commands { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{expectedExamples[i]}) - } - }) - - t.Run("verb commands with specific resources show proper examples", func(t *testing.T) { - // Test that verb commands with specific resources show examples (noun-first format from underlying commands) - expectedExamples := []string{ - "oc oadp nonadmin restore get", - "oc oadp nonadmin restore create", - "oc oadp nonadmin restore describe my-restore", - "oc oadp nonadmin restore logs my-restore", - "oc oadp nonadmin restore delete my-restore", - } - - commands := [][]string{ - {"nonadmin", "get", "restore", "--help"}, - {"nonadmin", "create", "restore", "--help"}, - {"nonadmin", "describe", "restore", "--help"}, - {"nonadmin", "logs", "restore", "--help"}, - {"nonadmin", "delete", "restore", "--help"}, - } - - for i, cmd := range commands { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{expectedExamples[i]}) - } - }) - - t.Run("shorthand verb commands show proper examples", func(t *testing.T) { - // Test that shorthand verb commands show examples - expectedExamples := []string{ - "oc oadp nonadmin get restore", - "oc oadp nonadmin create restore", - "oc oadp nonadmin describe restore", - "oc oadp nonadmin logs restore", - "oc oadp nonadmin delete restore", - } - - commands := [][]string{ - {"na", "get", "--help"}, - {"na", "create", "--help"}, - {"na", "describe", "--help"}, - {"na", "logs", "--help"}, - {"na", "delete", "--help"}, - } - - for i, cmd := range commands { - testutil.TestHelpCommand(t, binaryPath, cmd, []string{expectedExamples[i]}) - } - }) -} - -// TestNonAdminRestoreCreateRequiresBackupName tests that create requires --backup-name -func TestNonAdminRestoreCreateRequiresBackupName(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("create help shows --backup-name flag", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "restore", "create", "--help"}, - []string{"--backup-name"}) - }) -} - -// TestNonAdminRestoreDescribeCommands tests describe command functionality -func TestNonAdminRestoreDescribeCommands(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - tests := []struct { - name string - args []string - expectContains []string - }{ - { - name: "nonadmin restore describe help", - args: []string{"nonadmin", "restore", "describe", "--help"}, - expectContains: []string{ - "Describe a non-admin restore", - "--details", - "--request-timeout", - }, - }, - { - name: "nonadmin describe restore help - verb-noun order", - args: []string{"nonadmin", "describe", "restore", "--help"}, - expectContains: []string{ - "Describe a non-admin restore", - }, - }, - { - name: "na describe restore help - shorthand", - args: []string{"na", "describe", "restore", "--help"}, - expectContains: []string{ - "Describe a non-admin restore", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, tt.args, tt.expectContains) - }) - } -} - -// TestNonAdminRestoreLogsCommands tests logs command functionality -func TestNonAdminRestoreLogsCommands(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - tests := []struct { - name string - args []string - expectContains []string - }{ - { - name: "nonadmin restore logs help", - args: []string{"nonadmin", "restore", "logs", "--help"}, - expectContains: []string{ - "Show logs for a non-admin restore", - "--request-timeout", - }, - }, - { - name: "nonadmin logs restore help - verb-noun order", - args: []string{"nonadmin", "logs", "restore", "--help"}, - expectContains: []string{ - "Show logs for a non-admin restore", - }, - }, - { - name: "na logs restore help - shorthand", - args: []string{"na", "logs", "restore", "--help"}, - expectContains: []string{ - "Show logs for a non-admin restore", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, tt.args, tt.expectContains) - }) - } -} - -// TestNonAdminRestoreDeleteCommands tests delete command functionality -func TestNonAdminRestoreDeleteCommands(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - tests := []struct { - name string - args []string - expectContains []string - }{ - { - name: "nonadmin restore delete help", - args: []string{"nonadmin", "restore", "delete", "--help"}, - expectContains: []string{ - "Delete one or more non-admin restores", - "--confirm", - "--all", - }, - }, - { - name: "nonadmin delete restore help - verb-noun order", - args: []string{"nonadmin", "delete", "restore", "--help"}, - expectContains: []string{ - "Delete one or more non-admin restores", - }, - }, - { - name: "na delete restore help - shorthand", - args: []string{"na", "delete", "restore", "--help"}, - expectContains: []string{ - "Delete one or more non-admin restores", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, tt.args, tt.expectContains) - }) - } -} - -// TestNonAdminRestoreDeleteAllFlag tests --all flag behavior -func TestNonAdminRestoreDeleteAllFlag(t *testing.T) { - binaryPath := testutil.BuildCLIBinary(t) - - t.Run("delete help shows --all flag", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "restore", "delete", "--help"}, - []string{"--all", "Delete all restores"}) - }) - - t.Run("delete help shows --confirm flag", func(t *testing.T) { - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "restore", "delete", "--help"}, - []string{"--confirm", "Skip confirmation"}) - }) - - t.Run("delete help has examples section", func(t *testing.T) { - // Test that examples section exists and shows various delete patterns - expectedExamples := []string{ - "oc oadp nonadmin restore delete my-restore", - "oc oadp nonadmin restore delete --all", - "oc oadp nonadmin restore delete my-restore --confirm", - } - - testutil.TestHelpCommand(t, binaryPath, - []string{"nonadmin", "restore", "delete", "--help"}, - expectedExamples) - }) -} diff --git a/cmd/non-admin/verbs/README.md b/cmd/non-admin/verbs/README.md deleted file mode 100644 index f59c65d9d..000000000 --- a/cmd/non-admin/verbs/README.md +++ /dev/null @@ -1,177 +0,0 @@ -# Non-Admin Verb-Noun Command System - -This directory contains the verb-noun command system for non-admin resources in the OADP CLI. - -## Overview - -The non-admin verb system works identically to the main verb system but is specifically designed for non-admin resources like: -- **backup** - Non-admin backups -- **bsl** - Backup storage locations - -## Usage - -### Supported Commands - -```bash -# Get commands -kubectl oadp nonadmin get backup -kubectl oadp nonadmin get bsl # Error: BSL doesn't support get - -# Create commands -kubectl oadp nonadmin create backup my-backup -kubectl oadp nonadmin create bsl my-bsl - -# Delete commands -kubectl oadp nonadmin delete backup my-backup -kubectl oadp nonadmin delete bsl # Error: BSL doesn't support delete - -# Describe commands -kubectl oadp nonadmin describe backup my-backup -kubectl oadp nonadmin describe bsl # Error: BSL doesn't support describe - -# Logs commands -kubectl oadp nonadmin logs backup my-backup -kubectl oadp nonadmin logs bsl # Error: BSL doesn't support logs -``` - -## Architecture - -### Files - -- **`builder.go`** - `NonAdminVerbBuilder` for building verb commands -- **`registry.go`** - Resource registration for backup and bsl -- **`verbs.go`** - Verb command definitions (get, create, delete, describe, logs) - -### Key Differences from Main Verbs - -1. **Builder Type**: `NonAdminVerbBuilder` instead of `VerbBuilder` -2. **Config Type**: `NonAdminVerbConfig` instead of `VerbConfig` -3. **Handler Type**: `NonAdminResourceHandler` instead of `ResourceHandler` -4. **Single Factory**: Only uses one factory (non-admin factory) - -## Adding New Non-Admin Resources - -### Step 1: Create Resource Commands - -Ensure your resource follows the noun-verb pattern: - -``` -cmd/non-admin/your-resource/ -├── your-resource.go # Main command -├── get.go # get subcommand (if supported) -├── create.go # create subcommand (if supported) -└── describe.go # describe subcommand (if supported) -``` - -### Step 2: Add Resource Registration - -In `cmd/non-admin/verbs/registry.go`: - -```go -// RegisterYourResourceResources registers your-resource for a specific verb -func RegisterYourResourceResources(builder *NonAdminVerbBuilder, verb string) { - // Only register for supported verbs - supportedVerbs := []string{"get", "create", "describe"} - for _, supportedVerb := range supportedVerbs { - if verb == supportedVerb { - builder.RegisterResource("your-resource", NonAdminResourceHandler{ - GetCommandFunc: func(factory client.Factory) *cobra.Command { - return yourresource.NewCommand(factory) - }, - GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { - return getSubCommand(resourceCmd, verb) - }, - }) - break - } - } -} -``` - -### Step 3: Register in Verb Commands - -Update each verb function in `cmd/non-admin/verbs/verbs.go`: - -```go -func NewGetCommand(factory client.Factory) *cobra.Command { - builder := NewNonAdminVerbBuilder(factory) - RegisterBackupResources(builder, "get") - RegisterBSLResources(builder, "get") - RegisterYourResourceResources(builder, "get") // Add this line - - return builder.BuildVerbCommand(NonAdminVerbConfig{ - // ... existing config - }) -} -``` - -### Step 4: Update Examples - -Add your resource to the examples in each verb: - -```go -Example: ` # Get all non-admin backups - kubectl oadp nonadmin get backup - - # Get all your-resources - kubectl oadp nonadmin get your-resource`, -``` - -## Conditional Registration Example - -The BSL resource only supports `create`, so it uses conditional registration: - -```go -func RegisterBSLResources(builder *NonAdminVerbBuilder, verb string) { - if verb == "create" { - builder.RegisterResource("bsl", NonAdminResourceHandler{ - // ... registration logic - }) - } -} -``` - -## Testing - -### Build and Test -```bash -go build -o kubectl-oadp . - -# Test new resource -./kubectl-oadp nonadmin get your-resource -./kubectl-oadp nonadmin create your-resource test-name -``` - -### Verify Error Handling -```bash -# Should show "unknown resource type" for unsupported verbs -./kubectl-oadp nonadmin get bsl -./kubectl-oadp nonadmin describe bsl -``` - -## Current Resources - -### Backup -- **Supported Verbs**: get, create, delete, describe, logs -- **Command**: `backup.NewBackupCommand(factory)` - -### BSL (Backup Storage Location) -- **Supported Verbs**: create only -- **Command**: `bsl.NewBSLCommand(factory)` - -## Integration - -The non-admin verb commands are integrated into the main non-admin command in `cmd/non-admin/nonadmin.go`: - -```go -// Add verb-based commands for compatibility with Velero CLI pattern -c.AddCommand(verbs.NewGetCommand(f)) -c.AddCommand(verbs.NewCreateCommand(f)) -c.AddCommand(verbs.NewDeleteCommand(f)) -c.AddCommand(verbs.NewDescribeCommand(f)) -c.AddCommand(verbs.NewLogsCommand(f)) -``` - -This allows users to use either pattern: -- `oadp nonadmin backup get` (noun-verb) -- `oadp nonadmin get backup` (verb-noun) diff --git a/cmd/non-admin/verbs/verbs.go b/cmd/non-admin/verbs/verbs.go deleted file mode 100644 index 2936295cf..000000000 --- a/cmd/non-admin/verbs/verbs.go +++ /dev/null @@ -1,148 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 verbs - -import ( - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" - - "github.com/migtools/oadp-cli/cmd/non-admin/backup" - "github.com/migtools/oadp-cli/cmd/non-admin/bsl" - "github.com/migtools/oadp-cli/cmd/non-admin/restore" -) - -// NewGetCommand creates the "get" verb command that delegates to noun commands -func NewGetCommand(factory client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "get", - Short: "Get one or more non-admin resources", - Long: "Get one or more non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", - Example: ` # Get all non-admin backups - oc oadp nonadmin get backup - - # Get a specific non-admin backup - oc oadp nonadmin get backup my-backup - - # Get all non-admin restores - oc oadp nonadmin get restore - - # Get a specific non-admin restore - oc oadp nonadmin get restore my-restore - - # Get all non-admin backup storage locations - oc oadp nonadmin get bsl - - # Get a specific backup storage location - oc oadp nonadmin get bsl my-storage`, - } - - c.AddCommand( - backup.NewGetCommand(factory, "backup"), - restore.NewGetCommand(factory, "restore"), - bsl.NewGetCommand(factory, "bsl"), - ) - - return c -} - -// NewCreateCommand creates the "create" verb command that delegates to noun commands -func NewCreateCommand(factory client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "create", - Short: "Create non-admin resources", - Long: "Create non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", - Example: ` # Create a non-admin backup - oc oadp nonadmin create backup my-backup - - # Create a non-admin restore - oc oadp nonadmin create restore my-restore --backup-name my-backup - - # Create a backup storage location - oc oadp nonadmin create bsl my-bsl`, - } - - c.AddCommand( - backup.NewCreateCommand(factory, "backup"), - restore.NewCreateCommand(factory, "restore"), - bsl.NewCreateCommand(factory, "bsl"), - ) - - return c -} - -// NewDeleteCommand creates the "delete" verb command that delegates to noun commands -func NewDeleteCommand(factory client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "delete", - Short: "Delete non-admin resources", - Long: "Delete non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", - Example: ` # Delete a non-admin backup - oc oadp nonadmin delete backup my-backup - - # Delete a non-admin restore - oc oadp nonadmin delete restore my-restore`, - } - - c.AddCommand( - backup.NewDeleteCommand(factory, "backup"), - restore.NewDeleteCommand(factory, "restore"), - ) - - return c -} - -// NewDescribeCommand creates the "describe" verb command that delegates to noun commands -func NewDescribeCommand(factory client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "describe", - Short: "Describe non-admin resources", - Long: "Describe non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", - Example: ` # Describe a non-admin backup - oc oadp nonadmin describe backup my-backup - - # Describe a non-admin restore - oc oadp nonadmin describe restore my-restore`, - } - - c.AddCommand( - backup.NewDescribeCommand(factory, "backup"), - restore.NewDescribeCommand(factory, "restore"), - ) - - return c -} - -// NewLogsCommand creates the "logs" verb command that delegates to noun commands -func NewLogsCommand(factory client.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "logs", - Short: "Get logs for non-admin resources", - Long: "Get logs for non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", - Example: ` # Get logs for a non-admin backup - oc oadp nonadmin logs backup my-backup - - # Get logs for a non-admin restore - oc oadp nonadmin logs restore my-restore`, - } - - c.AddCommand( - backup.NewLogsCommand(factory, "backup"), - restore.NewLogsCommand(factory, "restore"), - ) - - return c -} diff --git a/cmd/root.go b/cmd/root.go index 429827a4d..d3550bef4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -31,8 +31,6 @@ import ( "github.com/fatih/color" "github.com/migtools/oadp-cli/cmd/completion" mustgather "github.com/migtools/oadp-cli/cmd/must-gather" - "github.com/migtools/oadp-cli/cmd/nabsl-request" - nonadmin "github.com/migtools/oadp-cli/cmd/non-admin" "github.com/migtools/oadp-cli/cmd/setup" "github.com/spf13/cobra" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -357,24 +355,6 @@ func wrapPreRunE(existing func(*cobra.Command, []string) error, additional func( } } -// isNonadminEnabled checks if nonadmin mode is enabled in the VeleroConfig. -// Handles both boolean and string representations since -// `oc oadp client config set nonadmin=true` stores the value as a string. -func isNonadminEnabled(config clientcmd.VeleroConfig) bool { - val, ok := config["nonadmin"] - if !ok { - return false - } - switch v := val.(type) { - case bool: - return v - case string: - return strings.EqualFold(v, "true") - default: - return false - } -} - // NewVeleroRootCommand returns a root command with all Velero CLI subcommands attached. func NewVeleroRootCommand(baseName string) *cobra.Command { @@ -386,12 +366,7 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { config = clientcmd.VeleroConfig{} } - // When nonadmin mode is enabled, remove the namespace override so the - // factory uses the current kubeconfig context namespace instead of an - // admin namespace like openshift-adp. - if isNonadminEnabled(config) { - delete(config, clientcmd.ConfigKeyNamespace) - } else if config.Namespace() == "" { + if config.Namespace() == "" { config[clientcmd.ConfigKeyNamespace] = "openshift-adp" } @@ -427,8 +402,6 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { f := &timeoutFactory{Factory: baseFactory} // Bind factory flags to enable -n/--namespace flag for admin commands. - // This allows admin Velero and NABSL-request commands to accept namespace via CLI flag. - // Nonadmin commands continue using GetCurrentNamespace() for security isolation. f.BindFlags(c.PersistentFlags()) c.AddCommand( @@ -447,24 +420,16 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { debug.NewCommand(f), ) - // Admin NABSL request commands - use Velero factory (admin namespace) - c.AddCommand(nabsl.NewNABSLRequestCommand(f)) - - // Custom subcommands - use NonAdmin factory - c.AddCommand(nonadmin.NewNonAdminCommand(f)) - // Must-gather command - diagnostic tool c.AddCommand(mustgather.NewMustGatherCommand(f)) - // Setup command - auto-detect and configure admin vs non-admin mode + // Setup command - detect and confirm cluster-admin access (admin-only on OADP 1.4) c.AddCommand(setup.NewSetupCommand(f)) // Apply velero->oadp replacement to all commands recursively - // Skip nonadmin commands since we have full control over their output + // Skip commands where we control the output or that need direct stdout access for _, cmd := range c.Commands() { - // Don't wrap nonadmin commands - we control them and they already use correct terminology - // Don't wrap completion - it needs direct stdout access for shell completion generation - if cmd.Use == "nonadmin" || cmd.Use == "nabsl-request" || cmd.Use == "must-gather" || cmd.Use == "setup" || strings.HasPrefix(cmd.Use, "completion") { + if cmd.Use == "must-gather" || cmd.Use == "setup" || strings.HasPrefix(cmd.Use, "completion") { continue } replaceVeleroWithOADP(cmd) @@ -475,22 +440,6 @@ func NewVeleroRootCommand(baseName string) *cobra.Command { renameTimeoutFlag(cmd) } - // When nonadmin mode is enabled, remove all admin commands so only - // nonadmin, client (for toggling the config), and help are available. - if isNonadminEnabled(config) { - allowedCmds := map[string]bool{ - "nonadmin": true, - "client": true, - "completion": true, - "setup": true, - } - for _, cmd := range c.Commands() { - if !allowedCmds[cmd.Use] { - c.RemoveCommand(cmd) - } - } - } - // Set custom usage template to show "oc oadp" instead of just "oadp" usageTemplate := c.UsageTemplate() usageTemplate = strings.ReplaceAll(usageTemplate, "{{.CommandPath}}", "oc {{.CommandPath}}") diff --git a/cmd/root_test.go b/cmd/root_test.go index 96cb819c4..511ed9a32 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -51,8 +51,6 @@ func TestRootCommand(t *testing.T) { "version", "backup", "restore", - "nabsl-request", - "nonadmin", }, }, { diff --git a/cmd/setup/detector.go b/cmd/setup/detector.go deleted file mode 100644 index c71cb79e2..000000000 --- a/cmd/setup/detector.go +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 setup - -import ( - "fmt" - "os/exec" - "strings" -) - -// DetectionResult holds the result of detecting user mode -type DetectionResult struct { - IsAdmin bool - Error error -} - -// detectUserMode detects whether the user has admin permissions by checking -// if they can create Velero Backup resources across all namespaces. -// Admin users can create backups.velero.io cluster-wide, while non-admin users -// can only create nonadminbackups.oadp.openshift.io in their own namespace. -func detectUserMode() DetectionResult { - // Check if user can create Velero Backups across all namespaces - // This is the core permission difference between admin and non-admin modes - cmd := exec.Command("oc", "auth", "can-i", "create", "backups.velero.io", "--all-namespaces") - output, err := cmd.CombinedOutput() - - if err != nil { - // Check if this is because oc command failed vs permission check - if exitErr, ok := err.(*exec.ExitError); ok { - // Exit code 1 typically means "no" for can-i - if exitErr.ExitCode() == 1 { - return DetectionResult{IsAdmin: false} - } - } - // Check if output indicates not logged in - outputStr := string(output) - if strings.Contains(outputStr, "Unauthorized") || strings.Contains(outputStr, "not logged in") { - return DetectionResult{Error: fmt.Errorf("not logged in to cluster")} - } - // Other errors (oc not found, cluster unreachable, etc.) - return DetectionResult{Error: fmt.Errorf("failed to check permissions: %w", err)} - } - - // Parse the output - result := strings.TrimSpace(string(output)) - - // "yes" means user can create backups cluster-wide (admin mode) - if result == "yes" { - return DetectionResult{IsAdmin: true} - } - - // "no" means user cannot (non-admin mode) - return DetectionResult{IsAdmin: false} -} diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index fb3d4fac5..bf73d6ab9 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -20,7 +20,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/migtools/oadp-cli/cmd/shared" "github.com/spf13/cobra" @@ -30,35 +29,26 @@ import ( // SetupOptions holds the options for the setup command type SetupOptions struct { - Force bool // Re-run detection even if already configured - - // Internal state - detectionResult DetectionResult + Force bool // Re-run setup even if already configured } // BindFlags binds the flags to the command func (o *SetupOptions) BindFlags(flags *pflag.FlagSet) { - flags.BoolVar(&o.Force, "force", false, "Re-run detection even if already configured") + flags.BoolVar(&o.Force, "force", false, "Re-run setup even if already configured") } // Complete completes the options func (o *SetupOptions) Complete(args []string, f client.Factory) error { - // No setup needed - detection uses oc CLI directly return nil } // Validate validates the options func (o *SetupOptions) Validate(c *cobra.Command, args []string, f client.Factory) error { - // No validation needed for setup command return nil } // Run executes the setup command func (o *SetupOptions) Run(c *cobra.Command, f client.Factory) error { - fmt.Println("Detecting user permissions...") - fmt.Println() - - // Silence usage help on errors during Run (we provide clear error messages) c.SilenceUsage = true // Check if already configured (unless --force flag set) @@ -68,8 +58,7 @@ func (o *SetupOptions) Run(c *cobra.Command, f client.Factory) error { return fmt.Errorf("failed to read existing config: %w", err) } - // Check if nonadmin field is explicitly set (not nil) - if existingConfig.NonAdmin != nil { + if existingConfig.Namespace != "" { fmt.Println("OADP CLI is already configured.") fmt.Println() o.printCurrentConfig(existingConfig) @@ -79,49 +68,15 @@ func (o *SetupOptions) Run(c *cobra.Command, f client.Factory) error { } } - // Run detection - o.detectionResult = detectUserMode() - - // Handle detection errors - if o.detectionResult.Error != nil { - // Provide specific guidance based on error type - errMsg := o.detectionResult.Error.Error() - if strings.Contains(errMsg, "not logged in") || strings.Contains(errMsg, "Unauthorized") { - fmt.Println("Error: Not logged in to cluster") - fmt.Println() - fmt.Println("Please log in to your cluster:") - fmt.Println(" oc login ") - return fmt.Errorf("not logged in to cluster") - } else { - fmt.Printf("Error: %v\n", o.detectionResult.Error) - fmt.Println() - fmt.Println("This could mean:") - fmt.Println(" - Your cluster is not accessible") - fmt.Println(" - Your kubeconfig is invalid") - fmt.Println(" - Network connectivity issues") - return o.detectionResult.Error - } - } - - // Read existing config to preserve fields like default-nabsl config, err := shared.ReadVeleroClientConfig() if err != nil { return fmt.Errorf("failed to read existing config: %w", err) } - // Update config based on detection result - if o.detectionResult.IsAdmin { - config.NonAdmin = false - } else { - config.NonAdmin = true - } - - // Write config file if err := shared.WriteVeleroClientConfig(config); err != nil { return fmt.Errorf("failed to write config: %w", err) } - // Print success message o.printSetupSuccess() return nil @@ -132,11 +87,7 @@ func (o *SetupOptions) printCurrentConfig(config *shared.ClientConfig) { homeDir, _ := os.UserHomeDir() configPath := filepath.Join(homeDir, ".config", "velero", "config.json") - if config.IsNonAdmin() { - fmt.Println("Current mode: non-admin") - } else { - fmt.Println("Current mode: admin") - } + fmt.Printf("Namespace: %s\n", config.Namespace) fmt.Printf("Configuration file: %s\n", configPath) } @@ -145,23 +96,11 @@ func (o *SetupOptions) printSetupSuccess() { homeDir, _ := os.UserHomeDir() configPath := filepath.Join(homeDir, ".config", "velero", "config.json") - if o.detectionResult.IsAdmin { - fmt.Println("✓ Admin mode enabled") - fmt.Println() - fmt.Printf("Configuration saved to: %s\n", configPath) - fmt.Println() - fmt.Println("You can now use OADP admin commands:") - fmt.Println(" oc oadp backup create my-backup") - fmt.Println(" oc oadp restore create my-restore") - } else { - fmt.Println("✓ Non-admin mode enabled") - fmt.Println() - fmt.Printf("Configuration saved to: %s\n", configPath) - fmt.Println() - fmt.Println("You can now use OADP non-admin commands:") - fmt.Println(" oc oadp nonadmin backup create my-backup") - fmt.Println(" oc oadp nonadmin restore create my-restore") - } + fmt.Printf("Configuration saved to: %s\n", configPath) + fmt.Println() + fmt.Println("You can now use OADP admin commands:") + fmt.Println(" oc oadp backup create my-backup") + fmt.Println(" oc oadp restore create my-restore") } // NewSetupCommand creates the setup command @@ -170,24 +109,19 @@ func NewSetupCommand(f client.Factory) *cobra.Command { c := &cobra.Command{ Use: "setup", - Short: "Auto-detect and configure admin vs non-admin mode", - Long: `Auto-detect and configure admin vs non-admin mode. - -This command detects whether you have cluster-wide admin permissions and -automatically configures the OADP CLI to use the appropriate mode: - -- Admin mode: Can create Velero Backup resources across all namespaces -- Non-admin mode: Can only create NonAdminBackup resources in current namespace + Short: "Configure the OADP CLI", + Long: `Configure the OADP CLI. -The detection works by checking RBAC permissions: oc auth can-i create backups.velero.io --all-namespaces +Saves the CLI configuration to ~/.config/velero/config.json. -Configuration is saved to: ~/.config/velero/config.json +On OADP 1.4, only cluster-admin operations are supported. +Use OADP 1.5 or later for non-admin backup and restore. Examples: - # Auto-detect and configure OADP CLI + # Configure OADP CLI oc oadp setup - # Re-run detection (reconfigure) + # Reconfigure oc oadp setup --force`, Args: cobra.ExactArgs(0), RunE: func(c *cobra.Command, args []string) error { diff --git a/cmd/shared/client.go b/cmd/shared/client.go index 36260e06a..4a80458c0 100644 --- a/cmd/shared/client.go +++ b/cmd/shared/client.go @@ -22,7 +22,6 @@ import ( "net" "time" - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" corev1 "k8s.io/api/core/v1" @@ -33,8 +32,6 @@ import ( // Configuration for creating Kubernetes clients type ClientOptions struct { - // OADP-NonAdmin CRD types - IncludeNonAdminTypes bool // Velero CRD types IncludeVeleroTypes bool // Kubernetes core types @@ -87,9 +84,8 @@ func NewClientWithScheme(f client.Factory, opts ClientOptions) (kbclient.WithWat // NewClientWithFullScheme creates a client with all commonly used scheme types func NewClientWithFullScheme(f client.Factory) (kbclient.WithWatch, error) { return NewClientWithScheme(f, ClientOptions{ - IncludeNonAdminTypes: true, - IncludeVeleroTypes: true, - IncludeCoreTypes: true, + IncludeVeleroTypes: true, + IncludeCoreTypes: true, }) } @@ -97,12 +93,6 @@ func NewClientWithFullScheme(f client.Factory) (kbclient.WithWatch, error) { func NewSchemeWithTypes(opts ClientOptions) (*runtime.Scheme, error) { scheme := runtime.NewScheme() - if opts.IncludeNonAdminTypes { - if err := nacv1alpha1.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to add OADP non-admin types to scheme: %w", err) - } - } - if opts.IncludeVeleroTypes { if err := velerov1.AddToScheme(scheme); err != nil { return nil, fmt.Errorf("failed to add Velero types to scheme: %w", err) diff --git a/cmd/shared/download.go b/cmd/shared/download.go deleted file mode 100644 index 368a08c75..000000000 --- a/cmd/shared/download.go +++ /dev/null @@ -1,367 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 shared - -import ( - "bufio" - "compress/gzip" - "context" - "fmt" - "io" - "net/http" - "os" - "strings" - "time" - - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" - velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -// DefaultHTTPTimeout is the default timeout for HTTP requests when downloading content from object storage. -// This prevents the CLI from hanging indefinitely if the connection stalls. -const DefaultHTTPTimeout = 10 * time.Minute - -// TimeoutEnvVar is the environment variable name that can be used to override the default timeout. -// Example: OADP_CLI_REQUEST_TIMEOUT=30m oc oadp nonadmin backup logs my-backup -const TimeoutEnvVar = "OADP_CLI_REQUEST_TIMEOUT" - -// getHTTPTimeout returns the HTTP timeout to use for download operations. -// It checks for an environment variable override first, then falls back to the default. -func getHTTPTimeout() time.Duration { - return GetHTTPTimeoutWithOverride(0) -} - -// GetHTTPTimeoutWithOverride returns the HTTP timeout to use for download operations. -// Priority order: override parameter (if > 0) > environment variable > default. -// This allows CLI flags to take precedence over environment variables. -func GetHTTPTimeoutWithOverride(override time.Duration) time.Duration { - // If an explicit override is provided (e.g., from --timeout flag), use it - if override > 0 { - return override - } - - // Check for environment variable - if envTimeout := os.Getenv(TimeoutEnvVar); envTimeout != "" { - if parsed, err := time.ParseDuration(envTimeout); err == nil { - return parsed - } - } - - return DefaultHTTPTimeout -} - -// httpClientWithTimeout returns an HTTP client with a configured timeout. -// Using a custom client instead of http.DefaultClient ensures downloads don't hang indefinitely. -func httpClientWithTimeout(timeout time.Duration) *http.Client { - return &http.Client{ - Timeout: timeout, - } -} - -// DownloadRequestOptions holds configuration for creating and processing NonAdminDownloadRequests -type DownloadRequestOptions struct { - // BackupName is the name of the backup to download data for - BackupName string - // DataType is the type of data to download (e.g., "BackupLog", "BackupResults", etc.) - DataType velerov1.DownloadTargetKind - // Namespace is the namespace where the download request will be created - Namespace string - // Timeout is the maximum time to wait for the download request to be processed - Timeout time.Duration - // PollInterval is how often to check the status of the download request - PollInterval time.Duration - // HTTPTimeout is the timeout for downloading content from the signed URL. - // If zero, uses the default timeout (env var or DefaultHTTPTimeout). - HTTPTimeout time.Duration - // OnProgress is an optional callback called on each polling iteration - OnProgress func() -} - -// CreateAndWaitForDownloadURL creates a NonAdminDownloadRequest, waits for it to be processed, -// and returns the signed URL. The request is NOT automatically cleaned up - caller is responsible. -// This is a lower-level function that allows callers to control cleanup timing. -func CreateAndWaitForDownloadURL(ctx context.Context, kbClient kbclient.Client, opts DownloadRequestOptions) (*nacv1alpha1.NonAdminDownloadRequest, string, error) { - // Set defaults - if opts.Timeout == 0 { - opts.Timeout = 120 * time.Second - } - if opts.PollInterval == 0 { - opts.PollInterval = 2 * time.Second - } - - // Create NonAdminDownloadRequest - req := &nacv1alpha1.NonAdminDownloadRequest{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: opts.BackupName + "-" + strings.ToLower(string(opts.DataType)) + "-", - Namespace: opts.Namespace, - }, - Spec: nacv1alpha1.NonAdminDownloadRequestSpec{ - Target: velerov1.DownloadTarget{ - Kind: opts.DataType, - Name: opts.BackupName, - }, - }, - } - - if err := kbClient.Create(ctx, req); err != nil { - return nil, "", fmt.Errorf("failed to create NonAdminDownloadRequest for %s: %w", opts.DataType, err) - } - - // Wait for the download request to be processed - signedURL, err := waitForDownloadURL(ctx, kbClient, req, opts.Timeout, opts.PollInterval, opts.OnProgress) - if err != nil { - return req, "", err - } - - return req, signedURL, nil -} - -// ProcessDownloadRequest creates a NonAdminDownloadRequest, waits for it to be processed, -// downloads the content from the signed URL, and returns it as a string. -// This function automatically cleans up the download request when done. -func ProcessDownloadRequest(ctx context.Context, kbClient kbclient.Client, opts DownloadRequestOptions) (string, error) { - req, signedURL, err := CreateAndWaitForDownloadURL(ctx, kbClient, opts) - if err != nil { - if req != nil { - // Clean up on error - deleteCtx, cancelDelete := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelDelete() - _ = kbClient.Delete(deleteCtx, req) - } - return "", err - } - - // Clean up the download request when done - defer func() { - deleteCtx, cancelDelete := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelDelete() - _ = kbClient.Delete(deleteCtx, req) - }() - - // Download and return the content using the specified HTTP timeout - httpTimeout := GetHTTPTimeoutWithOverride(opts.HTTPTimeout) - content, err := DownloadContentWithTimeout(signedURL, httpTimeout) - if err != nil { - return "", err - } - return content, nil -} - -// waitForDownloadURL waits for a NonAdminDownloadRequest to be processed and returns the signed URL. -// If onProgress is provided, it will be called on each polling iteration. -func waitForDownloadURL(ctx context.Context, kbClient kbclient.Client, req *nacv1alpha1.NonAdminDownloadRequest, timeout, pollInterval time.Duration, onProgress func()) (string, error) { - timeoutCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - ticker := time.NewTicker(pollInterval) - defer ticker.Stop() - - for { - select { - case <-timeoutCtx.Done(): - return "", fmt.Errorf("timed out waiting for NonAdminDownloadRequest to be processed") - case <-ticker.C: - if onProgress != nil { - onProgress() - } - - var updated nacv1alpha1.NonAdminDownloadRequest - if err := kbClient.Get(ctx, kbclient.ObjectKey{ - Namespace: req.Namespace, - Name: req.Name, - }, &updated); err != nil { - // If context expired during Get, let next iteration handle it - if ctx.Err() != nil { - continue - } - return "", fmt.Errorf("failed to get NonAdminDownloadRequest: %w", err) - } - - // Check if the download request was processed successfully - for _, condition := range updated.Status.Conditions { - if condition.Type == "Processed" && condition.Status == "True" { - if updated.Status.VeleroDownloadRequest.Status.DownloadURL != "" { - return updated.Status.VeleroDownloadRequest.Status.DownloadURL, nil - } - } - } - - // Check for failure conditions - for _, condition := range updated.Status.Conditions { - if condition.Status == "True" && condition.Reason == "Error" { - return "", fmt.Errorf("NonAdminDownloadRequest failed: %s - %s", condition.Type, condition.Message) - } - } - } - } -} - -// DownloadContent fetches content from a signed URL and returns it as a string. -// It handles both gzipped and non-gzipped content automatically. -// Uses DefaultHTTPTimeout (or OADP_CLI_REQUEST_TIMEOUT env var) to prevent hanging indefinitely. -func DownloadContent(url string) (string, error) { - return DownloadContentWithTimeout(url, getHTTPTimeout()) -} - -// DownloadContentWithTimeout fetches content from a signed URL with a specified timeout. -// It handles both gzipped and non-gzipped content automatically. -func DownloadContentWithTimeout(url string, timeout time.Duration) (string, error) { - client := httpClientWithTimeout(timeout) - - resp, err := client.Get(url) - if err != nil { - return "", fmt.Errorf("failed to download content from URL %q: %w", url, err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("failed to download content: status %s, body: %s", resp.Status, string(bodyBytes)) - } - - // Use a buffered reader to peek at the content and detect gzip format - bufReader := bufio.NewReader(resp.Body) - var reader io.Reader = bufReader - - // Check if content is gzipped by: - // 1. Content-Encoding header (HTTP-level compression) - // 2. Magic bytes 0x1f 0x8b at start (file-level gzip) - // Object storage signed URLs often serve .gz files without Content-Encoding header, - // so we need to detect gzip by inspecting the actual file content. - isGzipped := strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") - - if !isGzipped { - // Peek at first 2 bytes to check for gzip magic bytes (0x1f 0x8b) - magicBytes, err := bufReader.Peek(2) - if err == nil && len(magicBytes) == 2 && magicBytes[0] == 0x1f && magicBytes[1] == 0x8b { - isGzipped = true - } - } - - if isGzipped { - gzr, err := gzip.NewReader(bufReader) - if err != nil { - return "", fmt.Errorf("failed to create gzip reader: %w", err) - } - defer gzr.Close() - reader = gzr - } - - // Read all content - content, err := io.ReadAll(reader) - if err != nil { - return "", fmt.Errorf("failed to read content: %w", err) - } - - return string(content), nil -} - -// StreamDownloadContent fetches content from a signed URL and streams it to the provided writer. -// This is useful for large files like logs that should be streamed rather than loaded into memory. -// Uses DefaultHTTPTimeout (or OADP_CLI_REQUEST_TIMEOUT env var) to prevent hanging indefinitely. -func StreamDownloadContent(url string, writer io.Writer) error { - return StreamDownloadContentWithTimeout(url, writer, getHTTPTimeout()) -} - -// StreamDownloadContentWithTimeout fetches content from a signed URL with a specified timeout -// and streams it to the provided writer. -func StreamDownloadContentWithTimeout(url string, writer io.Writer, timeout time.Duration) error { - client := httpClientWithTimeout(timeout) - resp, err := client.Get(url) - if err != nil { - return fmt.Errorf("failed to download content from URL %q: %w", url, err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) - return fmt.Errorf("failed to download content: status %s, body: %s", resp.Status, string(bodyBytes)) - } - - // Use a buffered reader to peek at the content and detect gzip format - bufReader := bufio.NewReader(resp.Body) - var reader io.Reader = bufReader - - // Check if content is gzipped by: - // 1. Content-Encoding header (HTTP-level compression) - // 2. Magic bytes 0x1f 0x8b at start (file-level gzip) - // Object storage signed URLs often serve .gz files without Content-Encoding header, - // so we need to detect gzip by inspecting the actual file content. - isGzipped := strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") - - if !isGzipped { - // Peek at first 2 bytes to check for gzip magic bytes (0x1f 0x8b) - magicBytes, err := bufReader.Peek(2) - if err == nil && len(magicBytes) == 2 && magicBytes[0] == 0x1f && magicBytes[1] == 0x8b { - isGzipped = true - } - } - - if isGzipped { - gzr, err := gzip.NewReader(bufReader) - if err != nil { - return fmt.Errorf("failed to create gzip reader: %w", err) - } - defer gzr.Close() - reader = gzr - } - - // Stream content to writer - if _, err := io.Copy(writer, reader); err != nil { - return fmt.Errorf("failed to stream content: %w", err) - } - - return nil -} - -// DefaultOperationTimeout is the default timeout for waiting for download requests to be processed. -const DefaultOperationTimeout = 5 * time.Minute - -// defaultStatusCheckTimeout is the timeout for checking status when formatting timeout errors. -const defaultStatusCheckTimeout = 5 * time.Second - -// FormatDownloadRequestTimeoutError creates a helpful error message when a download request times out. -// It attempts to fetch the current status of the request to provide diagnostic information. -func FormatDownloadRequestTimeoutError(kbClient kbclient.Client, req *nacv1alpha1.NonAdminDownloadRequest, timeout time.Duration) error { - // If client is available, try to get the current status for better diagnostics - if kbClient != nil { - // Use a fresh context to check final status since the original context is expired - statusCtx, cancel := context.WithTimeout(context.Background(), defaultStatusCheckTimeout) - defer cancel() - - var updated nacv1alpha1.NonAdminDownloadRequest - if err := kbClient.Get(statusCtx, kbclient.ObjectKey{ - Namespace: req.Namespace, - Name: req.Name, - }, &updated); err == nil { - // Format status conditions for helpful error message - var statusInfo string - if len(updated.Status.Conditions) > 0 { - var conditions []string - for _, c := range updated.Status.Conditions { - conditions = append(conditions, fmt.Sprintf("%s=%s (reason: %s)", c.Type, c.Status, c.Reason)) - } - statusInfo = fmt.Sprintf(" Current status: %s.", strings.Join(conditions, ", ")) - } - return fmt.Errorf("timed out after %v waiting for NonAdminDownloadRequest %q to be processed. statusInfo: %s", timeout, req.Name, statusInfo) - } - } - - return fmt.Errorf("timed out after %v waiting for NonAdminDownloadRequest %q to be processed", timeout, req.Name) -} diff --git a/cmd/shared/download_test.go b/cmd/shared/download_test.go deleted file mode 100644 index 01c825aad..000000000 --- a/cmd/shared/download_test.go +++ /dev/null @@ -1,552 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 shared - -import ( - "bytes" - "compress/gzip" - "net/http" - "net/http/httptest" - "os" - "strings" - "testing" - "time" - - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" -) - -// TestDefaultHTTPTimeout verifies the default timeout constant -func TestDefaultHTTPTimeout(t *testing.T) { - expected := 10 * time.Minute - if DefaultHTTPTimeout != expected { - t.Errorf("DefaultHTTPTimeout = %v, want %v", DefaultHTTPTimeout, expected) - } -} - -// TestTimeoutEnvVar verifies the environment variable name constant -func TestTimeoutEnvVar(t *testing.T) { - expected := "OADP_CLI_REQUEST_TIMEOUT" - if TimeoutEnvVar != expected { - t.Errorf("TimeoutEnvVar = %q, want %q", TimeoutEnvVar, expected) - } -} - -// TestGetHTTPTimeout tests the getHTTPTimeout function -func TestGetHTTPTimeout(t *testing.T) { - tests := []struct { - name string - envValue string - want time.Duration - }{ - { - name: "no env var set returns default", - envValue: "", - want: DefaultHTTPTimeout, - }, - { - name: "valid duration in minutes", - envValue: "30m", - want: 30 * time.Minute, - }, - { - name: "valid duration in seconds", - envValue: "120s", - want: 120 * time.Second, - }, - { - name: "valid duration in hours", - envValue: "1h", - want: 1 * time.Hour, - }, - { - name: "valid complex duration", - envValue: "1h30m", - want: 90 * time.Minute, - }, - { - name: "invalid duration falls back to default", - envValue: "invalid", - want: DefaultHTTPTimeout, - }, - { - name: "empty string returns default", - envValue: "", - want: DefaultHTTPTimeout, - }, - { - name: "numeric only (no unit) falls back to default", - envValue: "30", - want: DefaultHTTPTimeout, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Save and restore original env var - originalValue := os.Getenv(TimeoutEnvVar) - defer os.Setenv(TimeoutEnvVar, originalValue) - - if tt.envValue != "" { - os.Setenv(TimeoutEnvVar, tt.envValue) - } else { - os.Unsetenv(TimeoutEnvVar) - } - - got := getHTTPTimeout() - if got != tt.want { - t.Errorf("getHTTPTimeout() = %v, want %v", got, tt.want) - } - }) - } -} - -// TestHttpClientWithTimeout verifies that the HTTP client is created with the correct timeout -func TestHttpClientWithTimeout(t *testing.T) { - tests := []struct { - name string - timeout time.Duration - }{ - { - name: "1 minute timeout", - timeout: 1 * time.Minute, - }, - { - name: "30 second timeout", - timeout: 30 * time.Second, - }, - { - name: "default timeout", - timeout: DefaultHTTPTimeout, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := httpClientWithTimeout(tt.timeout) - if client == nil { - t.Fatal("httpClientWithTimeout returned nil") - } - if client.Timeout != tt.timeout { - t.Errorf("client.Timeout = %v, want %v", client.Timeout, tt.timeout) - } - }) - } -} - -// TestDownloadContentWithTimeout tests downloading content with explicit timeout -func TestDownloadContentWithTimeout(t *testing.T) { - tests := []struct { - name string - serverResponse string - serverStatus int - contentType string - gzipped bool - timeout time.Duration - wantContent string - wantErr bool - errContains string - }{ - { - name: "successful plain text download", - serverResponse: "Hello, World!", - serverStatus: http.StatusOK, - contentType: "text/plain", - gzipped: false, - timeout: 5 * time.Second, - wantContent: "Hello, World!", - wantErr: false, - }, - { - name: "successful gzipped download", - serverResponse: "Gzipped content here", - serverStatus: http.StatusOK, - contentType: "application/gzip", - gzipped: true, - timeout: 5 * time.Second, - wantContent: "Gzipped content here", - wantErr: false, - }, - { - name: "server returns 404", - serverResponse: "Not Found", - serverStatus: http.StatusNotFound, - contentType: "text/plain", - gzipped: false, - timeout: 5 * time.Second, - wantErr: true, - errContains: "404", - }, - { - name: "server returns 500", - serverResponse: "Internal Server Error", - serverStatus: http.StatusInternalServerError, - contentType: "text/plain", - gzipped: false, - timeout: 5 * time.Second, - wantErr: true, - errContains: "500", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", tt.contentType) - if tt.gzipped { - w.Header().Set("Content-Encoding", "gzip") - var buf bytes.Buffer - gz := gzip.NewWriter(&buf) - _, _ = gz.Write([]byte(tt.serverResponse)) - gz.Close() - w.WriteHeader(tt.serverStatus) - _, _ = w.Write(buf.Bytes()) - } else { - w.WriteHeader(tt.serverStatus) - _, _ = w.Write([]byte(tt.serverResponse)) - } - })) - defer server.Close() - - content, err := DownloadContentWithTimeout(server.URL, tt.timeout) - - if tt.wantErr { - if err == nil { - t.Errorf("DownloadContentWithTimeout() expected error, got nil") - } else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { - t.Errorf("DownloadContentWithTimeout() error = %v, want error containing %q", err, tt.errContains) - } - return - } - - if err != nil { - t.Errorf("DownloadContentWithTimeout() unexpected error: %v", err) - return - } - - if content != tt.wantContent { - t.Errorf("DownloadContentWithTimeout() = %q, want %q", content, tt.wantContent) - } - }) - } -} - -// TestDownloadContent tests that DownloadContent uses the default timeout mechanism -func TestDownloadContent(t *testing.T) { - // Save and restore original env var - originalValue := os.Getenv(TimeoutEnvVar) - defer os.Setenv(TimeoutEnvVar, originalValue) - os.Unsetenv(TimeoutEnvVar) - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("test content")) - })) - defer server.Close() - - content, err := DownloadContent(server.URL) - if err != nil { - t.Errorf("DownloadContent() unexpected error: %v", err) - return - } - - if content != "test content" { - t.Errorf("DownloadContent() = %q, want %q", content, "test content") - } -} - -// TestStreamDownloadContentWithTimeout tests streaming content with explicit timeout -func TestStreamDownloadContentWithTimeout(t *testing.T) { - tests := []struct { - name string - serverResponse string - serverStatus int - contentType string - gzipped bool - timeout time.Duration - wantContent string - wantErr bool - errContains string - }{ - { - name: "successful plain text stream", - serverResponse: "Streaming content", - serverStatus: http.StatusOK, - contentType: "text/plain", - gzipped: false, - timeout: 5 * time.Second, - wantContent: "Streaming content", - wantErr: false, - }, - { - name: "successful gzipped stream", - serverResponse: "Gzipped streaming content", - serverStatus: http.StatusOK, - contentType: "application/gzip", - gzipped: true, - timeout: 5 * time.Second, - wantContent: "Gzipped streaming content", - wantErr: false, - }, - { - name: "server returns 403", - serverResponse: "Forbidden", - serverStatus: http.StatusForbidden, - contentType: "text/plain", - gzipped: false, - timeout: 5 * time.Second, - wantErr: true, - errContains: "403", - }, - { - name: "large content stream", - serverResponse: strings.Repeat("Large content block. ", 1000), - serverStatus: http.StatusOK, - contentType: "text/plain", - gzipped: false, - timeout: 5 * time.Second, - wantContent: strings.Repeat("Large content block. ", 1000), - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", tt.contentType) - if tt.gzipped { - w.Header().Set("Content-Encoding", "gzip") - var buf bytes.Buffer - gz := gzip.NewWriter(&buf) - _, _ = gz.Write([]byte(tt.serverResponse)) - gz.Close() - w.WriteHeader(tt.serverStatus) - _, _ = w.Write(buf.Bytes()) - } else { - w.WriteHeader(tt.serverStatus) - _, _ = w.Write([]byte(tt.serverResponse)) - } - })) - defer server.Close() - - var buf bytes.Buffer - err := StreamDownloadContentWithTimeout(server.URL, &buf, tt.timeout) - - if tt.wantErr { - if err == nil { - t.Errorf("StreamDownloadContentWithTimeout() expected error, got nil") - } else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { - t.Errorf("StreamDownloadContentWithTimeout() error = %v, want error containing %q", err, tt.errContains) - } - return - } - - if err != nil { - t.Errorf("StreamDownloadContentWithTimeout() unexpected error: %v", err) - return - } - - if buf.String() != tt.wantContent { - t.Errorf("StreamDownloadContentWithTimeout() = %q, want %q", buf.String(), tt.wantContent) - } - }) - } -} - -// TestStreamDownloadContent tests that StreamDownloadContent uses the default timeout mechanism -func TestStreamDownloadContent(t *testing.T) { - // Save and restore original env var - originalValue := os.Getenv(TimeoutEnvVar) - defer os.Setenv(TimeoutEnvVar, originalValue) - os.Unsetenv(TimeoutEnvVar) - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("streamed test content")) - })) - defer server.Close() - - var buf bytes.Buffer - err := StreamDownloadContent(server.URL, &buf) - if err != nil { - t.Errorf("StreamDownloadContent() unexpected error: %v", err) - return - } - - if buf.String() != "streamed test content" { - t.Errorf("StreamDownloadContent() = %q, want %q", buf.String(), "streamed test content") - } -} - -// TestDownloadContentWithTimeout_InvalidURL tests handling of invalid URLs -func TestDownloadContentWithTimeout_InvalidURL(t *testing.T) { - _, err := DownloadContentWithTimeout("http://invalid-url-that-does-not-exist.local:12345", 1*time.Second) - if err == nil { - t.Error("DownloadContentWithTimeout() expected error for invalid URL, got nil") - } -} - -// TestStreamDownloadContentWithTimeout_InvalidURL tests handling of invalid URLs in streaming -func TestStreamDownloadContentWithTimeout_InvalidURL(t *testing.T) { - var buf bytes.Buffer - err := StreamDownloadContentWithTimeout("http://invalid-url-that-does-not-exist.local:12345", &buf, 1*time.Second) - if err == nil { - t.Error("StreamDownloadContentWithTimeout() expected error for invalid URL, got nil") - } -} - -// TestGetHTTPTimeoutWithEnvVar tests that the env var override works correctly -func TestGetHTTPTimeoutWithEnvVar(t *testing.T) { - // Save and restore original env var - originalValue := os.Getenv(TimeoutEnvVar) - defer os.Setenv(TimeoutEnvVar, originalValue) - - // Set custom timeout - os.Setenv(TimeoutEnvVar, "5m") - - timeout := getHTTPTimeout() - expected := 5 * time.Minute - - if timeout != expected { - t.Errorf("getHTTPTimeout() with env var = %v, want %v", timeout, expected) - } -} - -// TestGetHTTPTimeoutWithOverride tests the priority order: override > env var > default -func TestGetHTTPTimeoutWithOverride(t *testing.T) { - tests := []struct { - name string - override time.Duration - envValue string - want time.Duration - }{ - { - name: "override takes precedence over env var", - override: 15 * time.Minute, - envValue: "30m", - want: 15 * time.Minute, - }, - { - name: "override takes precedence over default when no env var", - override: 20 * time.Minute, - envValue: "", - want: 20 * time.Minute, - }, - { - name: "zero override falls back to env var", - override: 0, - envValue: "25m", - want: 25 * time.Minute, - }, - { - name: "zero override and no env var falls back to default", - override: 0, - envValue: "", - want: DefaultHTTPTimeout, - }, - { - name: "zero override with invalid env var falls back to default", - override: 0, - envValue: "invalid", - want: DefaultHTTPTimeout, - }, - { - name: "small override value is respected", - override: 30 * time.Second, - envValue: "10m", - want: 30 * time.Second, - }, - { - name: "large override value is respected", - override: 2 * time.Hour, - envValue: "5m", - want: 2 * time.Hour, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Save and restore original env var - originalValue := os.Getenv(TimeoutEnvVar) - defer os.Setenv(TimeoutEnvVar, originalValue) - - if tt.envValue != "" { - os.Setenv(TimeoutEnvVar, tt.envValue) - } else { - os.Unsetenv(TimeoutEnvVar) - } - - got := GetHTTPTimeoutWithOverride(tt.override) - if got != tt.want { - t.Errorf("GetHTTPTimeoutWithOverride(%v) = %v, want %v", tt.override, got, tt.want) - } - }) - } -} - -// TestDefaultOperationTimeout verifies the default operation timeout constant -func TestDefaultOperationTimeout(t *testing.T) { - expected := 5 * time.Minute - if DefaultOperationTimeout != expected { - t.Errorf("DefaultOperationTimeout = %v, want %v", DefaultOperationTimeout, expected) - } -} - -// TestFormatDownloadRequestTimeoutError_NilClient tests error formatting when client is nil or request fails -func TestFormatDownloadRequestTimeoutError_BasicMessage(t *testing.T) { - // Test that the function returns a properly formatted error message - // even when we can't fetch the status (simulated by passing nil client) - timeout := 5 * time.Minute - - // Create a mock request - req := &nacv1alpha1.NonAdminDownloadRequest{} - req.Name = "test-backup-logs-abc123" - req.Namespace = "test-namespace" - - // With a nil client, the Get will fail, so we'll get the basic error message - err := FormatDownloadRequestTimeoutError(nil, req, timeout) - - // Should contain timeout duration and request name - if err == nil { - t.Fatal("expected error, got nil") - } - - errStr := err.Error() - if !strings.Contains(errStr, "5m0s") { - t.Errorf("error should contain timeout duration '5m0s', got: %s", errStr) - } - if !strings.Contains(errStr, "test-backup-logs-abc123") { - t.Errorf("error should contain request name, got: %s", errStr) - } - if !strings.Contains(errStr, "timed out") { - t.Errorf("error should contain 'timed out', got: %s", errStr) - } -} - -// TestGetHTTPTimeoutWithOverride_ZeroReturnsDefault verifies that zero override with no env var returns default -func TestGetHTTPTimeoutWithOverride_ZeroReturnsDefault(t *testing.T) { - // Save and restore original env var - originalValue := os.Getenv(TimeoutEnvVar) - defer os.Setenv(TimeoutEnvVar, originalValue) - os.Unsetenv(TimeoutEnvVar) - - got := GetHTTPTimeoutWithOverride(0) - if got != DefaultHTTPTimeout { - t.Errorf("GetHTTPTimeoutWithOverride(0) without env var = %v, want %v", got, DefaultHTTPTimeout) - } -} diff --git a/cmd/shared/factories.go b/cmd/shared/factories.go index cb83d1ac9..40019be34 100644 --- a/cmd/shared/factories.go +++ b/cmd/shared/factories.go @@ -21,40 +21,11 @@ import ( "fmt" "os" "path/filepath" - "strings" ) // ClientConfig represents the structure of the Velero client configuration file type ClientConfig struct { - Namespace string `json:"namespace"` - NonAdmin interface{} `json:"nonadmin,omitempty"` - DefaultNABSL string `json:"default-nabsl,omitempty"` -} - -// IsNonAdmin returns true if the nonadmin configuration is enabled. -// Handles both boolean and string representations since -// `oc oadp client config set nonadmin=true` stores the value as a string. -func (c *ClientConfig) IsNonAdmin() bool { - if c == nil { - return false - } - switch v := c.NonAdmin.(type) { - case bool: - return v - case string: - return strings.EqualFold(v, "true") - default: - return false - } -} - -// GetDefaultNABSL returns the default NonAdminBackupStorageLocation if set. -// Returns empty string if not configured. -func (c *ClientConfig) GetDefaultNABSL() string { - if c == nil { - return "" - } - return c.DefaultNABSL + Namespace string `json:"namespace"` } // ReadVeleroClientConfig reads the Velero client configuration from ~/.config/velero/config.json @@ -115,18 +86,6 @@ func readConfigMap(configPath string) (map[string]interface{}, error) { // mergeClientConfig merges the ClientConfig into the config map, updating only managed keys func mergeClientConfig(configMap map[string]interface{}, config *ClientConfig) { configMap["namespace"] = config.Namespace - - if config.NonAdmin != nil { - configMap["nonadmin"] = config.NonAdmin - } else { - delete(configMap, "nonadmin") - } - - if config.DefaultNABSL != "" { - configMap["default-nabsl"] = config.DefaultNABSL - } else { - delete(configMap, "default-nabsl") - } } // writeConfigMap writes the config map to the specified path @@ -151,7 +110,7 @@ func writeConfigMap(configPath string, configMap map[string]interface{}) error { } // WriteVeleroClientConfig writes the client configuration to ~/.config/velero/config.json -// It merges only the keys managed by this CLI (namespace, nonadmin, default-nabsl) +// It merges only the keys managed by this CLI (namespace) // with the existing config file, preserving any other Velero configuration keys. func WriteVeleroClientConfig(config *ClientConfig) error { configPath, err := getVeleroConfigPath() diff --git a/cmd/shared/nabsl_requests.go b/cmd/shared/nabsl_requests.go deleted file mode 100644 index 1e89934b7..000000000 --- a/cmd/shared/nabsl_requests.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2025 The OADP CLI Contributors. - -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 shared - -import ( - "context" - "fmt" - - "k8s.io/apimachinery/pkg/api/errors" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" -) - -// FindNABSLRequestByNameOrUUID finds a NonAdminBackupStorageLocationRequest by either: -// 1. Direct UUID lookup (if nameOrUUID is the actual request UUID) -// 2. NABSL name lookup (searches through all requests to find one with matching source NABSL name) -// -// This handles the common pattern where users can specify either the NABSL-friendly name -// or the system-generated UUID for approval/rejection operations. -func FindNABSLRequestByNameOrUUID(ctx context.Context, client kbclient.WithWatch, nameOrUUID string, adminNamespace string) (string, error) { - - // UUID lookup - var testRequest nacv1alpha1.NonAdminBackupStorageLocationRequest - err := client.Get(ctx, kbclient.ObjectKey{ - Name: nameOrUUID, - Namespace: adminNamespace, - }, &testRequest) - if err == nil { - return nameOrUUID, nil - } else if errors.IsNotFound(err) { - return "", fmt.Errorf("request for NABSL %q not found", nameOrUUID) - } - - // Fallback:Match NABSL name to UUID - var requestList nacv1alpha1.NonAdminBackupStorageLocationRequestList - err = client.List(ctx, &requestList, kbclient.InNamespace(adminNamespace)) - if err != nil { - return "", fmt.Errorf("failed to list requests: %w", err) - } - - for _, request := range requestList.Items { - if request.Status.SourceNonAdminBSL != nil && - request.Status.SourceNonAdminBSL.Name == nameOrUUID { - return request.Name, nil - } - } - - return "", fmt.Errorf("request for NABSL %q not found", nameOrUUID) -} diff --git a/docs/OADP-1.5.md b/docs/OADP-1.5.md deleted file mode 100644 index 74037c8a2..000000000 --- a/docs/OADP-1.5.md +++ /dev/null @@ -1,104 +0,0 @@ -# OADP CLI — `oadp-1.5` branch - -Developer guide for the **OADP 1.5 release line** of `migtools/oadp-cli`. - -## Purpose - -Branch **`oadp-1.5`** builds the kubectl plugin (`kubectl oadp`) and the **download server** (`Containerfile.download`) against the same dependency stack as **OADP Operator 1.5** (Velero 1.16, `oadp-non-admin` oadp-1.5, etc.). - -It is branched from **`oadp-1.6`** with **dependency and version-string changes only**. No CLI commands were removed for this backport. - -| OpenShift (typical) | OADP operator | oadp-cli branch | -|---------------------|---------------|-----------------| -| 4.19 – 4.21 | 1.5 | **`oadp-1.5`** | -| 4.22+ | 1.6 | **`oadp-1.6`** | - -Reference: [OADP PARTNERS.md](https://github.com/openshift/oadp-operator/blob/oadp-dev/PARTNERS.md) - -## `oadp-1.5` vs `oadp-1.6` - -### Code changes - -| File | Change | -|------|--------| -| `go.mod` / `go.sum` | Downgrade module pins to OADP 1.5 line | -| `Makefile` | `VERSION ?= oadp-1.5` | -| `Containerfile.download` | `OADP_VERSION=oadp-1.5` | - -### Dependency comparison - -| Component | `oadp-1.5` (this branch) | `oadp-1.6` | -|-----------|---------------------------|------------| -| **Velero module** | `v1.16.0` | `v1.18.1` | -| **openshift/velero replace** | `…20260526…87a03c3d2c32` | `…20260601…af1b4409d3db` | -| **controller-runtime** | `v0.19.3` | `v0.21.0` | -| **oadp-non-admin** | `aad3132759e1` (oadp-1.5) | `54d1934bbb11` (oadp-1.6) | -| **openshift/oadp-operator replace** | not pinned in go.mod (via `oadp-non-admin`) | not pinned in go.mod | -| **kopia replace** | removed | `github.com/migtools/kopia` | -| **Go** | `1.25.8` | `1.25.0` | -| **k8s.io/client-go** (direct) | `v0.33.11` | `v0.33.11` | -| **Version string** | `oadp-1.5` | `oadp-1.6` | - - -### CLI features: same as `oadp-1.6` - -The plugin on **`oadp-1.5`** exposes the **same commands** as **`oadp-1.6`**, including: - -- Admin Velero commands (`backup`, `restore`, `schedule`, …) -- `nonadmin` (NAB / NAR) -- `nabsl-request` -- `must-gather`, `setup`, `client`, `completion` - -### OADP product differences (platform, not oadp-cli-only) - -Use **OADP 1.5 operator** on the cluster with this CLI. OADP **1.6** adds platform changes this branch does **not** target, for example: - -- **Velero 1.18** (vs 1.16 on 1.5) -- **Restic uploader removed** for new backups on the 1.6 line (Kopia-only for new FS backups) -- Planned **automatic operator upgrades** (file-based catalogs) on 1.6 -- New operator CRDs on 1.6 (e.g. VM file restore) — not required for this CLI backport - -See [OADP operator wiki](https://github.com/openshift/oadp-operator/wiki/Latest-OADP-product-release-updates) and release notes for product-level detail. - -## Build and test - -```bash -git checkout oadp-1.5 -make build -make install ASSUME_DEFAULT=true -kubectl oadp version -``` - -Run the full dev test suite: - -```bash -# If nonadmin=true in config.json, admin-command tests fail. -kubectl oadp client config set nonadmin=false - -make test -make lint # optional -``` - -Local image build: - -```bash -podman build -f Containerfile.download -t oadp-cli-oadp-1.5:local . -``` - -## Branch workflow - -| Branch | Use | -|--------|-----| -| **`oadp-1.5`** | OADP 1.5 releases, OCP 4.19–4.21 | -| **`oadp-1.6`** | OADP 1.6 releases, OCP 4.22+ | -| **`oadp-dev`** | Development / next release | - -Bugfixes for 1.5: branch from **`oadp-1.5`**, cherry-pick to **`oadp-1.6`** / **`oadp-dev`** as maintainers direct. - -## Related repos - -| Repo | Branch | Role | -|------|--------|------| -| [openshift/oadp-operator](https://github.com/openshift/oadp-operator) | `oadp-1.5` | Operator, Velero, DPA | -| [migtools/oadp-non-admin](https://github.com/migtools/oadp-non-admin) | `oadp-1.5` | NAB CRDs / controllers | -| **migtools/oadp-cli** | **`oadp-1.5`** | This plugin + download server | diff --git a/docs/oadp-self-service.md b/docs/oadp-self-service.md deleted file mode 100644 index 591c3f91c..000000000 --- a/docs/oadp-self-service.md +++ /dev/null @@ -1,635 +0,0 @@ -# OADP Self Service Overview - -## Overview - -OADP Self Service enables non-administrator users to perform backup and restore operations in their authorized namespaces without requiring -cluster-wide administrator privileges. This feature provides secure self-service data protection capabilities while maintaining proper administrator -controls, restrictions and enforcements over the user's backup and restore operations. - -### Key Benefits - -- Allows users to perform namespace-scoped backup and restore operations -- Provides users with secure access to backup logs and status information -- Enables users to create dedicated backup storage locations with user owned buckets and credentials -- Maintains cluster administrator control over non-administrator operations through -restrictions and enforcements - - -## OADP Self Service Details - -OADP self-service introduces a significant change to backup and restore operations in OpenShift. Previously, only cluster administrators could perform these operations. -Now, regular OpenShift users can perform backup and restore operations within their authorized namespaces. -This is achieved through custom resources that securely manage these operations while maintaining proper access controls and visibility. -The self-service functionality is implemented in a way that ensures users can only operate within their assigned namespaces and permissions, -while cluster administrators maintain overall control through restrictions and enforcements. - -### Glossary of terms - -* **NAB** - NonAdminBackup. A custom resource that users directly create to request a velero backup of the namespace from which the NAB object is created. -* **NAR** - NonAdminRestore. A custom resource that users directly create to request a velero restore of the namespace from which the NAR object is created. -* **NAC** - NonAdminController. A controller that validates the NAB and NAR objects and creates the velero backup and restore and related objects. The NAC is essentially a proxy between non admin users and velero. -* **NABSL** - NonAdminBackupStorageLocation. A custom resource that users directly create to request a velero backup storage location. Users can use object storage that is specifically created for their project, deliniated from other users and projects. -* **NADR** - NonAdminDownloadRequest. A custom resource that users directly create to request a velero backup download. Users will be provided with a secured URL to download details regarding the backup or restore. - -### Cluster Administrator Setup - -Install and configure the OADP operator according to the documentation and your requirements. - -To enable OADP Self-Service the DPA spec must the spec.nonAdmin.enable field to true. - -``` - nonAdmin: - enable: true -``` - -Once the OADP DPA is reconciled the cluster administrator should see the non-admin-controller running in the openshift-adp namespace. The Openshift users without cluster admin rights will now be able to create NAB or NAR objects and related objects in their namespace to create a backup or restore. - -## OpenShift User Instructions - -Prior to OpenShift users taking advantage of OADP self-service feature the OpenShift cluster administrator must have completed the following prerequisite steps: - -* The OADP DPA has been configured to support self-service -* The cluster administrator has created the users - * account - * namespace - * namespace privileges, e.g. namespace admin. - * optionally the cluster administrator can create a NABSL for the user. - -### OpenShift self-service required permissions: - -Ensure users have appropriate permissions in its namespace. Users must have editor roles for the following objects in their namespace. - * nonadminbackups.oadp.openshift.io - * nonadminbackupstoragelocations.oadp.openshift.io - * nonadminrestores.oadp.openshift.io - * nonadmindownloadrequests.oadp.openshift.io - - For example - ```yaml - # config/rbac/nonadminbackup_editor_role.yaml - - apiGroups: - - oadp.openshift.io - resources: - - nonadminbackups - - nonadminrestores - - nonadminbackupstoragelocations - - nonadmindownloadrequests - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - oadp.openshift.io - resources: - - nonadminbackups/status - verbs: - - get - # config/rbac/nonadminrestore_editor_role.yaml - - apiGroups: - - oadp.openshift.io - resources: - - nonadminrestores - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - oadp.openshift.io - resources: - - nonadminrestores/status - verbs: - - get - ``` - - **note** Users will not be able to edit the NABSLs that led to the creation of the BSL. They will only have the ability to create new NABSLs. The NAC controller will disallow such edit and inform user within the condition of the edited NABSL object. - -## Self-Service workflow - -Non Cluster Administrators can utilize OADP self-service by creating NonAdminBackup (NAB) and NonAdminRestore (NAR) objects in the namespace to be backed up or restored. A NonAdminBackup is an OpenShift custom resource that securely facilitates the creation, status and lifecycle of a Velero Backup custom resource. - -```mermaid -sequenceDiagram - participant User - participant NAB as NonAdminBackup - participant NAC as NonAdminController - participant VB as Velero Backup - - User->>NAB: Creates NonAdminBackup CR - NAB->>NAC: Detected by controller - NAC-->>NAB: Validates backup request - NAC->>VB: Creates Velero Backup CR - VB-->>NAB: Status updates - NAB-->>User: View backup status -``` - -![nab-backup-workflow](https://hackmd.io/_uploads/BJz4bEbKyx.jpg) - -For the most part one can think of a NonAdminBackup and a Velero Backup in very much the same way. Both objects specify a velero backup and how the backup should be executed. There are a few differences to keep in mind when creating a NonAdminBackup. - -1. The NonAdminBackup creates the Velero Backup CR instance in a secure way that limits the users access. -2. A user cannot specify the namespace that will be backed up. The namespace from which the NAB object is created is the defined namespace to be backed up. -3. In addition to the creation of the Velero Backup the NonAdminBackup object's main purpose is to track the status of the Velero Backup in a secure and clear way. - -### NonAdminBackup NAB Example: - -``` -apiVersion: oadp.openshift.io/v1alpha1 -kind: NonAdminBackup -metadata: - name: mybackup-1 - namespace: nacuser1 -spec: - backupSpec: - snapshotMoveData: true -``` - -Once created the NAB will look similar to the following: - -``` -apiVersion: oadp.openshift.io/v1alpha1 -kind: NonAdminBackup -metadata: - creationTimestamp: "2025-02-21T20:57:35Z" - finalizers: - - nonadminbackup.oadp.openshift.io/finalizer - generation: 2 - name: mybackup-1 - namespace: nacuser1 <--- The namespace that NAC controller will set on the Velero Backup object to backup - resourceVersion: "20714121" - uid: 93effb39-9762-4d04-8e9e-194ebe6b9b31 -spec: - backupSpec: - snapshotMoveData: true -status: - conditions: - - lastTransitionTime: "2025-02-21T20:57:35Z" - message: backup accepted <--- The NAC controller reconciled the NAB object and created the Velero Backup object - reason: BackupAccepted - status: "True" - type: Accepted - - lastTransitionTime: "2025-02-21T20:57:35Z" - message: Created Velero Backup object - reason: BackupScheduled - status: "True" - type: Queued - phase: Created <--- The NAB object is in the Created phase - queueInfo: - estimatedQueuePosition: 0 <--- The NAB object status in the queue, once complete it is set to 0 - veleroBackup: - nacuuid: nacuser1-mybackup-1-588ce989-387e-4352-b04a-1fd6b7712370 <--- The NAC controller created the Velero Backup object and set the nacuuid - name: nacuser1-mybackup-1-588ce989-387e-4352-b04a-1fd6b7712370 <--- The associated Velero Backup name - namespace: openshift-adp - status: <--- The status of the Velero backup object displayed by the NAB object - backupItemOperationsAttempted: 3 - backupItemOperationsCompleted: 3 - completionTimestamp: "2025-02-21T20:59:28Z" - expiration: "2025-03-23T20:57:35Z" - formatVersion: 1.1.0 - hookStatus: {} - phase: Completed <--- The Velero backup object is in the Completed phase, successful - progress: - itemsBackedUp: 57 - totalItems: 57 - startTimestamp: "2025-02-21T20:57:35Z" - version: 1 -``` -The complete nonAdminBackup resource definition can be found here: [NonAdminBackup CRD](https://github.com/openshift/oadp-operator/blob/master/bundle/manifests/oadp.openshift.io_nonadminbackups.yaml) - -### NonAdminRestore NAR Example: - -``` -apiVersion: oadp.openshift.io/v1alpha1 -kind: NonAdminRestore -metadata: - name: example - namespace: nacuser1 -spec: - restoreSpec: - backupName: mybackup-1 <-- references the NAB object, not the Velero backup object -``` - -Once created the NAR will look similar to the following: - -``` -apiVersion: oadp.openshift.io/v1alpha1 -kind: NonAdminRestore -metadata: - creationTimestamp: "2025-02-21T21:12:54Z" - finalizers: - - nonadminrestore.oadp.openshift.io/finalizer - generation: 2 - name: example - namespace: nacuser1 - resourceVersion: "20719136" - uid: 0f1d8346-d8be-4621-8d67-0877f15e82fb -spec: - restoreSpec: - backupName: mybackup-1 - hooks: {} - itemOperationTimeout: 0s -status: - conditions: - - lastTransitionTime: "2025-02-21T21:12:54Z" - message: restore accepted <--- The NAC controller reconciled the NAR object and created the Velero Restore object - reason: RestoreAccepted - status: "True" - type: Accepted - - lastTransitionTime: "2025-02-21T21:12:54Z" - message: Created Velero Restore object <--- The NAC controller created the Velero Restore object - reason: RestoreScheduled - status: "True" - type: Queued - phase: Created <--- The NAR object is in the Created phase - queueInfo: - estimatedQueuePosition: 0 <--- The NAR object status in the queue, once complete it is set to 0 - veleroRestore: - nacuuid: nacuser1-example-b844be97-7ee4-4702-91b8-ffc84697675a <--- The NAC controller created the Velero Restore object and set the nacuuid - name: nacuser1-example-b844be97-7ee4-4702-91b8-ffc84697675a <--- The associated Velero Restore name - namespace: openshift-adp - status: - completionTimestamp: "2025-02-21T21:12:57Z" - hookStatus: {} - phase: Completed <--- The Velero restore object is in the Completed phase, successful - progress: - itemsRestored: 54 - totalItems: 54 - startTimestamp: "2025-02-21T21:12:55Z" - warnings: 0 -``` -The complete nonAdminRestore resource definition can be found here: [NonAdminRestore CRD](https://github.com/openshift/oadp-operator/blob/master/bundle/manifests/oadp.openshift.io_nonadminrestores.yaml) - -### NonAdminBackupStorageLocation NABSL: -Cluster administrators can gain efficiencies by delegating backup and restore operations to OpenShift users. It is recommended that cluster administrators carefully manage the NABSL to conform to any company policies, compliance requirements, etc. - -1. **Direct Creation**: Cluster administrators can create NABSLs directly for non-admin users. -2. **Approval Workflow**: Cluster administrators can enable an approval process where: - - Users create a NABSL, which triggers the creation of a NonAdminBackupStorageLocationRequest object in the openshift-adp namespace. - - Administrators review and either approve or reject these requests. Once approved, a Velero BSL is created in the openshift-adp namespace, and the user is notified of the approval on the NABSL status. If rejected, the status of the NABSL is updated to reflect the rejection. - - Administrators can also revoke previously approved NABSL, which results in the removal of the Velero BSL, and the user is notified of the rejection. This is achieved by modifying the approve field back to pending or reject. - - This is an opt-in feature and must be explicitly enabled by the cluster administrator. -3. **Automatic Approval**: Users create NABSL from the user namespace, these are automatically approved when nonAdmin.requireApprovalForBSL is set to false or not set. - -For security purposes it is recommended that cluster administrators use either the direct creation or the approval workflow. The automatic approval option is less secure as it does not require administrator review. It should also be noted that updating the NABSL after the initial creation will NOT change the associated Velero BSL. Updating the NABSL is not supported for non-admin users. - - -To enable the approval workflow the DPA spec must be set as follows: -``` - nonAdmin: - enable: true - requireApprovalForBSL: true -``` -Cluster administrators can view the NABSLApprovalRequest object in the openshift-adp namespace. - -``` -oc -n openshift-adp get NonAdminBackupStorageLocationRequests -``` -Approval or rejection is accomplished by updating the NABSLApprovalRequest object. - -``` -spec: - approvalDecision: reject [accept, reject] -``` - -If approved both the NABSL and the BSL are created. The NABSL is created in the users namespace, while the BSL is created in the OADP namespace, such as openshift-adp. - -### User Creation of NABSL: - -A non-admin user can create a NABSL in their namespace. - -``` -apiVersion: oadp.openshift.io/v1alpha1 -kind: NonAdminBackupStorageLocation -metadata: - name: nacuser1-nabsl - namespace: nacuser1 -spec: - backupStorageLocationSpec: - config: - checksumAlgorithm: "" - profile: default - region: us-west-2 - credential: - key: cloud - name: cloud-credentials - objectStorage: - bucket: bucket1uswest2 - prefix: velero - provider: aws -``` - -If the cluster administrator has enabled the requireApprovalForBSL flag then the NABSL will be in the Pending state until the cluster administrator approves the NABSL. - - - -### NAB / NAR Status - -#### Phase -The phase field is a simple one high-level summary of the lifecycle of the objects, that only moves forward. Once a phase changes, it can not return to the previous value. - -| **Value** | **Description** | -|-----------|-----------------| -| New | *NonAdminBackup/NonAdminRestore* resource was accepted by the NAB/NAR Controller, but it has not yet been validated by the NAB/NAR Controller | -| BackingOff | *NonAdminBackup/NonAdminRestore* resource was invalidated by the NAB/NAR Controller, due to invalid Spec. NAB/NAR Controller will not reconcile the object further, until user updates it | -| Created | *NonAdminBackup/NonAdminRestore* resource was validated by the NAB/NAR Controller and Velero *Backup/restore* was created. The Phase will not have additional information about the *Backup/Restore* run | -| Deletion | *NonAdminBackup/NonAdminRestore* resource has been marked for deletion. The NAB/NAR Controller will delete the corresponding Velero *Backup/Restore* if it exists. Once this deletion completes, the *NonAdminBackup/NonAdminRestore* object itself will also be removed | - - - - - -## Advanced Cluster Administrator Features - -### Cluster Administrator Enforceable Spec Fields -There are several types of cluster scoped objects that non-admin users should not have access to backup or restore. OADP self-service automatically excludes the following list of cluster scoped resources from being backed up or restored. - -* SCCs -* ClusterRoles -* ClusterRoleBindings -* CRDs -* PriorityClasses -* virtualmachineclusterinstancetypes -* virtualmachineclusterpreferences - -Cluster administrators may also enforce company or compliance policy by utilizing templated NABSL's, NAB's and NAR's that require fields values to be set and conform to the administrator defined policy. Admin Enforceable fields are fields that the cluster administrator can enforce non cluster admin users to use. Restricted fields are automatically managed by OADP and cannot be modified by either administrators or users. - -#### NABSL -The following NABSL fields are currently supported for template enforcement: - -| **NABSL Field** | **Admin Enforceable** | **Restricted** | **special case** | -|----------------------------|-----------------|----------------|-----------------| -| `backupSyncPeriod` | | | ⚠️ Must be set lower than the DPA.backupSyncPeriod and lower than the garbage collection period | -| `provider` | | | ⚠️ special case | -| `objectStorage` | ✅ Yes | | | -| `credential` | ✅ Yes | | | -| `config` | ✅ Yes | | | -| `accessMode` | ✅ Yes | | | -| `validationFrequency` | ✅ Yes | | | -| `default` | | | ⚠️ Must be false or empty | - -For example if the cluster administrator wanted to mandate that all NABSL's used a particular aws s3 bucket. - -``` -spec: - config: - checksumAlgorithm: "" - profile: default - region: us-west-2 - credential: - key: cloud - name: cloud-credentials - objectStorage: - bucket: my-company-bucket <--- - prefix: velero - provider: aws -``` -The DPA spec must be set in the following way: - -``` -nonAdmin: - enable: true - enforceBSLSpec: - config: <--- entire config must match expected NaBSL config - checksumAlgorithm: "" - profile: default - region: us-west-2 - objectStorage: <--- all of the objectStorage options must match expected NaBSL options - bucket: my-company-bucket - prefix: velero - provider: aws -``` - -#### Restricted NonAdminBackups - -In the same sense as the NABSL, cluster administrators can also restrict the NonAdminBackup spec fields to ensure the backup request conforms to the administrator defined policy. Most of the backup spec fields can be restricted by the cluster administrator, below is a table of reference for the current implementation. - - -| **Backup Spec Field** | **Admin Enforceable** | **Restricted** | **special case** | -|--------------------------------------------|--------------|--------------------------|-----------------| -| `csiSnapshotTimeout` | ✅ Yes | | | -| `itemOperationTimeout` | ✅ Yes | | | -| `resourcePolicy` | ✅ Yes | | ⚠️ Non-admin users can specify the config-map that admins created in OADP Operator NS(Admins enforcing this value be a good alternative here), they cannot specify their own configmap as its lifecycle handling is not currently managed by NAC controller | -| `includedNamespaces` | ❌ No | ✅ Yes | ⚠️ Admins cannot enforce this because it does not make sense for a cluster wide non-admin backup setting, we have validations in place such that only the NS admins NS in included in the NAB spec. | -| `excludedNamespaces` | ✅ Yes | ✅ Yes | ⚠️ This spec is restricted for non-admin users and hence not enforceable by admins | -| `includedResources` | ✅ Yes | | | -| `excludedResources` | ✅ Yes | | | -| `orderedResources` | ✅ Yes | | | -| `includeClusterResources` | ✅ Yes | | ⚠️ Non-admin users can only set this spec to false if they want, all other values are restricted, similar rule for admin enforcement regarding this spec value. | -| `excludedClusterScopedResources` | ✅ Yes | | | -| `includedClusterScopedResources` | ✅ Yes | | ⚠️ This spec is restricted and non-enforceable, only empty list is acceptable | -| `excludedNamespaceScopedResources` | ✅ Yes | | | -| `includedNamespaceScopedResources` | ✅ Yes | | | -| `labelSelector` | ✅ Yes | | | -| `orLabelSelectors` | ✅ Yes | | | -| `snapshotVolumes` | ✅ Yes | | | -| `storageLocation` | | | ⚠️ Can be empty (implying default BSL usage) or needs to be an existing NABSL | -| `volumeSnapshotLocations` | | | ⚠️ Not supported for non-admin users, default will be used if needed | -| `ttl` | ✅ Yes | | | -| `defaultVolumesToFsBackup` | ✅ Yes | | | -| `snapshotMoveData` | ✅ Yes | | | -| `datamover` | ✅ Yes | | | -| `uploaderConfig.parallelFilesUpload` | ✅ Yes | | | -| `hooks` | | | ⚠️ special case | - -An example enforcement set in the DPA spec to enforce the - * ttl to be set to "158h0m0s" - * snapshotMoveData to be set to true - -``` - nonAdmin: - enable: true - enforcedBackupSpec.ttl: "158h0m0s" - enforcedBackupSpec.snapshotMoveData: true -``` - -#### Restricted NonAdminRestore NAR - -NonAdminRestores spec fields can also be restricted by the cluster administrator. The following NAR spec fields are currently supported for template enforcement: - -| **Field** | **Admin Enforceable** | **Restricted** | **special case** | -|-------------------------------|--------------|--------------------|-----------------| -| `backupName` | ❌ No | | | -| `scheduleName` | ❌ No | ✅ Yes | ⚠️ not supported for non-admin users, we don't have non-admin backup schedule API as of now. | -| `itemOperationTimeout` | ✅ Yes | | | -| `uploaderConfig` | ✅ Yes | | | -| `includedNamespaces` | ❌ No | ✅ Yes | ⚠️ restricted for non-admin users and hence non-enforceable by admins | -| `excludedNamespaces` | ❌ No | ✅ Yes | ⚠️ restricted for non-admin users and hence non-enforceable by admins | -| `includedResources` | ✅ Yes | | | -| `excludedResources` | ✅ Yes | | | -| `restoreStatus` | ✅ Yes | | | -| `includeClusterResources` | ✅ Yes | | | -| `labelSelector` | ✅ Yes | | | -| `orLabelSelectors` | ✅ Yes | | | -| `namespaceMapping` | ❌ No | ✅ Yes | ⚠️ restricted for non-admin users and hence non-enforceable by admins | -| `restorePVs` | ✅ Yes | | | -| `preserveNodePorts` | ✅ Yes | | | -| `existingResourcePolicy` | | | ⚠️ special case | -| `hooks` | | | ⚠️ special case | -| `resourceModifers` | | | ⚠️ Non-admin users can specify the config-map that admins created in OADP Operator NS(Admins enforcing this value be a good alternative here), they cannot specify their own configmap as its lifecycle handling is not currently managed by NAC controller | - - - -## OpenShift Console and OADP Self-Service - -At the time of writing OADP self-service objects can only be created in the OpenShift Console via the API Explorer. - -Navigate to: Administrator -> Home -> API Explorer -> Filter on `NonAdmin`. Choose the object you wish to create. - - * NonAdminBackup - * NonAdminRestore - * NonAdminBackupStorageLocation - * NonAdminDownloadRequest - -Click on instances and the create button. - - -## Unsupported features of OADP regarding self-service - * Cross Cluster or Migrations are NOT supported by self-service. This type of OADP operation is only supported for the cluster administrator. - * non-admin VSL's are not supported. The VSL created by the cluster-admin in DPA would be the only VSL non-admin users can employ. - * ResourceModifiers and Volume policies are not supported for non-admin user backup and restore operations. - * Backup and restore logs via NonAdminDownloadRequest is not supported for default BSL's. If the cluster administrator would - like users to have access to logs, NonAdminBackupStorageLocation's must be created for the non-admin users. - - -## Security Considerations for Cluster Administrators - - * By enabling self-service, cluster administrators will expose the name of the namespace where OADP is running via the backup logs. non-admin users are NOT granted any access to the OADP operator namespace. - * The nonadmin controller will not allow users to set includeClusterResources in a backup or restore. This is to prevent a scenario where a non-admin user would attempt to restore a cluster scoped resource to a namespace. OADP's backup policy is to automatically include cluster scoped resources like PV's that are associated with the namespace being backed up. Additionally cluster administrators can template an enforcement to `excludedClusterScopedResources` to prevent cluster scoped resources from being backed up. - ---- - -# CLI Implementation: MVP Approach - -## Overview - -The OADP CLI implements a Minimal Viable Product (MVP) approach for the `nonadmin backup create` and `nonadmin restore create` commands. This approach uses **struct embedding** from Velero's CreateOptions to reduce code duplication while maintaining clear control over which features are exposed to non-admin users. - -## Implementation Pattern: Struct Embedding - -Both commands follow the same pattern established by Velero's CLI: - -```go -type CreateOptions struct { - *velero.CreateOptions // Embed Velero's full feature set - - // NAB/NAR-specific fields - Name string - client kbclient.WithWatch - currentNamespace string -} -``` - -### Benefits - -- ✅ **Reduces Code Duplication** - No need to manually declare 30+ fields -- ✅ **Automatic Velero Compatibility** - Struct updates flow automatically -- ✅ **Clear Control Gate** - `BindFlags()` controls what's exposed to users -- ✅ **Easy Enhancement Path** - Add flags incrementally as features mature -- ✅ **Type Safety** - Reuses Velero's validated type definitions - -## Backup Create: MVP Flags (12 total) - -### Resource Filtering (2) -- `--include-resources` (default: `["*"]`) -- `--exclude-resources` - -### Label Selection (2) -- `--selector` / `-l` -- `--or-selector` - -### Cluster Resources (1) -- `--include-cluster-resources` (users can only set to false) - -### Timing & Storage (4) -- `--ttl` -- `--storage-location` -- `--csi-snapshot-timeout` -- `--item-operation-timeout` - -### Snapshot Control (3) -- `--snapshot-volumes` -- `--snapshot-move-data` -- `--default-volumes-to-fs-backup` - -### Flags Removed from Original Implementation - -**Restricted (API limitations):** -- `--include-namespaces` - Auto-managed by NAC -- `--exclude-namespaces` - Restricted for non-admin -- `--include-cluster-scoped-resources` - Only empty list acceptable -- `--volume-snapshot-locations` - Not supported - -**Not in MVP (future enhancements):** -- `--labels` / `--annotations` - Metadata -- `--from-schedule` - Requires schedule API -- `--ordered-resources` - Advanced feature -- `--data-mover` - Advanced feature -- `--resource-policies-configmap` - Advanced feature -- `--parallel-files-upload` - Performance tuning -- `--exclude-cluster-scoped-resources` - Advanced filtering -- `--include-namespace-scoped-resources` - Advanced filtering -- `--exclude-namespace-scoped-resources` - Advanced filtering - -## Restore Create: MVP Flags (7 total) - -### Core (3) -- `--backup-name` (required) -- `--include-resources` -- `--exclude-resources` - -### Label Selection (2) -- `--selector` / `-l` -- `--or-selector` - -### Cluster Resources (1) -- `--include-cluster-resources` - -### Timing (1) -- `--item-operation-timeout` - -## Documentation - -Comprehensive README files document the MVP approach: - -- **cmd/non-admin/backup/README.md** - Complete backup flag reference -- **cmd/non-admin/restore/README.md** - Complete restore flag reference - -Each README includes: -- MVP flags table with descriptions -- Restricted flags table with reasons -- Future enhancement flags table -- Examples -- Architecture notes - -## Migration from Previous Version - -Users previously using non-MVP flags will need to: - -1. Remove unsupported flags from backup/restore commands -2. For metadata (`--labels`/`--annotations`): Use kubectl to add after creation -3. For `--from-schedule`: Create from admin schedule spec directly - -## Alignment with NAB/NAR API - -The MVP flags directly correspond to the API restrictions tables shown above: - -| CLI Approach | API Field Status | -|--------------|------------------| -| Not exposed | Restricted | -| MVP flag | Admin enforceable or unrestricted | -| Future enhancement | Admin enforceable | - -This ensures the CLI accurately reflects the underlying API capabilities and restrictions. - - - - - - - diff --git a/go.mod b/go.mod index 7192a1f54..7b4f749f9 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,10 @@ go 1.25.8 require ( github.com/fatih/color v1.18.0 - github.com/migtools/oadp-non-admin v0.0.0-20260318101237-30487177ef60 - github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/vmware-tanzu/velero v1.14.1-rc.1 golang.org/x/sync v0.20.0 - gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.29.2 k8s.io/apimachinery v0.29.2 k8s.io/client-go v0.29.0 @@ -31,7 +28,6 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect - github.com/aws/aws-sdk-go v1.44.253 // indirect github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/config v1.26.3 // indirect @@ -52,7 +48,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect github.com/aws/smithy-go v1.24.2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bombsimon/logrusr/v3 v3.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chmduquesne/rollinghash v4.0.0+incompatible // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -61,6 +56,7 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/frankban/quicktest v1.14.3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -81,14 +77,12 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.3 // indirect - github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/cronexpr v1.1.2 // indirect github.com/hashicorp/go-hclog v1.2.0 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/joho/godotenv v1.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -107,23 +101,21 @@ require ( github.com/minio/minio-go/v7 v7.0.69 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect - github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/mxk/go-vss v1.2.0 // indirect github.com/natefinch/atomic v1.0.1 // indirect github.com/oklog/run v1.0.0 // indirect - github.com/openshift/oadp-operator v1.0.2-0.20250913003306-ab5b96fecb7d // indirect + github.com/onsi/gomega v1.34.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/robfig/cron v1.1.0 // indirect github.com/rs/xid v1.5.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/vladimirvivien/gexe v0.1.1 // indirect diff --git a/go.sum b/go.sum index 4c17284e2..9460a0696 100644 --- a/go.sum +++ b/go.sum @@ -87,11 +87,7 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.44.253 h1:iqDd0okcH4ShfFexz2zzf4VmeDFf6NOMm07pHnEb8iY= -github.com/aws/aws-sdk-go v1.44.253/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= @@ -137,8 +133,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/bombsimon/logrusr/v3 v3.0.0 h1:tcAoLfuAhKP9npBxWzSdpsvKPQt1XV02nSf2lZA82TQ= -github.com/bombsimon/logrusr/v3 v3.0.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -321,6 +315,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -368,8 +363,6 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -417,10 +410,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -455,6 +444,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -487,8 +477,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/migtools/oadp-non-admin v0.0.0-20260318101237-30487177ef60 h1:hGcGlJjj4jaUsLVvLOuN2qiWs4Zj1l1coFQcMH9gLis= -github.com/migtools/oadp-non-admin v0.0.0-20260318101237-30487177ef60/go.mod h1:vvFfZSIXq73z6wiw2tAVXYUp/j7bvh6RFXCtet8rHLA= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0= @@ -505,7 +493,6 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= @@ -521,7 +508,6 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/mxk/go-vss v1.2.0 h1:JpdOPc/P6B3XyRoddn0iMiG/ADBi3AuEsv8RlTb+JeE= github.com/mxk/go-vss v1.2.0/go.mod h1:ZQ4yFxCG54vqPnCd+p2IxAe5jwZdz56wSjbwzBXiFd8= @@ -547,8 +533,6 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/openshift/oadp-operator v1.0.2-0.20260618162355-c10a22a15a36 h1:5oFuhrMZkCuVcfhQuVEm3pjNkJ2bEV7+wG8klBoIGbw= -github.com/openshift/oadp-operator v1.0.2-0.20260618162355-c10a22a15a36/go.mod h1:M4QDpo/xCTL08mJeD13LumLMzxHupPO/qU0FcLFBP24= github.com/openshift/velero v0.10.2-0.20260526150244-ea5de9549ff4 h1:KMKRjGBRN6whQhhUq/iPNWocTizYB1tP4wloMrwMnlE= github.com/openshift/velero v0.10.2-0.20260526150244-ea5de9549ff4/go.mod h1:EhlV4JPcN4CKxZZuXr/ar32NCphXmXbRi7WSREuokQ8= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -589,11 +573,10 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/robfig/cron v1.1.0 h1:jk4/Hud3TTdcrJgUOBgsqrZBarcxl6ADIjSC2iniwLY= -github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= @@ -670,7 +653,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= @@ -738,7 +720,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -778,7 +759,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -824,8 +804,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -854,7 +832,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -915,9 +892,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -926,9 +901,7 @@ golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -940,7 +913,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1001,7 +973,6 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1156,7 +1127,6 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration_test.go b/integration_test.go index 093f21d88..94f7e0485 100644 --- a/integration_test.go +++ b/integration_test.go @@ -135,7 +135,7 @@ func TestCommandArchitecture(t *testing.T) { binaryPath := testutil.BuildCLIBinary(t) t.Run("all major commands exist", func(t *testing.T) { - majorCommands := []string{"backup", "restore", "nabsl-request", "nonadmin", "client", "version"} + majorCommands := []string{"backup", "restore", "schedule", "backup-location", "client", "version", "must-gather", "setup"} output, _ := testutil.RunCommand(t, binaryPath, "--help") @@ -146,14 +146,14 @@ func TestCommandArchitecture(t *testing.T) { } }) - t.Run("nabsl-request command has correct subcommands", func(t *testing.T) { - expectedSubcommands := []string{"approve", "reject", "describe", "get"} + t.Run("nonadmin and nabsl-request commands are not present", func(t *testing.T) { + removedCommands := []string{"nabsl-request", "nonadmin"} - output, _ := testutil.RunCommand(t, binaryPath, "nabsl-request", "--help") + output, _ := testutil.RunCommand(t, binaryPath, "--help") - for _, subcmd := range expectedSubcommands { - if !strings.Contains(output, subcmd) { - t.Errorf("Expected nabsl-request help to contain %q subcommand", subcmd) + for _, cmd := range removedCommands { + if strings.Contains(output, cmd) { + t.Errorf("Expected root help to NOT contain %q command on oadp-1.4 branch", cmd) } } })