github.com/hernad/nomad@v1.6.112/command/volume_snapshot_list.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/dustin/go-humanize"
    15  	"github.com/hernad/nomad/api"
    16  	"github.com/hernad/nomad/api/contexts"
    17  	flaghelper "github.com/hernad/nomad/helper/flags"
    18  	"github.com/posener/complete"
    19  )
    20  
    21  type VolumeSnapshotListCommand struct {
    22  	Meta
    23  }
    24  
    25  func (c *VolumeSnapshotListCommand) Help() string {
    26  	helpText := `
    27  Usage: nomad volume snapshot list [-plugin plugin_id]
    28  
    29    Display a list of CSI volume snapshots for a plugin along
    30    with their source volume ID as known to the external
    31    storage provider.
    32  
    33    When ACLs are enabled, this command requires a token with the
    34    'csi-list-volumes' capability for the plugin's namespace.
    35  
    36  General Options:
    37  
    38    ` + generalOptionsUsage(usageOptsDefault) + `
    39  
    40  List Options:
    41  
    42    -page-token
    43      Where to start pagination.
    44  
    45    -per-page
    46      How many results to show per page. Defaults to 30.
    47  
    48    -plugin: Display only snapshots managed by a particular plugin. This
    49      parameter is required.
    50  
    51    -secret
    52      Secrets to pass to the plugin to list snapshots. Accepts multiple
    53      flags in the form -secret key=value
    54  
    55    -verbose
    56      Display full information for snapshots.
    57  `
    58  
    59  	return strings.TrimSpace(helpText)
    60  }
    61  
    62  func (c *VolumeSnapshotListCommand) Synopsis() string {
    63  	return "Display a list of volume snapshots for plugin"
    64  }
    65  
    66  func (c *VolumeSnapshotListCommand) AutocompleteFlags() complete.Flags {
    67  	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
    68  		complete.Flags{})
    69  }
    70  
    71  func (c *VolumeSnapshotListCommand) AutocompleteArgs() complete.Predictor {
    72  	return complete.PredictFunc(func(a complete.Args) []string {
    73  		client, err := c.Meta.Client()
    74  		if err != nil {
    75  			return nil
    76  		}
    77  
    78  		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Plugins, nil)
    79  		if err != nil {
    80  			return []string{}
    81  		}
    82  		return resp.Matches[contexts.Plugins]
    83  	})
    84  }
    85  
    86  func (c *VolumeSnapshotListCommand) Name() string { return "volume snapshot list" }
    87  
    88  func (c *VolumeSnapshotListCommand) Run(args []string) int {
    89  	var pluginID string
    90  	var verbose bool
    91  	var secretsArgs flaghelper.StringFlag
    92  	var perPage int
    93  	var pageToken string
    94  
    95  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
    96  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    97  	flags.StringVar(&pluginID, "plugin", "", "")
    98  	flags.BoolVar(&verbose, "verbose", false, "")
    99  	flags.Var(&secretsArgs, "secret", "secrets for snapshot, ex. -secret key=value")
   100  	flags.IntVar(&perPage, "per-page", 30, "")
   101  	flags.StringVar(&pageToken, "page-token", "", "")
   102  
   103  	if err := flags.Parse(args); err != nil {
   104  		c.Ui.Error(fmt.Sprintf("Error parsing arguments %s", err))
   105  		return 1
   106  	}
   107  
   108  	args = flags.Args()
   109  	if len(args) > 0 {
   110  		c.Ui.Error("This command takes no arguments")
   111  		c.Ui.Error(commandErrorText(c))
   112  		return 1
   113  	}
   114  
   115  	// Get the HTTP client
   116  	client, err := c.Meta.Client()
   117  	if err != nil {
   118  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   119  		return 1
   120  	}
   121  
   122  	plugs, _, err := client.CSIPlugins().List(&api.QueryOptions{Prefix: pluginID})
   123  	if err != nil {
   124  		c.Ui.Error(fmt.Sprintf("Error querying CSI plugins: %s", err))
   125  		return 1
   126  	}
   127  	if len(plugs) == 0 {
   128  		c.Ui.Error(fmt.Sprintf("No plugins(s) with prefix or ID %q found", pluginID))
   129  		return 1
   130  	}
   131  	if len(plugs) > 1 {
   132  		if pluginID != plugs[0].ID {
   133  			out, err := c.csiFormatPlugins(plugs)
   134  			if err != nil {
   135  				c.Ui.Error(fmt.Sprintf("Error formatting: %s", err))
   136  				return 1
   137  			}
   138  			c.Ui.Error(fmt.Sprintf("Prefix matched multiple plugins\n\n%s", out))
   139  			return 1
   140  		}
   141  	}
   142  	pluginID = plugs[0].ID
   143  
   144  	secrets := api.CSISecrets{}
   145  	for _, kv := range secretsArgs {
   146  		if key, value, found := strings.Cut(kv, "="); found {
   147  			secrets[key] = value
   148  		} else {
   149  			c.Ui.Error("Secret must be in the format: -secret key=value")
   150  			return 1
   151  		}
   152  	}
   153  
   154  	req := &api.CSISnapshotListRequest{
   155  		PluginID: pluginID,
   156  		Secrets:  secrets,
   157  		QueryOptions: api.QueryOptions{
   158  			PerPage:   int32(perPage),
   159  			NextToken: pageToken,
   160  			Params:    map[string]string{},
   161  		},
   162  	}
   163  
   164  	resp, _, err := client.CSIVolumes().ListSnapshotsOpts(req)
   165  	if err != nil && !errors.Is(err, io.EOF) {
   166  		c.Ui.Error(fmt.Sprintf(
   167  			"Error querying CSI external snapshots for plugin %q: %s", pluginID, err))
   168  		return 1
   169  	}
   170  	if resp == nil || len(resp.Snapshots) == 0 {
   171  		// several plugins return EOF once you hit the end of the page,
   172  		// rather than an empty list
   173  		return 0
   174  	}
   175  
   176  	c.Ui.Output(csiFormatSnapshots(resp.Snapshots, verbose))
   177  
   178  	if resp.NextToken != "" {
   179  		c.Ui.Output(fmt.Sprintf(`
   180  Results have been paginated. To get the next page run:
   181  %s -page-token %s`, argsWithoutPageToken(os.Args), resp.NextToken))
   182  	}
   183  
   184  	return 0
   185  }
   186  
   187  func csiFormatSnapshots(snapshots []*api.CSISnapshot, verbose bool) string {
   188  	rows := []string{"Snapshot ID|Volume ID|Size|Create Time|Ready?"}
   189  	length := 12
   190  	if verbose {
   191  		length = 30
   192  	}
   193  	for _, v := range snapshots {
   194  		rows = append(rows, fmt.Sprintf("%s|%s|%s|%s|%v",
   195  			v.ID,
   196  			limit(v.ExternalSourceVolumeID, length),
   197  			humanize.IBytes(uint64(v.SizeBytes)),
   198  			formatUnixNanoTime(v.CreateTime*1e9), // seconds to nanoseconds
   199  			v.IsReady,
   200  		))
   201  	}
   202  	return formatList(rows)
   203  }
   204  
   205  func (c *VolumeSnapshotListCommand) csiFormatPlugins(plugs []*api.CSIPluginListStub) (string, error) {
   206  	// TODO: this has a lot of overlap with 'nomad plugin status', so we
   207  	// should factor out some shared formatting helpers.
   208  	sort.Slice(plugs, func(i, j int) bool { return plugs[i].ID < plugs[j].ID })
   209  	length := 30
   210  	rows := make([]string, len(plugs)+1)
   211  	rows[0] = "ID|Provider|Controllers Healthy/Expected|Nodes Healthy/Expected"
   212  	for i, p := range plugs {
   213  		rows[i+1] = fmt.Sprintf("%s|%s|%d/%d|%d/%d",
   214  			limit(p.ID, length),
   215  			p.Provider,
   216  			p.ControllersHealthy,
   217  			p.ControllersExpected,
   218  			p.NodesHealthy,
   219  			p.NodesExpected,
   220  		)
   221  	}
   222  	return formatList(rows), nil
   223  }