diff --git a/.gitignore b/.gitignore index e69de29b..270d6000 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,34 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Built binaries +kubectl-oadp + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..f7f4f401 --- /dev/null +++ b/Makefile @@ -0,0 +1,88 @@ +# Makefile for OADP CLI +# +# Simple Makefile for building, testing, and installing the OADP CLI + +# Variables +BINARY_NAME = kubectl-oadp +INSTALL_PATH ?= /usr/local/bin + +# Platform variables for multi-arch builds +# Usage: make build PLATFORM=linux/amd64 +PLATFORM ?= +GOOS = $(word 1,$(subst /, ,$(PLATFORM))) +GOARCH = $(word 2,$(subst /, ,$(PLATFORM))) + +# Default target +.PHONY: help +help: ## Show this help message + @echo "OADP CLI Makefile" + @echo "" + @echo "Available targets:" + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @echo "" + @echo "Build with different platforms:" + @echo " make build PLATFORM=linux/amd64" + @echo " make build PLATFORM=linux/arm64" + @echo " make build PLATFORM=darwin/amd64" + @echo " make build PLATFORM=darwin/arm64" + @echo " make build PLATFORM=windows/amd64" + +# Build targets +.PHONY: build +build: ## Build the kubectl plugin binary (use PLATFORM=os/arch for cross-compilation) + @if [ -n "$(PLATFORM)" ]; then \ + echo "Building $(BINARY_NAME) for $(PLATFORM)..."; \ + GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(BINARY_NAME)-$(GOOS)-$(GOARCH) .; \ + echo "✅ Built $(BINARY_NAME)-$(GOOS)-$(GOARCH) successfully!"; \ + else \ + echo "Building $(BINARY_NAME) for current platform ($$(go env GOOS)/$$(go env GOARCH))..."; \ + go build -o $(BINARY_NAME) .; \ + echo "✅ Built $(BINARY_NAME) successfully!"; \ + fi + +# Installation targets +.PHONY: install +install: build ## Build and install the kubectl plugin + @echo "Installing $(BINARY_NAME) to $(INSTALL_PATH)..." + mv $(BINARY_NAME) $(INSTALL_PATH)/ + @echo "✅ $(BINARY_NAME) installed successfully!" + @echo "You can now use: kubectl oadp --help" + +# Testing targets +.PHONY: test +test: ## Run all tests + @echo "Running tests..." + go test ./... + @echo "✅ Tests completed!" + +# Cleanup targets +.PHONY: clean +clean: ## Remove built binaries + @echo "Cleaning up..." + @rm -f $(BINARY_NAME) $(BINARY_NAME)-* + @echo "✅ Cleanup complete!" + +# Status and utility targets +.PHONY: status +status: ## Show build status and installation info + @echo "=== OADP CLI Status ===" + @echo "" + @echo "📁 Repository:" + @pwd + @echo "" + @echo "🔧 Local binary:" + @ls -la $(BINARY_NAME) 2>/dev/null || echo " No local binary found" + @echo "" + @echo "📦 Installed plugin:" + @ls -la $(INSTALL_PATH)/$(BINARY_NAME) 2>/dev/null || echo " Plugin not installed" + @echo "" + @echo "✅ Plugin accessibility:" + @if kubectl plugin list 2>/dev/null | grep -q "kubectl-oadp"; then \ + echo " ✅ kubectl-oadp plugin is installed and accessible"; \ + echo " Version check:"; \ + kubectl oadp version 2>/dev/null || echo " (version command not available)"; \ + else \ + echo " ❌ kubectl-oadp plugin is NOT accessible"; \ + echo " Available plugins:"; \ + kubectl plugin list 2>/dev/null | head -5 || echo " (no plugins found or kubectl not available)"; \ + fi diff --git a/README.md b/README.md index e9c71fb9..1d7c9cc3 100644 --- a/README.md +++ b/README.md @@ -27,18 +27,18 @@ oadp ### Quick Installation -Use the provided script for quick build and installation: +Use the Makefile for easy build and installation: ```sh -chmod +x quick-create.sh -./quick-create.sh +# Build and install the kubectl plugin +make install ``` ### Manual Installation 1. **Build the CLI:** ```sh - go build -o kubectl-oadp . + make build ``` 2. **Install as kubectl plugin:** @@ -51,6 +51,23 @@ chmod +x quick-create.sh kubectl oadp --help ``` +### Development Workflow + +```sh +# Build and test locally +make build +./kubectl-oadp --help + +# Run tests +make test + +# Check status +make status + +# View all available commands +make help +``` + ## Usage Examples ### NonAdminBackup Operations @@ -99,7 +116,10 @@ This project includes comprehensive CLI integration tests organized by functiona ### Quick Test Commands ```bash -# Run all tests (standard Go pattern) +# Run all tests +make test + +# Standard Go pattern (also works) go test ./... ``` diff --git a/cmd/non-admin/backup/backup.go b/cmd/non-admin/backup/backup.go index 64f74be8..62992293 100644 --- a/cmd/non-admin/backup/backup.go +++ b/cmd/non-admin/backup/backup.go @@ -32,11 +32,7 @@ func NewBackupCommand(f client.Factory) *cobra.Command { c.AddCommand( NewCreateCommand(f, "create"), - // NewGetCommand(f, "get"), - // NewLogsCommand(f), - // NewDescribeCommand(f, "describe"), - // NewDownloadCommand(f), - // NewDeleteCommand(f, "delete"), + NewDeleteCommand(f, "delete"), ) return c diff --git a/cmd/non-admin/backup/create.go b/cmd/non-admin/backup/create.go index 9c38a434..e5965b45 100644 --- a/cmd/non-admin/backup/create.go +++ b/cmd/non-admin/backup/create.go @@ -35,7 +35,6 @@ import ( "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" "github.com/vmware-tanzu/velero/pkg/util/kube" - "k8s.io/client-go/tools/clientcmd" ) func NewCreateCommand(f client.Factory, use string) *cobra.Command { @@ -206,40 +205,6 @@ func (o *CreateOptions) validateFromScheduleFlag(c *cobra.Command) error { return nil } -// getCurrentNamespace gets the current namespace from the kubeconfig context -func getCurrentNamespace() (string, error) { - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - configOverrides := &clientcmd.ConfigOverrides{} - kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) - - namespace, _, err := kubeConfig.Namespace() - if err != nil { - return "", fmt.Errorf("failed to get current namespace from kubeconfig: %w", err) - } - - // If no namespace is set in kubeconfig, default to the user's name from context - if namespace == "" || namespace == "default" { - rawConfig, err := kubeConfig.RawConfig() - if err != nil { - return "", fmt.Errorf("failed to get raw kubeconfig: %w", err) - } - - currentContext := rawConfig.CurrentContext - if _, exists := rawConfig.Contexts[currentContext]; exists { - // Try to extract user namespace from context name (assuming format like "user/cluster/user") - parts := strings.Split(currentContext, "/") - if len(parts) >= 3 { - userNamespace := parts[2] // Assuming the user namespace is the third part - return userNamespace, nil - } - } - - return "default", nil - } - - return namespace, nil -} - func (o *CreateOptions) Complete(args []string, f client.Factory) error { // If an explicit name is specified, use that name if len(args) > 0 { diff --git a/cmd/non-admin/backup/delete.go b/cmd/non-admin/backup/delete.go new file mode 100644 index 00000000..c4f22e8b --- /dev/null +++ b/cmd/non-admin/backup/delete.go @@ -0,0 +1,264 @@ +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" + + 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 [NAME...]", + Short: "Delete one or more non-admin backups", + Long: "Delete one or more non-admin backups by setting the deletebackup field to true", + Args: cobra.MinimumNArgs(1), + Run: func(c *cobra.Command, args []string) { + cmd.CheckError(o.Complete(args, f)) + cmd.CheckError(o.Validate()) + cmd.CheckError(o.Run()) + }, + } + + 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 + 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") +} + +// 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 + + // Get the Kubernetes client + kbClient, err := f.KubebuilderWatchClient() + if err != nil { + return err + } + + // Add NonAdminBackup types to the scheme + err = nacv1alpha1.AddToScheme(kbClient.Scheme()) + if err != nil { + return fmt.Errorf("failed to add NonAdminBackup types to scheme: %w", err) + } + + o.client = kbClient + + // Always use the current namespace from kubectl context + currentNS, err := getCurrentNamespace() + if err != nil { + return fmt.Errorf("failed to determine current namespace: %w", err) + } + o.Namespace = currentNS + + return nil +} + +// Validate validates the options +func (o *DeleteOptions) Validate() error { + if len(o.Names) == 0 { + return fmt.Errorf("at least one backup name is required") + } + 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 + 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(" kubectl 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 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/nonadminbackup_builder.go b/cmd/non-admin/backup/nonadminbackup_builder.go index 03e37cd6..4f977a77 100644 --- a/cmd/non-admin/backup/nonadminbackup_builder.go +++ b/cmd/non-admin/backup/nonadminbackup_builder.go @@ -17,7 +17,11 @@ limitations under the License. package backup import ( + "fmt" + "strings" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" ) @@ -162,3 +166,37 @@ func WithAnnotationsMap(annotations map[string]string) ObjectMetaOpt { obj.SetAnnotations(existingAnnotations) } } + +// getCurrentNamespace gets the current namespace from the kubeconfig context +func getCurrentNamespace() (string, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + + namespace, _, err := kubeConfig.Namespace() + if err != nil { + return "", fmt.Errorf("failed to get current namespace from kubeconfig: %w", err) + } + + // If no namespace is set in kubeconfig, default to the user's name from context + if namespace == "" || namespace == "default" { + rawConfig, err := kubeConfig.RawConfig() + if err != nil { + return "", fmt.Errorf("failed to get raw kubeconfig: %w", err) + } + + currentContext := rawConfig.CurrentContext + if _, exists := rawConfig.Contexts[currentContext]; exists { + // Try to extract user namespace from context name (assuming format like "user/cluster/user") + parts := strings.Split(currentContext, "/") + if len(parts) >= 3 { + userNamespace := parts[2] // Assuming the user namespace is the third part + return userNamespace, nil + } + } + + return "default", nil + } + + return namespace, nil +} diff --git a/kubectl-oadp-design.md b/kubectl-oadp-design.md deleted file mode 100644 index 654a1635..00000000 --- a/kubectl-oadp-design.md +++ /dev/null @@ -1,26 +0,0 @@ -# Kubectl-oadp plugin design - -## Abstract -The purpose of this plugin is to allow the customer to delete backups, along with creating restores in OADP without needing to alias velero to do so. - -## Background -The current OADP cli is suboptimal as oc backup delete $foo deletes the k8 object instead of the backup but velero backup delete $foo deletes the backup, along with the backup files in storage. Currently, customers would need to alias velero in order to delete their backups, which is not ideal. The purpose of kubectl-oadp would be to make the cli experience better and easier to use along with enabling users to be able to get the logs of the backups. - - -## Goals -- Customers can create backups and restores -- A non-cluster admin can create Non-Admin-Backups (NAB) - -## High-Level Design -Creating a kubectl plugin (kubctl-oadp) will be a good solution to the problem at hand. It will be able to create/delete backups and restores. Non-cluster admin will be able to create NABs without the need for cluster admin to do it for them. - - -## Detailed Design -The kubectl plugin will have imports from velero to help with the creation/deletion of backups and restores. It will be written in Golang and be using cobra for command-line parsing. The non-admin cli can be a subset of some backup clis that already exist such as backup.go and create.go. - -CLI Examples - -oc oadp backup create -oc oadp backup logs -oc oadp restore create -oc oadp restore logs diff --git a/quick-create.sh b/quick-create.sh deleted file mode 100755 index fa47b642..00000000 --- a/quick-create.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# Build the kubectl-oadp plugin -go build -o kubectl-oadp . - -# Move to system binary location -sudo mv kubectl-oadp /usr/local/bin/ - -echo "kubectl-oadp plugin installed successfully!" -echo "You can now use: kubectl oadp --help"