Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,17 @@ install: build ## Build and install the kubectl plugin to ~/.local/bin (no sudo
echo " ├─ Temporarily updating PATH for verification"; \
if PATH="$(INSTALL_PATH):$$PATH" command -v kubectl >/dev/null 2>&1; then \
if PATH="$(INSTALL_PATH):$$PATH" kubectl plugin list 2>/dev/null | grep -q "kubectl-oadp"; then \
echo " ├─ ✅ Installation verified: kubectl oadp plugin is accessible"; \
PATH="$(INSTALL_PATH):$$PATH" kubectl oadp version 2>/dev/null || echo " │ └─ (Note: version command requires cluster access)"; \
echo " └─ ✅ Installation verified: kubectl oadp plugin is accessible"; \
echo ""; \
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; \
echo "🎉 Installation complete!"; \
echo ""; \
echo " ⚠️ To use in this terminal session, run:"; \
echo " export PATH=\"$(INSTALL_PATH):$$PATH\""; \
echo ""; \
echo " Quick start:"; \
echo " • kubectl oadp --help # Show available commands"; \
echo " • kubectl oadp backup get # List backups"; \
else \
echo " ├─ ❌ Installation verification failed: kubectl oadp plugin not found"; \
echo " │ └─ Try running: export PATH=\"$(INSTALL_PATH):$$PATH\""; \
Expand All @@ -242,9 +251,22 @@ install: build ## Build and install the kubectl plugin to ~/.local/bin (no sudo
if command -v kubectl >/dev/null 2>&1; then \
if kubectl plugin list 2>/dev/null | grep -q "kubectl-oadp"; then \
echo " ├─ ✅ Installation verified: kubectl oadp plugin is accessible"; \
echo " ├─ Running version command..."; \
echo " └─ Running version command..."; \
echo ""; \
version_output=$$(kubectl oadp version 2>&1 | grep -v "WARNING: the client version does not match"); \
if [ $$? -eq 0 ] && [ -n "$$version_output" ]; then \
echo "$$version_output" | sed 's/^/ /'; \
else \
echo " (Note: version command requires cluster access)"; \
fi; \
echo ""; \
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; \
echo "🎉 Installation complete!"; \
echo ""; \
kubectl oadp version 2>/dev/null || echo " │ └─ (Note: version command requires cluster access)"; \
echo " Quick start:"; \
echo " • kubectl oadp --help # Show available commands"; \
echo " • kubectl oadp backup get # List backups"; \
echo " • kubectl oadp version # Show version info"; \
else \
echo " └─ ❌ Installation verification failed: kubectl oadp plugin not found"; \
fi; \
Expand Down
246 changes: 242 additions & 4 deletions cmd/non-admin/backup/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package backup

import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
Expand All @@ -18,7 +19,10 @@ import (
)

func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
var requestTimeout time.Duration
var (
requestTimeout time.Duration
details bool
)

c := &cobra.Command{
Use: use + " NAME",
Expand Down Expand Up @@ -68,15 +72,24 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
}

// Print in Velero-style format
printNonAdminBackupDetails(cmd, &nab)
printNonAdminBackupDetails(cmd, &nab, kbClient, backupName, userNamespace, effectiveTimeout)

// 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: ` kubectl oadp nonadmin backup describe my-backup
kubectl oadp nonadmin backup describe my-backup --request-timeout=30m`,
kubectl oadp nonadmin backup describe my-backup --details
kubectl 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)
Expand All @@ -85,7 +98,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
}

// printNonAdminBackupDetails prints backup details in Velero admin describe format
func printNonAdminBackupDetails(cmd *cobra.Command, nab *nacv1alpha1.NonAdminBackup) {
func printNonAdminBackupDetails(cmd *cobra.Command, nab *nacv1alpha1.NonAdminBackup, kbClient kbclient.Client, backupName string, userNamespace string, timeout time.Duration) {
out := cmd.OutOrStdout()

// Get Velero backup reference if available
Expand Down Expand Up @@ -309,6 +322,25 @@ func printNonAdminBackupDetails(cmd *cobra.Command, nab *nacv1alpha1.NonAdminBac

fmt.Fprintf(out, "\n")

// Fetch and display Resource List
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")

Expand Down Expand Up @@ -347,6 +379,212 @@ func printNonAdminBackupDetails(cmd *cobra.Command, nab *nacv1alpha1.NonAdminBac
}
}

// 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 "<none>")
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]
output.WriteString(fmt.Sprintf(" %s:\n", gvk))
for _, item := range items {
output.WriteString(fmt.Sprintf(" - %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(" <none>")
}
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(" <none>")
}

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 "<none>")
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 (
Expand Down
Loading
Loading