From 12656abde1a1035cd253f2a935a94bb1b1703916 Mon Sep 17 00:00:00 2001 From: Nicholas Yancey Date: Tue, 1 Jul 2025 10:06:00 -0400 Subject: [PATCH 1/9] New Branch, describe and logs --- cmd/non-admin/backup/describe.go | 157 +++++++++++++++++++++++++++++++ cmd/non-admin/backup/logs.go | 58 ++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 cmd/non-admin/backup/describe.go create mode 100644 cmd/non-admin/backup/logs.go diff --git a/cmd/non-admin/backup/describe.go b/cmd/non-admin/backup/describe.go new file mode 100644 index 00000000..c21b2768 --- /dev/null +++ b/cmd/non-admin/backup/describe.go @@ -0,0 +1,157 @@ +package nonadmin + +import ( + "context" + "fmt" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/client" + "gopkg.in/yaml.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func NewDescribeCommand(f client.Factory, use string) *cobra.Command { + return &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] + veleroNamespace := "openshift-adp" + + dynClient, err := f.DynamicClient() + if err != nil { + return fmt.Errorf("failed to get dynamic client: %w", err) + } + + backupList, err := dynClient.Resource(velerov1.SchemeGroupVersion.WithResource("backups")).Namespace(veleroNamespace).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("failed to list Velero backups: %w", err) + } + + var found *velerov1.Backup + for _, item := range backupList.Items { + if item.GetName() == backupName || + item.GetAnnotations()["openshift.io/oadp-nab-origin-name"] == backupName { + var b velerov1.Backup + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.UnstructuredContent(), &b); err == nil { + found = &b + break + } + } + } + + if found == nil { + return fmt.Errorf("no Velero backup found for non-admin backup %s", backupName) + } + + // Print metadata + fmt.Fprintf(cmd.OutOrStdout(), "Name: %s\n", found.Name) + fmt.Fprintf(cmd.OutOrStdout(), "Namespace: %s\n", found.Namespace) + + // Print labels + fmt.Fprintf(cmd.OutOrStdout(), "Labels:\n") + labelKeys := make([]string, 0, len(found.Labels)) + for k := range found.Labels { + labelKeys = append(labelKeys, k) + } + sort.Strings(labelKeys) + for _, k := range labelKeys { + fmt.Fprintf(cmd.OutOrStdout(), " %s=%s\n", k, found.Labels[k]) + } + + // Print annotations + fmt.Fprintf(cmd.OutOrStdout(), "Annotations:\n") + annotationKeys := make([]string, 0, len(found.Annotations)) + for k := range found.Annotations { + annotationKeys = append(annotationKeys, k) + } + sort.Strings(annotationKeys) + for _, k := range annotationKeys { + fmt.Fprintf(cmd.OutOrStdout(), " %s=%s\n", k, found.Annotations[k]) + } + + // Print creation timestamp, etc. + fmt.Fprintf(cmd.OutOrStdout(), "Creation Timestamp: %s\n", found.CreationTimestamp.Format(time.RFC3339)) + fmt.Fprintf(cmd.OutOrStdout(), "Phase: %s\n", found.Status.Phase) + fmt.Fprintf(cmd.OutOrStdout(), "Start Timestamp: %s\n", formatTime(found.Status.StartTimestamp)) + fmt.Fprintf(cmd.OutOrStdout(), "Completion Timestamp:%s\n", formatTime(found.Status.CompletionTimestamp)) + fmt.Fprintf(cmd.OutOrStdout(), "Expiration: %s\n", formatTime(found.Status.Expiration)) + fmt.Fprintf(cmd.OutOrStdout(), "Format Version: %s\n", found.Status.FormatVersion) + fmt.Fprintf(cmd.OutOrStdout(), "Version: %d\n", found.Status.Version) + + // Print Spec (all fields, YAML for clarity) + specYaml, err := yaml.Marshal(found.Spec) + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Spec: \n", err) + } else { + // Remove the IncludedNamespaces line(s) from the YAML output + lines := strings.Split(string(specYaml), "\n") + var filtered []string + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if !strings.HasPrefix(trimmed, "includedNamespaces:") { + filtered = append(filtered, line) + } + } + fmt.Fprintf(cmd.OutOrStdout(), "Spec:\n%s", indent(strings.Join(filtered, "\n"), " ")) + } + + // Print Status (all fields, YAML for clarity) + statusYaml, err := yaml.Marshal(found.Status) + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Status: \n", err) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Status:\n%s", indent(string(statusYaml), " ")) + } + + // Print Events + fmt.Fprintf(cmd.OutOrStdout(), "Events:\n") + kubeClient, err := f.KubeClient() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), " \n", err) + } else { + eventsClient := kubeClient.CoreV1().Events(veleroNamespace) + eventList, err := eventsClient.List(context.TODO(), metav1.ListOptions{ + FieldSelector: fmt.Sprintf("involvedObject.kind=Backup,involvedObject.name=%s", found.Name), + }) + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), " \n", err) + } else if len(eventList.Items) == 0 { + fmt.Fprintf(cmd.OutOrStdout(), " \n") + } else { + for _, e := range eventList.Items { + fmt.Fprintf(cmd.OutOrStdout(), " %s: %s\n", e.Reason, e.Message) + } + } + } + + return nil + }, + Example: `oc oadp nonadmin backup describe my-backup`, + } +} + +// Helper to indent YAML blocks for pretty output +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") +} + +// Helper to format metav1.Time or nil +func formatTime(t *metav1.Time) string { + if t == nil || t.IsZero() { + return "" + } + return t.Time.Format(time.RFC3339) +} diff --git a/cmd/non-admin/backup/logs.go b/cmd/non-admin/backup/logs.go new file mode 100644 index 00000000..bd31fe1c --- /dev/null +++ b/cmd/non-admin/backup/logs.go @@ -0,0 +1,58 @@ +package nonadmin + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + "github.com/vmware-tanzu/velero/pkg/client" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewLogsCommand(f client.Factory, use string) *cobra.Command { + return &cobra.Command{ + Use: use + " NAME", + Short: "Show logs for a non-admin backup (from the Velero controller)", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + veleroNamespace := "openshift-adp" + + kubeClient, err := f.KubeClient() + if err != nil { + return fmt.Errorf("failed to get kube client: %w", err) + } + + podList, err := kubeClient.CoreV1().Pods(veleroNamespace).List(context.TODO(), metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=velero", + }) + if err != nil { + return fmt.Errorf("failed to list Velero controller pods: %w", err) + } + if len(podList.Items) == 0 { + return fmt.Errorf("no Velero controller pod found in namespace %s", veleroNamespace) + } + + // Print logs from the first Velero controller pod + pod := podList.Items[0] + fmt.Fprintf(cmd.OutOrStdout(), "Logs from Velero controller pod %s:\n", pod.Name) + req := kubeClient.CoreV1().Pods(veleroNamespace).GetLogs(pod.Name, &corev1.PodLogOptions{}) + logStream, err := req.Stream(context.TODO()) + if err != nil { + return fmt.Errorf("failed to stream logs: %w", err) + } + defer logStream.Close() + buf := new(strings.Builder) + _, err = io.Copy(buf, logStream) + if err != nil { + return fmt.Errorf("failed to read logs: %w", err) + } + fmt.Fprint(cmd.OutOrStdout(), buf.String()) + + return nil + }, + Example: ` oc oadp backup logs my-backup`, + } +} From 0dd09c940383e2aaddb943a67b996ad44da207d4 Mon Sep 17 00:00:00 2001 From: Nicholas Yancey Date: Tue, 1 Jul 2025 10:35:51 -0400 Subject: [PATCH 2/9] Changes so files work as intended --- cmd/non-admin/backup/backup.go | 4 ++-- cmd/non-admin/backup/describe.go | 2 +- cmd/non-admin/backup/logs.go | 2 +- go.mod | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/non-admin/backup/backup.go b/cmd/non-admin/backup/backup.go index 64f74be8..ce38fd19 100644 --- a/cmd/non-admin/backup/backup.go +++ b/cmd/non-admin/backup/backup.go @@ -33,8 +33,8 @@ func NewBackupCommand(f client.Factory) *cobra.Command { c.AddCommand( NewCreateCommand(f, "create"), // NewGetCommand(f, "get"), - // NewLogsCommand(f), - // NewDescribeCommand(f, "describe"), + NewLogsCommand(f, "logs"), + NewDescribeCommand(f, "describe"), // NewDownloadCommand(f), // NewDeleteCommand(f, "delete"), ) diff --git a/cmd/non-admin/backup/describe.go b/cmd/non-admin/backup/describe.go index c21b2768..77510344 100644 --- a/cmd/non-admin/backup/describe.go +++ b/cmd/non-admin/backup/describe.go @@ -1,4 +1,4 @@ -package nonadmin +package backup import ( "context" diff --git a/cmd/non-admin/backup/logs.go b/cmd/non-admin/backup/logs.go index bd31fe1c..dcbca6af 100644 --- a/cmd/non-admin/backup/logs.go +++ b/cmd/non-admin/backup/logs.go @@ -1,4 +1,4 @@ -package nonadmin +package backup import ( "context" diff --git a/go.mod b/go.mod index e098bdf9..80271e4a 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/vmware-tanzu/velero v1.16.1 + gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.33.1 k8s.io/apimachinery v0.33.1 k8s.io/client-go v0.33.1 sigs.k8s.io/controller-runtime v0.19.3 @@ -87,7 +89,6 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.1 // indirect k8s.io/apiextensions-apiserver v0.31.3 // indirect k8s.io/cli-runtime v0.31.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect From 6989271f0b597a00a9634499b58c59107441bd74 Mon Sep 17 00:00:00 2001 From: Nicholas Yancey Date: Tue, 1 Jul 2025 11:23:13 -0400 Subject: [PATCH 3/9] Changed describe.go to exclude includednamespaces --- cmd/non-admin/backup/describe.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/cmd/non-admin/backup/describe.go b/cmd/non-admin/backup/describe.go index 77510344..47b9db51 100644 --- a/cmd/non-admin/backup/describe.go +++ b/cmd/non-admin/backup/describe.go @@ -85,17 +85,31 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { fmt.Fprintf(cmd.OutOrStdout(), "Format Version: %s\n", found.Status.FormatVersion) fmt.Fprintf(cmd.OutOrStdout(), "Version: %d\n", found.Status.Version) - // Print Spec (all fields, YAML for clarity) + // Print Spec (all fields, YAML for clarity), removing includedNamespaces block specYaml, err := yaml.Marshal(found.Spec) if err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Spec: \n", err) } else { - // Remove the IncludedNamespaces line(s) from the YAML output lines := strings.Split(string(specYaml), "\n") var filtered []string - for _, line := range lines { + skip := false + for i := 0; i < len(lines); i++ { + line := lines[i] trimmed := strings.TrimSpace(line) - if !strings.HasPrefix(trimmed, "includedNamespaces:") { + if !skip && (strings.HasPrefix(trimmed, "includedNamespaces:") || strings.HasPrefix(trimmed, "includednamespaces:")) { + skip = true + continue + } + if skip { + // Skip all list items or indented lines after the key + if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") || trimmed == "" { + continue + } else { + // Found a new top-level key, stop skipping + skip = false + } + } + if !skip { filtered = append(filtered, line) } } From 09a4d566fa8f3a65c8d70ed2cb0844b79d79b8aa Mon Sep 17 00:00:00 2001 From: Nicholas Yancey Date: Thu, 3 Jul 2025 11:38:02 -0400 Subject: [PATCH 4/9] Design Doc Update --- kubectl-oadp-design.md | 75 +++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/kubectl-oadp-design.md b/kubectl-oadp-design.md index 654a1635..5f18eb9f 100644 --- a/kubectl-oadp-design.md +++ b/kubectl-oadp-design.md @@ -1,26 +1,79 @@ # 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. +The purpose of the kubectl-oadp plugin is to allow the customer to create and delete backups, along with creating restores in OADP without needing to alias velero to do so. Non-cluster admins should also be able to create NABs (Non-Admin Backups) and get the logs from them. ## 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. - +The current OpenShift cli is suboptimal as oc backup delete $foo deletes the k8s 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) +- Customers can create, delete, and get the logs of the backups and restores +- A non-cluster admin can create, delete and receive the logs of the 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. +## Non-Goals +- Non-Admin-Restore and other Non-Admin CRs due to time constraints +## Use-Case +A use case of the kubectl-oadp cli could be when a non-cluster admin would like to create a NAB or view the logs of a NAB without having to depend on the cluster admins to do so. Another use case would be if a developer would want to create a normal backup, they can just use this plugin to do so. + +## High-Level Design +Creating a kubectl plugin (kubectl-oadp) will be a good solution to the problem at hand. It will be able to create/delete backups and restores utilizing the go package imports. Non-cluster admin will be able to create NABs without the need for cluster admin to do it for them. A way to distinguish between creating either NABs or regular backups would be in the cli using the non-admin api. For instance, if you would like to create a NAB, you would have to do kubectl oadp nonadmin backup create [backupname]. ## 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. +What we discovered with the regular commands such as version, backup, and restore we can just import the libraries + +```go +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/vmware-tanzu/velero/pkg/cmd/cli/backup" + "github.com/vmware-tanzu/velero/pkg/cmd/cli/restore" + "github.com/vmware-tanzu/velero/pkg/cmd/cli/version" +) +``` +With non-admin, we would have to create the cli ourselves since there are no CLIs for it. + +```go +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.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)) + }, + +``` CLI Examples +```sh +kubectl oadp backup create +kubectl oadp backup delete +kubectl oadp nonadmin backup create +kubectl oadp nonadmin backup logs +kubectl oadp nonadmin backup describe +kubectl oadp restore create +``` + +## Alternatives Considered +An alternative that was considered was creating our own CLI from scratch and not using a plugin. We can instead use the existing oc commands and just add on to them with a kubectl plugin. + +Aliasing is another way in which you could access the velero command line. However, this is not ideal because non admins do not have permission to use the velero cli, so the kubectl plugin would allow non admins to retrieve their non admin backups similar to admins via velero cli. + +## Security Considerations +The security for the plugin is controlled by OpenShift RBAC, enabling cluster admins to manage user permissions. This is utilized to restrict users to commands permitted within their namespace. The plugin also generates error messages like "Unauthorized Access" when users attempt commands without proper permissions. + +## Compatibility +This plugin would need to be updated so that it would be importing the right version of the velero backup and restore libraries. -oc oadp backup create -oc oadp backup logs -oc oadp restore create -oc oadp restore logs +## Future Work +Some future work that could be expanded upon would be Non-admin Restores, and other Non-admin CRs such as NonAdminBackupStorageLocation. These would allow more options for those who would like to use different non admin commands. From 1ddeecc972d5d66a84fb26ec02680f95370e5ef6 Mon Sep 17 00:00:00 2001 From: Nicholas Yancey Date: Thu, 3 Jul 2025 15:35:03 -0400 Subject: [PATCH 5/9] Added new logs.go file --- cmd/non-admin/backup/logs.go | 117 ++++++++++++++++++++++++++++------- go.mod | 2 +- 2 files changed, 95 insertions(+), 24 deletions(-) diff --git a/cmd/non-admin/backup/logs.go b/cmd/non-admin/backup/logs.go index dcbca6af..b2ed00b2 100644 --- a/cmd/non-admin/backup/logs.go +++ b/cmd/non-admin/backup/logs.go @@ -1,58 +1,129 @@ package backup import ( + "bufio" + "compress/gzip" "context" "fmt" "io" - "strings" + "net/http" + "time" "github.com/spf13/cobra" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" ) func NewLogsCommand(f client.Factory, use string) *cobra.Command { return &cobra.Command{ Use: use + " NAME", - Short: "Show logs for a non-admin backup (from the Velero controller)", + Short: "Show logs for a non-admin backup", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + veleroNamespace := "openshift-adp" + backupName := args[0] - kubeClient, err := f.KubeClient() + scheme := runtime.NewScheme() + if err := velerov1.AddToScheme(scheme); err != nil { + return fmt.Errorf("failed to add Velero types to scheme: %w", err) + } + restConfig, err := f.ClientConfig() if err != nil { - return fmt.Errorf("failed to get kube client: %w", err) + return fmt.Errorf("failed to get rest config: %w", err) } - - podList, err := kubeClient.CoreV1().Pods(veleroNamespace).List(context.TODO(), metav1.ListOptions{ - LabelSelector: "app.kubernetes.io/name=velero", - }) + kbClient, err := kbclient.New(restConfig, kbclient.Options{Scheme: scheme}) if err != nil { - return fmt.Errorf("failed to list Velero controller pods: %w", err) + return fmt.Errorf("failed to create controller-runtime client: %w", err) + } + + req := &velerov1.DownloadRequest{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: backupName + "-logs-", + Namespace: veleroNamespace, + }, + Spec: velerov1.DownloadRequestSpec{ + Target: velerov1.DownloadTarget{ + Kind: "BackupLog", + Name: backupName, + }, + }, + } + + if err := kbClient.Create(ctx, req); err != nil { + return fmt.Errorf("failed to create DownloadRequest: %w", err) } - if len(podList.Items) == 0 { - return fmt.Errorf("no Velero controller pod found in namespace %s", veleroNamespace) + + defer func() { + deleteCtx, cancelDelete := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelDelete() + if err := kbClient.Delete(deleteCtx, req); err != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "Warning: Failed to delete DownloadRequest %s/%s: %v\n", req.Namespace, req.Name, err) + } + }() + + var signedURL string + timeout := time.After(60 * time.Second) + tick := time.Tick(1 * time.Second) + Loop: + for { + select { + case <-timeout: + return fmt.Errorf("timed out waiting for DownloadRequest to be processed") + case <-tick: + var updated velerov1.DownloadRequest + if err := kbClient.Get(ctx, kbclient.ObjectKey{ + Namespace: req.Namespace, + Name: req.Name, + }, &updated); err != nil { + return fmt.Errorf("failed to get DownloadRequest: %w", err) + } + + switch updated.Status.Phase { + case velerov1.DownloadRequestPhaseProcessed: + if updated.Status.DownloadURL != "" { + signedURL = updated.Status.DownloadURL + break Loop + } + case "Failed": + return fmt.Errorf("DownloadRequest failed: phase=%s", updated.Status.Phase) + default: + } + } } - // Print logs from the first Velero controller pod - pod := podList.Items[0] - fmt.Fprintf(cmd.OutOrStdout(), "Logs from Velero controller pod %s:\n", pod.Name) - req := kubeClient.CoreV1().Pods(veleroNamespace).GetLogs(pod.Name, &corev1.PodLogOptions{}) - logStream, err := req.Stream(context.TODO()) + resp, err := http.Get(signedURL) if err != nil { - return fmt.Errorf("failed to stream logs: %w", err) + return fmt.Errorf("failed to download logs from URL %q: %w", signedURL, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to download logs: status %s, body: %s", resp.Status, string(bodyBytes)) } - defer logStream.Close() - buf := new(strings.Builder) - _, err = io.Copy(buf, logStream) + + gzr, err := gzip.NewReader(resp.Body) if err != nil { + return fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gzr.Close() + + scanner := bufio.NewScanner(gzr) + for scanner.Scan() { + fmt.Fprintln(cmd.OutOrStdout(), scanner.Text()) + } + if err := scanner.Err(); err != nil && err != io.EOF { return fmt.Errorf("failed to read logs: %w", err) } - fmt.Fprint(cmd.OutOrStdout(), buf.String()) return nil }, - Example: ` oc oadp backup logs my-backup`, + Example: ` kubectl oadp backup logs my-backup`, } } diff --git a/go.mod b/go.mod index 80271e4a..a0594d76 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/spf13/pflag v1.0.6 github.com/vmware-tanzu/velero v1.16.1 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.33.1 k8s.io/apimachinery v0.33.1 k8s.io/client-go v0.33.1 sigs.k8s.io/controller-runtime v0.19.3 @@ -89,6 +88,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.33.1 // indirect k8s.io/apiextensions-apiserver v0.31.3 // indirect k8s.io/cli-runtime v0.31.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect From e37ca39dad08b532484179f78ca567a76b3803ad Mon Sep 17 00:00:00 2001 From: Nicholas Yancey Date: Mon, 7 Jul 2025 12:07:43 -0400 Subject: [PATCH 6/9] Major changes to logs.go file --- cmd/non-admin/backup/logs.go | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/cmd/non-admin/backup/logs.go b/cmd/non-admin/backup/logs.go index b2ed00b2..3a57f555 100644 --- a/cmd/non-admin/backup/logs.go +++ b/cmd/non-admin/backup/logs.go @@ -9,6 +9,7 @@ import ( "net/http" "time" + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" "github.com/spf13/cobra" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" @@ -26,10 +27,13 @@ func NewLogsCommand(f client.Factory, use string) *cobra.Command { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() - veleroNamespace := "openshift-adp" + userNamespace := f.Namespace() backupName := args[0] scheme := runtime.NewScheme() + if err := nacv1alpha1.AddToScheme(scheme); err != nil { + return fmt.Errorf("failed to add OADP non-admin types to scheme: %w", err) + } if err := velerov1.AddToScheme(scheme); err != nil { return fmt.Errorf("failed to add Velero types to scheme: %w", err) } @@ -42,12 +46,12 @@ func NewLogsCommand(f client.Factory, use string) *cobra.Command { return fmt.Errorf("failed to create controller-runtime client: %w", err) } - req := &velerov1.DownloadRequest{ + req := &nacv1alpha1.NonAdminDownloadRequest{ ObjectMeta: metav1.ObjectMeta{ GenerateName: backupName + "-logs-", - Namespace: veleroNamespace, + Namespace: userNamespace, }, - Spec: velerov1.DownloadRequestSpec{ + Spec: nacv1alpha1.NonAdminDownloadRequestSpec{ Target: velerov1.DownloadTarget{ Kind: "BackupLog", Name: backupName, @@ -56,15 +60,13 @@ func NewLogsCommand(f client.Factory, use string) *cobra.Command { } if err := kbClient.Create(ctx, req); err != nil { - return fmt.Errorf("failed to create DownloadRequest: %w", err) + return fmt.Errorf("failed to create NonAdminDownloadRequest: %w", err) } defer func() { deleteCtx, cancelDelete := context.WithTimeout(context.Background(), 5*time.Second) defer cancelDelete() - if err := kbClient.Delete(deleteCtx, req); err != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "Warning: Failed to delete DownloadRequest %s/%s: %v\n", req.Namespace, req.Name, err) - } + _ = kbClient.Delete(deleteCtx, req) }() var signedURL string @@ -74,24 +76,24 @@ func NewLogsCommand(f client.Factory, use string) *cobra.Command { for { select { case <-timeout: - return fmt.Errorf("timed out waiting for DownloadRequest to be processed") + return fmt.Errorf("timed out waiting for NonAdminDownloadRequest to be processed") case <-tick: - var updated velerov1.DownloadRequest + var updated nacv1alpha1.NonAdminDownloadRequest if err := kbClient.Get(ctx, kbclient.ObjectKey{ Namespace: req.Namespace, Name: req.Name, }, &updated); err != nil { - return fmt.Errorf("failed to get DownloadRequest: %w", err) + return fmt.Errorf("failed to get NonAdminDownloadRequest: %w", err) } switch updated.Status.Phase { - case velerov1.DownloadRequestPhaseProcessed: - if updated.Status.DownloadURL != "" { - signedURL = updated.Status.DownloadURL + case "Processed": + if updated.Status.VeleroDownloadRequest.Status.DownloadURL != "" { + signedURL = updated.Status.VeleroDownloadRequest.Status.DownloadURL break Loop } case "Failed": - return fmt.Errorf("DownloadRequest failed: phase=%s", updated.Status.Phase) + return fmt.Errorf("NonAdminDownloadRequest failed: phase=%s", updated.Status.Phase) default: } } From 4b9a854c21d41b8c8da3019158b45ec7dabe2839 Mon Sep 17 00:00:00 2001 From: Nicholas Yancey Date: Mon, 7 Jul 2025 15:21:15 -0400 Subject: [PATCH 7/9] Major changes to describe.go file --- cmd/non-admin/backup/describe.go | 369 ++++++++++++++++++++++--------- go.mod | 2 +- 2 files changed, 268 insertions(+), 103 deletions(-) diff --git a/cmd/non-admin/backup/describe.go b/cmd/non-admin/backup/describe.go index 47b9db51..ca431cd4 100644 --- a/cmd/non-admin/backup/describe.go +++ b/cmd/non-admin/backup/describe.go @@ -1,154 +1,319 @@ package backup import ( + "compress/gzip" "context" "fmt" + "io" + "net/http" "sort" "strings" "time" + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" "github.com/spf13/cobra" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "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" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" ) func NewDescribeCommand(f client.Factory, use string) *cobra.Command { - return &cobra.Command{ + 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] - veleroNamespace := "openshift-adp" + userNamespace := f.Namespace() - dynClient, err := f.DynamicClient() + // Setup scheme and client for NonAdminBackup resources + scheme := runtime.NewScheme() + if err := nacv1alpha1.AddToScheme(scheme); err != nil { + return fmt.Errorf("failed to add OADP non-admin types to scheme: %w", err) + } + if err := velerov1.AddToScheme(scheme); err != nil { + return fmt.Errorf("failed to add Velero types to scheme: %w", err) + } + + restConfig, err := f.ClientConfig() if err != nil { - return fmt.Errorf("failed to get dynamic client: %w", err) + return fmt.Errorf("failed to get rest config: %w", err) } - backupList, err := dynClient.Resource(velerov1.SchemeGroupVersion.WithResource("backups")).Namespace(veleroNamespace).List(context.TODO(), metav1.ListOptions{}) + kbClient, err := kbclient.New(restConfig, kbclient.Options{Scheme: scheme}) if err != nil { - return fmt.Errorf("failed to list Velero backups: %w", err) + return fmt.Errorf("failed to create controller-runtime client: %w", err) } - var found *velerov1.Backup - for _, item := range backupList.Items { - if item.GetName() == backupName || - item.GetAnnotations()["openshift.io/oadp-nab-origin-name"] == backupName { - var b velerov1.Backup - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.UnstructuredContent(), &b); err == nil { - found = &b - break - } + // List NonAdminBackup resources in user's namespace + var nabList nacv1alpha1.NonAdminBackupList + if err := kbClient.List(context.TODO(), &nabList, kbclient.InNamespace(userNamespace)); err != nil { + return fmt.Errorf("failed to list NonAdminBackup resources: %w", err) + } + + // Find the requested backup + var foundNAB *nacv1alpha1.NonAdminBackup + for i := range nabList.Items { + if nabList.Items[i].Name == backupName { + foundNAB = &nabList.Items[i] + break } } - if found == nil { - return fmt.Errorf("no Velero backup found for non-admin backup %s", backupName) + if foundNAB == nil { + return fmt.Errorf("NonAdminBackup %q not found in namespace %q", backupName, userNamespace) } - // Print metadata - fmt.Fprintf(cmd.OutOrStdout(), "Name: %s\n", found.Name) - fmt.Fprintf(cmd.OutOrStdout(), "Namespace: %s\n", found.Namespace) + // Use NonAdminDescribeBackup function to get comprehensive output + return NonAdminDescribeBackup(cmd, kbClient, foundNAB, userNamespace) + }, + Example: ` # Describe a non-admin backup with detailed information + oc oadp nonadmin backup describe my-backup`, + } - // Print labels - fmt.Fprintf(cmd.OutOrStdout(), "Labels:\n") - labelKeys := make([]string, 0, len(found.Labels)) - for k := range found.Labels { - labelKeys = append(labelKeys, k) - } - sort.Strings(labelKeys) - for _, k := range labelKeys { - fmt.Fprintf(cmd.OutOrStdout(), " %s=%s\n", k, found.Labels[k]) - } + // Add output formatting flags for future enhancement + output.BindFlags(c.Flags()) + output.ClearOutputFlagDefault(c) - // Print annotations - fmt.Fprintf(cmd.OutOrStdout(), "Annotations:\n") - annotationKeys := make([]string, 0, len(found.Annotations)) - for k := range found.Annotations { - annotationKeys = append(annotationKeys, k) - } - sort.Strings(annotationKeys) - for _, k := range annotationKeys { - fmt.Fprintf(cmd.OutOrStdout(), " %s=%s\n", k, found.Annotations[k]) - } + return c +} - // Print creation timestamp, etc. - fmt.Fprintf(cmd.OutOrStdout(), "Creation Timestamp: %s\n", found.CreationTimestamp.Format(time.RFC3339)) - fmt.Fprintf(cmd.OutOrStdout(), "Phase: %s\n", found.Status.Phase) - fmt.Fprintf(cmd.OutOrStdout(), "Start Timestamp: %s\n", formatTime(found.Status.StartTimestamp)) - fmt.Fprintf(cmd.OutOrStdout(), "Completion Timestamp:%s\n", formatTime(found.Status.CompletionTimestamp)) - fmt.Fprintf(cmd.OutOrStdout(), "Expiration: %s\n", formatTime(found.Status.Expiration)) - fmt.Fprintf(cmd.OutOrStdout(), "Format Version: %s\n", found.Status.FormatVersion) - fmt.Fprintf(cmd.OutOrStdout(), "Version: %d\n", found.Status.Version) - - // Print Spec (all fields, YAML for clarity), removing includedNamespaces block - specYaml, err := yaml.Marshal(found.Spec) - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), "Spec: \n", err) - } else { - lines := strings.Split(string(specYaml), "\n") - var filtered []string - skip := false - for i := 0; i < len(lines); i++ { - line := lines[i] - trimmed := strings.TrimSpace(line) - if !skip && (strings.HasPrefix(trimmed, "includedNamespaces:") || strings.HasPrefix(trimmed, "includednamespaces:")) { - skip = true +// NonAdminDescribeBackup mirrors Velero's output.DescribeBackup functionality +// but works within non-admin RBAC boundaries using NonAdminDownloadRequest +func NonAdminDescribeBackup(cmd *cobra.Command, kbClient kbclient.Client, nab *nacv1alpha1.NonAdminBackup, userNamespace string) error { + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + 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 + + // Get backup details using NonAdminDownloadRequest for BackupResourceList + if resourceList, err := downloadBackupData(ctx, kbClient, userNamespace, veleroBackupName, "BackupResourceList"); 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 := downloadBackupData(ctx, kbClient, userNamespace, veleroBackupName, "BackupVolumeInfos"); 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 := downloadBackupData(ctx, kbClient, userNamespace, veleroBackupName, "BackupItemOperations"); err == nil { + fmt.Fprintf(cmd.OutOrStdout(), "\nBackup Item Operations:\n") + fmt.Fprintf(cmd.OutOrStdout(), "%s", indent(itemOps, " ")) + } + + // Get backup results using NonAdminDownloadRequest + if results, err := downloadBackupData(ctx, kbClient, userNamespace, veleroBackupName, "BackupResults"); err == nil { + fmt.Fprintf(cmd.OutOrStdout(), "\nBackup Results:\n") + fmt.Fprintf(cmd.OutOrStdout(), "%s", indent(results, " ")) + } + } + + // 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 { + lines := strings.Split(string(specYaml), "\n") + var filtered []string + skip := false + for i := 0; i < len(lines); i++ { + line := lines[i] + trimmed := strings.TrimSpace(line) + if !skip && (strings.HasPrefix(trimmed, "includedNamespaces:") || strings.HasPrefix(trimmed, "includednamespaces:")) { + skip = true + continue + } + if skip { + // Skip all list items or indented lines after the key + if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") || trimmed == "" { continue - } - if skip { - // Skip all list items or indented lines after the key - if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") || trimmed == "" { - continue - } else { - // Found a new top-level key, stop skipping - skip = false - } - } - if !skip { - filtered = append(filtered, line) + } else { + // Found a new top-level key, stop skipping + skip = false } } - fmt.Fprintf(cmd.OutOrStdout(), "Spec:\n%s", indent(strings.Join(filtered, "\n"), " ")) + if !skip { + filtered = append(filtered, line) + } } + fmt.Fprintf(cmd.OutOrStdout(), "\nSpec:\n%s", indent(strings.Join(filtered, "\n"), " ")) + } + } - // Print Status (all fields, YAML for clarity) - statusYaml, err := yaml.Marshal(found.Status) - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), "Status: \n", err) - } else { - fmt.Fprintf(cmd.OutOrStdout(), "Status:\n%s", indent(string(statusYaml), " ")) + // Print NonAdminBackup Status + statusYaml, err := yaml.Marshal(nab.Status) + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "\nStatus: \n", err) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "\nStatus:\n%s", indent(string(statusYaml), " ")) + } + + // 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) } + } - // Print Events - fmt.Fprintf(cmd.OutOrStdout(), "Events:\n") - kubeClient, err := f.KubeClient() - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), " \n", err) - } else { - eventsClient := kubeClient.CoreV1().Events(veleroNamespace) - eventList, err := eventsClient.List(context.TODO(), metav1.ListOptions{ - FieldSelector: fmt.Sprintf("involvedObject.kind=Backup,involvedObject.name=%s", found.Name), - }) - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), " \n", err) - } else if len(eventList.Items) == 0 { - fmt.Fprintf(cmd.OutOrStdout(), " \n") - } else { - for _, e := range eventList.Items { - fmt.Fprintf(cmd.OutOrStdout(), " %s: %s\n", e.Reason, e.Message) - } - } + 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 + return nil +} + +// downloadBackupData uses NonAdminDownloadRequest to fetch detailed backup information +// This replaces direct access to Velero backups with RBAC-compliant requests +func downloadBackupData(ctx context.Context, kbClient kbclient.Client, userNamespace, backupName, dataType string) (string, error) { + // Create NonAdminDownloadRequest for the specified data type + req := &nacv1alpha1.NonAdminDownloadRequest{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: backupName + "-" + strings.ToLower(dataType) + "-", + Namespace: userNamespace, + }, + Spec: nacv1alpha1.NonAdminDownloadRequestSpec{ + Target: velerov1.DownloadTarget{ + Kind: velerov1.DownloadTargetKind(dataType), + Name: backupName, + }, }, - Example: `oc oadp nonadmin backup describe my-backup`, } + + if err := kbClient.Create(ctx, req); err != nil { + return "", fmt.Errorf("failed to create NonAdminDownloadRequest for %s: %w", dataType, 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) + }() + + // Wait for the download request to be processed + timeout := time.After(30 * time.Second) + tick := time.Tick(1 * time.Second) + + for { + select { + case <-timeout: + return "", fmt.Errorf("timed out waiting for %s download request to be processed", dataType) + case <-tick: + var updated nacv1alpha1.NonAdminDownloadRequest + if err := kbClient.Get(ctx, kbclient.ObjectKey{ + Namespace: req.Namespace, + Name: req.Name, + }, &updated); err != nil { + return "", fmt.Errorf("failed to get NonAdminDownloadRequest: %w", err) + } + + switch updated.Status.Phase { + case "Processed": + if updated.Status.VeleroDownloadRequest.Status.DownloadURL != "" { + // Download and return the content + return downloadContent(updated.Status.VeleroDownloadRequest.Status.DownloadURL) + } + case "Failed": + return "", fmt.Errorf("NonAdminDownloadRequest failed for %s: phase=%s", dataType, updated.Status.Phase) + default: + // Continue waiting + } + } + } +} + +// downloadContent fetches content from a signed URL and returns it as a string +func downloadContent(url string) (string, error) { + resp, err := http.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)) + } + + // Try to decompress if it's gzipped + var reader io.Reader = resp.Body + if strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") { + gzr, err := gzip.NewReader(resp.Body) + 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 } // Helper to indent YAML blocks for pretty output diff --git a/go.mod b/go.mod index a0594d76..80271e4a 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/spf13/pflag v1.0.6 github.com/vmware-tanzu/velero v1.16.1 gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.33.1 k8s.io/apimachinery v0.33.1 k8s.io/client-go v0.33.1 sigs.k8s.io/controller-runtime v0.19.3 @@ -88,7 +89,6 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.1 // indirect k8s.io/apiextensions-apiserver v0.31.3 // indirect k8s.io/cli-runtime v0.31.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect From 2ba9c6463c0d660b18b7e8324901ec1b9883f151 Mon Sep 17 00:00:00 2001 From: Nicholas Yancey Date: Tue, 8 Jul 2025 10:56:14 -0400 Subject: [PATCH 8/9] Resolving Conflicts --- cmd/non-admin/backup/backup.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/non-admin/backup/backup.go b/cmd/non-admin/backup/backup.go index 29c1a757..38f00ca9 100644 --- a/cmd/non-admin/backup/backup.go +++ b/cmd/non-admin/backup/backup.go @@ -32,7 +32,6 @@ func NewBackupCommand(f client.Factory) *cobra.Command { c.AddCommand( NewCreateCommand(f, "create"), - NewLogsCommand(f, "logs"), NewDescribeCommand(f, "describe"), NewDeleteCommand(f, "delete"), From c6b1ab5152789d495da6bd6ea94f8a7f2f186787 Mon Sep 17 00:00:00 2001 From: Nicholas Yancey Date: Tue, 8 Jul 2025 12:16:31 -0400 Subject: [PATCH 9/9] Resolving Conflicts 2 --- cmd/non-admin/backup/describe.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cmd/non-admin/backup/describe.go b/cmd/non-admin/backup/describe.go index 3a56d897..38c26bc4 100644 --- a/cmd/non-admin/backup/describe.go +++ b/cmd/non-admin/backup/describe.go @@ -313,7 +313,7 @@ func downloadContent(url string) (string, error) { return string(content), nil } -// Helper to indent YAML blocks for pretty output +// Helper to indent YAML blocks func indent(s, prefix string) string { lines := strings.Split(s, "\n") for i, line := range lines { @@ -323,11 +323,3 @@ func indent(s, prefix string) string { } return strings.Join(lines, "\n") } - -// Helper to format metav1.Time or nil -func formatTime(t *metav1.Time) string { - if t == nil || t.IsZero() { - return "" - } - return t.Time.Format(time.RFC3339) -}