github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/volume_status_csi.go (about)

     1  package command
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/nomad/api"
    11  )
    12  
    13  func (c *VolumeStatusCommand) csiBanner() {
    14  	if !(c.json || len(c.template) > 0) {
    15  		c.Ui.Output(c.Colorize().Color("[bold]Container Storage Interface[reset]"))
    16  	}
    17  }
    18  
    19  func (c *VolumeStatusCommand) csiStatus(client *api.Client, id string) int {
    20  	// Invoke list mode if no volume id
    21  	if id == "" {
    22  		return c.listVolumes(client)
    23  	}
    24  
    25  	// Prefix search for the volume
    26  	vols, _, err := client.CSIVolumes().List(&api.QueryOptions{Prefix: id})
    27  	if err != nil {
    28  		c.Ui.Error(fmt.Sprintf("Error querying volumes: %s", err))
    29  		return 1
    30  	}
    31  	if len(vols) == 0 {
    32  		c.Ui.Error(fmt.Sprintf("No volumes(s) with prefix or ID %q found", id))
    33  		return 1
    34  	}
    35  
    36  	var ns string
    37  
    38  	if len(vols) == 1 {
    39  		// need to set id from the actual ID because it might be a prefix
    40  		id = vols[0].ID
    41  		ns = vols[0].Namespace
    42  	}
    43  
    44  	// List sorts by CreateIndex, not by ID, so we need to search for
    45  	// exact matches but account for multiple exact ID matches across
    46  	// namespaces
    47  	if len(vols) > 1 {
    48  		exactMatchesCount := 0
    49  		for _, vol := range vols {
    50  			if vol.ID == id {
    51  				exactMatchesCount++
    52  				ns = vol.Namespace
    53  			}
    54  		}
    55  		if exactMatchesCount != 1 {
    56  			out, err := c.csiFormatVolumes(vols)
    57  			if err != nil {
    58  				c.Ui.Error(fmt.Sprintf("Error formatting: %s", err))
    59  				return 1
    60  			}
    61  			c.Ui.Error(fmt.Sprintf("Prefix matched multiple volumes\n\n%s", out))
    62  			return 1
    63  		}
    64  	}
    65  
    66  	// Try querying the volume
    67  	client.SetNamespace(ns)
    68  	vol, _, err := client.CSIVolumes().Info(id, nil)
    69  	if err != nil {
    70  		c.Ui.Error(fmt.Sprintf("Error querying volume: %s", err))
    71  		return 1
    72  	}
    73  
    74  	str, err := c.formatBasic(vol)
    75  	if err != nil {
    76  		c.Ui.Error(fmt.Sprintf("Error formatting volume: %s", err))
    77  		return 1
    78  	}
    79  	c.Ui.Output(str)
    80  
    81  	return 0
    82  }
    83  
    84  func (c *VolumeStatusCommand) listVolumes(client *api.Client) int {
    85  
    86  	c.csiBanner()
    87  	vols, _, err := client.CSIVolumes().List(nil)
    88  	if err != nil {
    89  		c.Ui.Error(fmt.Sprintf("Error querying volumes: %s", err))
    90  		return 1
    91  	}
    92  
    93  	if len(vols) == 0 {
    94  		// No output if we have no volumes
    95  		c.Ui.Error("No CSI volumes")
    96  	} else {
    97  		str, err := c.csiFormatVolumes(vols)
    98  		if err != nil {
    99  			c.Ui.Error(fmt.Sprintf("Error formatting: %s", err))
   100  			return 1
   101  		}
   102  		c.Ui.Output(str)
   103  	}
   104  	if !c.verbose {
   105  		return 0
   106  	}
   107  
   108  	plugins, _, err := client.CSIPlugins().List(nil)
   109  	if err != nil {
   110  		c.Ui.Error(fmt.Sprintf("Error querying CSI plugins: %s", err))
   111  		return 1
   112  	}
   113  
   114  	if len(plugins) == 0 {
   115  		return 0 // No more output if we have no plugins
   116  	}
   117  
   118  	var code int
   119  	q := &api.QueryOptions{PerPage: 30} // TODO: tune page size
   120  
   121  NEXT_PLUGIN:
   122  	for _, plugin := range plugins {
   123  		if !plugin.ControllerRequired || plugin.ControllersHealthy < 1 {
   124  			continue // only controller plugins can support this query
   125  		}
   126  		for {
   127  			externalList, _, err := client.CSIVolumes().ListExternal(plugin.ID, q)
   128  			if err != nil && !errors.Is(err, io.EOF) {
   129  				c.Ui.Error(fmt.Sprintf(
   130  					"Error querying CSI external volumes for plugin %q: %s", plugin.ID, err))
   131  				// we'll stop querying this plugin, but there may be more to
   132  				// query, so report and set the error code but move on to the
   133  				// next plugin
   134  				code = 1
   135  				continue NEXT_PLUGIN
   136  			}
   137  			if externalList == nil || len(externalList.Volumes) == 0 {
   138  				// several plugins return EOF once you hit the end of the page,
   139  				// rather than an empty list
   140  				continue NEXT_PLUGIN
   141  			}
   142  			rows := []string{"External ID|Condition|Nodes"}
   143  			for _, v := range externalList.Volumes {
   144  				condition := "OK"
   145  				if v.IsAbnormal {
   146  					condition = fmt.Sprintf("Abnormal (%v)", v.Status)
   147  				}
   148  				rows = append(rows, fmt.Sprintf("%s|%s|%s",
   149  					limit(v.ExternalID, c.length),
   150  					limit(condition, 20),
   151  					strings.Join(v.PublishedExternalNodeIDs, ","),
   152  				))
   153  			}
   154  			c.Ui.Output(formatList(rows))
   155  
   156  			q.NextToken = externalList.NextToken
   157  			if q.NextToken == "" {
   158  				break
   159  			}
   160  			// we can't know the shape of arbitrarily-sized lists of volumes,
   161  			// so break after each page
   162  			c.Ui.Output("...")
   163  		}
   164  	}
   165  
   166  	return code
   167  }
   168  
   169  func (c *VolumeStatusCommand) csiFormatVolumes(vols []*api.CSIVolumeListStub) (string, error) {
   170  	// Sort the output by volume id
   171  	sort.Slice(vols, func(i, j int) bool { return vols[i].ID < vols[j].ID })
   172  
   173  	if c.json || len(c.template) > 0 {
   174  		out, err := Format(c.json, c.template, vols)
   175  		if err != nil {
   176  			return "", fmt.Errorf("format error: %v", err)
   177  		}
   178  		return out, nil
   179  	}
   180  
   181  	return csiFormatSortedVolumes(vols)
   182  }
   183  
   184  // Format the volumes, assumes that we're already sorted by volume ID
   185  func csiFormatSortedVolumes(vols []*api.CSIVolumeListStub) (string, error) {
   186  	rows := make([]string, len(vols)+1)
   187  	rows[0] = "ID|Name|Plugin ID|Schedulable|Access Mode"
   188  	for i, v := range vols {
   189  		rows[i+1] = fmt.Sprintf("%s|%s|%s|%t|%s",
   190  			v.ID,
   191  			v.Name,
   192  			v.PluginID,
   193  			v.Schedulable,
   194  			v.AccessMode,
   195  		)
   196  	}
   197  	return formatList(rows), nil
   198  }
   199  
   200  func (c *VolumeStatusCommand) formatBasic(vol *api.CSIVolume) (string, error) {
   201  	if c.json || len(c.template) > 0 {
   202  		out, err := Format(c.json, c.template, vol)
   203  		if err != nil {
   204  			return "", fmt.Errorf("format error: %v", err)
   205  		}
   206  		return out, nil
   207  	}
   208  
   209  	output := []string{
   210  		fmt.Sprintf("ID|%s", vol.ID),
   211  		fmt.Sprintf("Name|%s", vol.Name),
   212  		fmt.Sprintf("External ID|%s", vol.ExternalID),
   213  		fmt.Sprintf("Plugin ID|%s", vol.PluginID),
   214  		fmt.Sprintf("Provider|%s", vol.Provider),
   215  		fmt.Sprintf("Version|%s", vol.ProviderVersion),
   216  		fmt.Sprintf("Schedulable|%t", vol.Schedulable),
   217  		fmt.Sprintf("Controllers Healthy|%d", vol.ControllersHealthy),
   218  		fmt.Sprintf("Controllers Expected|%d", vol.ControllersExpected),
   219  		fmt.Sprintf("Nodes Healthy|%d", vol.NodesHealthy),
   220  		fmt.Sprintf("Nodes Expected|%d", vol.NodesExpected),
   221  
   222  		fmt.Sprintf("Access Mode|%s", vol.AccessMode),
   223  		fmt.Sprintf("Attachment Mode|%s", vol.AttachmentMode),
   224  		fmt.Sprintf("Mount Options|%s", csiVolMountOption(vol.MountOptions, nil)),
   225  		fmt.Sprintf("Namespace|%s", vol.Namespace),
   226  	}
   227  
   228  	// Exit early
   229  	if c.short {
   230  		return formatKV(output), nil
   231  	}
   232  
   233  	full := []string{formatKV(output)}
   234  
   235  	if len(vol.Topologies) > 0 {
   236  		topoBanner := c.Colorize().Color("\n[bold]Topologies[reset]")
   237  		topo := c.formatTopology(vol)
   238  		full = append(full, topoBanner)
   239  		full = append(full, topo)
   240  	}
   241  
   242  	// Format the allocs
   243  	banner := c.Colorize().Color("\n[bold]Allocations[reset]")
   244  	allocs := formatAllocListStubs(vol.Allocations, c.verbose, c.length)
   245  	full = append(full, banner)
   246  	full = append(full, allocs)
   247  
   248  	return strings.Join(full, "\n"), nil
   249  }
   250  
   251  func (c *VolumeStatusCommand) formatTopology(vol *api.CSIVolume) string {
   252  	rows := []string{"Topology|Segments"}
   253  	for i, t := range vol.Topologies {
   254  		if t == nil {
   255  			continue
   256  		}
   257  		segmentPairs := make([]string, 0, len(t.Segments))
   258  		for k, v := range t.Segments {
   259  			segmentPairs = append(segmentPairs, fmt.Sprintf("%s=%s", k, v))
   260  		}
   261  		// note: this looks awkward because we don't have any other
   262  		// place where we list collections of arbitrary k/v's like
   263  		// this without just dumping JSON formatted outputs. It's likely
   264  		// the spec will expand to add extra fields, in which case we'll
   265  		// add them here and drop the first column
   266  		rows = append(rows, fmt.Sprintf("%02d|%v", i, strings.Join(segmentPairs, ", ")))
   267  	}
   268  	if len(rows) == 1 {
   269  		return ""
   270  	}
   271  	return formatList(rows)
   272  }
   273  
   274  func csiVolMountOption(volume, request *api.CSIMountOptions) string {
   275  	var req, opts *api.CSIMountOptions
   276  
   277  	if request != nil {
   278  		req = &api.CSIMountOptions{
   279  			FSType:     request.FSType,
   280  			MountFlags: request.MountFlags,
   281  		}
   282  	}
   283  
   284  	if volume == nil {
   285  		opts = req
   286  	} else {
   287  		opts = &api.CSIMountOptions{
   288  			FSType:     volume.FSType,
   289  			MountFlags: volume.MountFlags,
   290  		}
   291  		opts.Merge(req)
   292  	}
   293  
   294  	if opts == nil {
   295  		return "<none>"
   296  	}
   297  
   298  	var out string
   299  	if opts.FSType != "" {
   300  		out = fmt.Sprintf("fs_type: %s", opts.FSType)
   301  	}
   302  
   303  	if len(opts.MountFlags) > 0 {
   304  		out = fmt.Sprintf("%s flags: %s", out, strings.Join(opts.MountFlags, ", "))
   305  	}
   306  
   307  	return out
   308  }