Skip to content

Commit faac019

Browse files
Merge pull request #198 from NicholasYancey/prompt-multi-nabsl
Prompts user to choose when more than one NABSL exists
2 parents e02116a + aaa30cb commit faac019

2 files changed

Lines changed: 107 additions & 30 deletions

File tree

cmd/non-admin/backup/create.go

Lines changed: 106 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ limitations under the License.
1717
*/
1818

1919
import (
20+
"bufio"
2021
"context"
2122
"fmt"
23+
"io"
24+
"os"
25+
"strconv"
26+
"strings"
2227

2328
"github.com/spf13/cobra"
2429
"github.com/spf13/pflag"
@@ -32,6 +37,7 @@ import (
3237
"github.com/vmware-tanzu/velero/pkg/cmd"
3338
velerobackup "github.com/vmware-tanzu/velero/pkg/cmd/cli/backup"
3439
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
40+
"golang.org/x/term"
3541
)
3642

3743
func NewCreateCommand(f client.Factory, use string) *cobra.Command {
@@ -84,6 +90,7 @@ type CreateOptions struct {
8490
currentNamespace string
8591
storageLocationFromConfig bool // Track if storage location came from config
8692
storageLocationAutoSelected bool // Track if storage location was auto-selected
93+
storageLocationPrompted bool // Track if storage location was chosen interactively
8794
}
8895

8996
func NewCreateOptions() *CreateOptions {
@@ -165,34 +172,8 @@ func (o *CreateOptions) Complete(args []string, f client.Factory) error {
165172
o.client = client
166173
o.currentNamespace = currentNS
167174

168-
// Load default NABSL from config if not provided via flag, or auto-select if exactly one exists
169-
if o.StorageLocation == "" {
170-
defaultNABSL := getNABSLFromConfig()
171-
if defaultNABSL != "" {
172-
o.StorageLocation = defaultNABSL
173-
o.storageLocationFromConfig = true
174-
} else {
175-
// Auto-select NABSL if exactly one approved/created exists in the namespace
176-
nabslList := &nacv1alpha1.NonAdminBackupStorageLocationList{}
177-
if err := o.client.List(context.TODO(), nabslList, &kbclient.ListOptions{
178-
Namespace: currentNS,
179-
}); err != nil {
180-
return fmt.Errorf("failed to list NonAdminBackupStorageLocations: %w", err)
181-
}
182-
183-
// Filter to only approved/created NABSLs (exclude pending/rejected)
184-
var usableNABSLs []nacv1alpha1.NonAdminBackupStorageLocation
185-
for _, nabsl := range nabslList.Items {
186-
if nabsl.Status.Phase == nacv1alpha1.NonAdminPhaseCreated {
187-
usableNABSLs = append(usableNABSLs, nabsl)
188-
}
189-
}
190-
191-
if len(usableNABSLs) == 1 {
192-
o.StorageLocation = usableNABSLs[0].Name
193-
o.storageLocationAutoSelected = true
194-
}
195-
}
175+
if err := o.resolveStorageLocation(currentNS); err != nil {
176+
return err
196177
}
197178

198179
return nil
@@ -217,9 +198,12 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
217198
fmt.Printf("Using default nonadmin backup storage location from config: %s\n", o.StorageLocation)
218199
}
219200
if o.storageLocationAutoSelected {
220-
fmt.Printf("Auto-selected storage location: %s (only NABSL in namespace)\n", o.StorageLocation)
201+
fmt.Printf("Auto-selected storage location: %s (only usable NABSL in namespace)\n", o.StorageLocation)
221202
fmt.Printf("Warning: If you create another NABSL in this namespace, future backups may not use the same location.\n")
222203
}
204+
if o.storageLocationPrompted {
205+
fmt.Printf("Selected storage location: %s\n", o.StorageLocation)
206+
}
223207

224208
fmt.Printf("NonAdminBackup request %q submitted successfully.\n", nonAdminBackup.Name)
225209
fmt.Printf("Run `oc oadp nonadmin backup describe %s` or `oc oadp nonadmin backup logs %s` for more details.\n", nonAdminBackup.Name, nonAdminBackup.Name)
@@ -296,3 +280,96 @@ func getNABSLFromConfig() string {
296280
}
297281
return ""
298282
}
283+
284+
func (o *CreateOptions) resolveStorageLocation(namespace string) error {
285+
if o.StorageLocation != "" {
286+
return nil
287+
}
288+
289+
if defaultNABSL := getNABSLFromConfig(); defaultNABSL != "" {
290+
o.StorageLocation = defaultNABSL
291+
o.storageLocationFromConfig = true
292+
return nil
293+
}
294+
295+
nabslList := &nacv1alpha1.NonAdminBackupStorageLocationList{}
296+
if err := o.client.List(context.TODO(), nabslList, &kbclient.ListOptions{
297+
Namespace: namespace,
298+
}); err != nil {
299+
return fmt.Errorf("failed to list NonAdminBackupStorageLocations: %w", err)
300+
}
301+
302+
return o.resolveStorageLocationFromList(namespace, nabslList.Items)
303+
}
304+
305+
func (o *CreateOptions) resolveStorageLocationFromList(namespace string, items []nacv1alpha1.NonAdminBackupStorageLocation) error {
306+
// Filter to only approved/created NABSLs (exclude pending/rejected)
307+
usable := make([]nacv1alpha1.NonAdminBackupStorageLocation, 0, len(items))
308+
for _, nabsl := range items {
309+
if nabsl.Status.Phase == nacv1alpha1.NonAdminPhaseCreated {
310+
usable = append(usable, nabsl)
311+
}
312+
}
313+
314+
switch len(usable) {
315+
case 0:
316+
if len(items) == 0 {
317+
return fmt.Errorf("no NonAdminBackupStorageLocations found in namespace %q\n"+
318+
"Create one with `oc oadp nonadmin bsl create` or specify `--storage-location`", namespace)
319+
}
320+
return fmt.Errorf("no usable NonAdminBackupStorageLocation with phase %q found in namespace %q\n"+
321+
"Check status with `oc oadp nonadmin bsl get` or specify `--storage-location`",
322+
nacv1alpha1.NonAdminPhaseCreated, namespace)
323+
case 1:
324+
o.StorageLocation = usable[0].Name
325+
o.storageLocationAutoSelected = true
326+
return nil
327+
default:
328+
selected, err := promptForNABSLSelection(usable, os.Stdin, os.Stderr)
329+
if err != nil {
330+
return err
331+
}
332+
o.StorageLocation = selected
333+
o.storageLocationPrompted = true
334+
return nil
335+
}
336+
}
337+
338+
func promptForNABSLSelection(items []nacv1alpha1.NonAdminBackupStorageLocation, in io.Reader, out io.Writer) (string, error) {
339+
inFile, inOk := in.(*os.File)
340+
outFile, outOk := out.(*os.File)
341+
if !inOk || !outOk || !term.IsTerminal(int(inFile.Fd())) || !term.IsTerminal(int(outFile.Fd())) {
342+
return "", fmt.Errorf("multiple NonAdminBackupStorageLocations found; specify one with --storage-location\n" +
343+
"To list available locations, run: oc oadp nonadmin bsl get")
344+
}
345+
346+
fmt.Fprintln(out, "Multiple non-admin backup storage locations found. Select one:")
347+
for i := range items {
348+
nabsl := &items[i]
349+
fmt.Fprintf(out, " %d) %s (%s)\n", i+1, nabsl.Name, formatNABSLPhase(nabsl))
350+
}
351+
352+
reader := bufio.NewReader(in)
353+
for {
354+
fmt.Fprintf(out, "Enter number (1-%d): ", len(items))
355+
response, err := reader.ReadString('\n')
356+
if err != nil {
357+
return "", fmt.Errorf("failed to read user input: %w", err)
358+
}
359+
360+
choice, err := strconv.Atoi(strings.TrimSpace(response))
361+
if err != nil || choice < 1 || choice > len(items) {
362+
fmt.Fprintln(out, "Invalid selection. Please enter a number from the list.")
363+
continue
364+
}
365+
366+
return items[choice-1].Name, nil
367+
}
368+
}
369+
370+
func formatNABSLPhase(nabsl *nacv1alpha1.NonAdminBackupStorageLocation) string {
371+
if nabsl.Status.Phase != "" {
372+
return string(nabsl.Status.Phase)
373+
}
374+
return "Unknown"
375+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/spf13/pflag v1.0.6
1313
github.com/vmware-tanzu/velero v1.14.0
1414
golang.org/x/sync v0.20.0
15+
golang.org/x/term v0.41.0
1516
gopkg.in/yaml.v2 v2.4.0
1617
k8s.io/api v0.33.3
1718
k8s.io/apimachinery v0.33.3
@@ -97,7 +98,6 @@ require (
9798
golang.org/x/net v0.52.0 // indirect
9899
golang.org/x/oauth2 v0.33.0 // indirect
99100
golang.org/x/sys v0.42.0 // indirect
100-
golang.org/x/term v0.41.0 // indirect
101101
golang.org/x/text v0.35.0 // indirect
102102
golang.org/x/time v0.14.0 // indirect
103103
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect

0 commit comments

Comments
 (0)