@@ -17,8 +17,13 @@ limitations under the License.
1717*/
1818
1919import (
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
3743func 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
8996func 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+ }
0 commit comments