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 }