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

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"strings"
     7  
     8  	"github.com/hashicorp/nomad/api/contexts"
     9  	"github.com/posener/complete"
    10  	"github.com/skratchdot/open-golang/open"
    11  )
    12  
    13  var (
    14  	// uiContexts is the contexts the ui can open automatically.
    15  	uiContexts = []contexts.Context{contexts.Jobs, contexts.Allocs, contexts.Nodes}
    16  )
    17  
    18  type UiCommand struct {
    19  	Meta
    20  }
    21  
    22  func (c *UiCommand) Help() string {
    23  	helpText := `
    24  Usage: nomad ui [options] <identifier>
    25  
    26  Open the Nomad Web UI in the default browser. An optional identifier may be
    27  provided, in which case the UI will be opened to view the details for that
    28  object. Supported identifiers are jobs, allocations and nodes.
    29  
    30  General Options:
    31  
    32    ` + generalOptionsUsage(usageOptsDefault) + `
    33  
    34  UI Options
    35  
    36    -authenticate: Exchange your Nomad ACL token for a one-time token in the
    37      web UI, if ACLs are enabled.
    38  
    39    -show-url: Show the Nomad UI URL instead of opening with the default browser.
    40  `
    41  
    42  	return strings.TrimSpace(helpText)
    43  }
    44  
    45  func (c *UiCommand) AutocompleteFlags() complete.Flags {
    46  	return c.Meta.AutocompleteFlags(FlagSetClient)
    47  }
    48  
    49  func (c *UiCommand) AutocompleteArgs() complete.Predictor {
    50  	return complete.PredictFunc(func(a complete.Args) []string {
    51  		client, err := c.Meta.Client()
    52  		if err != nil {
    53  			return nil
    54  		}
    55  
    56  		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.All, nil)
    57  		if err != nil {
    58  			return []string{}
    59  		}
    60  
    61  		final := make([]string, 0)
    62  
    63  		for _, allowed := range uiContexts {
    64  			matches, ok := resp.Matches[allowed]
    65  			if !ok {
    66  				continue
    67  			}
    68  			if len(matches) == 0 {
    69  				continue
    70  			}
    71  
    72  			final = append(final, matches...)
    73  		}
    74  
    75  		return final
    76  	})
    77  }
    78  
    79  func (c *UiCommand) Synopsis() string {
    80  	return "Open the Nomad Web UI"
    81  }
    82  
    83  func (c *UiCommand) Name() string { return "ui" }
    84  
    85  func (c *UiCommand) Run(args []string) int {
    86  	var authenticate bool
    87  	var showUrl bool
    88  
    89  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
    90  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    91  	flags.BoolVar(&authenticate, "authenticate", false, "")
    92  	flags.BoolVar(&showUrl, "show-url", false, "")
    93  
    94  	if err := flags.Parse(args); err != nil {
    95  		return 1
    96  	}
    97  
    98  	// Check that we got no more than one argument
    99  	args = flags.Args()
   100  	if l := len(args); l > 1 {
   101  		c.Ui.Error("This command takes no or one optional argument, [<identifier>]")
   102  		c.Ui.Error(commandErrorText(c))
   103  		return 1
   104  	}
   105  
   106  	// Get the HTTP client
   107  	client, err := c.Meta.Client()
   108  	if err != nil {
   109  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   110  		return 1
   111  	}
   112  
   113  	url, err := url.Parse(client.Address())
   114  	if err != nil {
   115  		c.Ui.Error(fmt.Sprintf("Error parsing Nomad address %q: %s", client.Address(), err))
   116  		return 1
   117  	}
   118  
   119  	// Set query params if necessary
   120  	qp := url.Query()
   121  	if ns := c.clientConfig().Namespace; ns != "" {
   122  		qp.Add("namespace", ns)
   123  	}
   124  	if region := c.clientConfig().Region; region != "" {
   125  		qp.Add("region", region)
   126  	}
   127  	url.RawQuery = qp.Encode()
   128  
   129  	// Set one-time secret
   130  	var ottSecret string
   131  	if authenticate {
   132  		ott, _, err := client.ACLTokens().UpsertOneTimeToken(nil)
   133  		if err != nil {
   134  			c.Ui.Error(fmt.Sprintf("Could not get one-time token: %s", err))
   135  			return 1
   136  		}
   137  		ottSecret = ott.OneTimeSecretID
   138  	}
   139  
   140  	// We were given an id so look it up
   141  	if len(args) == 1 {
   142  		id := args[0]
   143  
   144  		// Query for the context associated with the id
   145  		res, _, err := client.Search().PrefixSearch(id, contexts.All, nil)
   146  		if err != nil {
   147  			c.Ui.Error(fmt.Sprintf("Error querying search with id: %q", err))
   148  			return 1
   149  		}
   150  
   151  		if res.Matches == nil {
   152  			c.Ui.Error(fmt.Sprintf("No matches returned for query: %q", err))
   153  			return 1
   154  		}
   155  
   156  		var match contexts.Context
   157  		var fullID string
   158  		matchCount := 0
   159  		for _, ctx := range uiContexts {
   160  			vers, ok := res.Matches[ctx]
   161  			if !ok {
   162  				continue
   163  			}
   164  
   165  			if l := len(vers); l == 1 {
   166  				match = ctx
   167  				fullID = vers[0]
   168  				matchCount++
   169  			} else if l > 0 && vers[0] == id {
   170  				// Exact match
   171  				match = ctx
   172  				fullID = vers[0]
   173  				break
   174  			}
   175  
   176  			// Only a single result should return, as this is a match against a full id
   177  			if matchCount > 1 || len(vers) > 1 {
   178  				c.logMultiMatchError(id, res.Matches)
   179  				return 1
   180  			}
   181  		}
   182  
   183  		switch match {
   184  		case contexts.Nodes:
   185  			url.Path = fmt.Sprintf("ui/clients/%s", fullID)
   186  		case contexts.Allocs:
   187  			url.Path = fmt.Sprintf("ui/allocations/%s", fullID)
   188  		case contexts.Jobs:
   189  			url.Path = fmt.Sprintf("ui/jobs/%s", fullID)
   190  		default:
   191  			c.Ui.Error(fmt.Sprintf("Unable to resolve ID: %q", id))
   192  			return 1
   193  		}
   194  	}
   195  
   196  	var output string
   197  	if authenticate && ottSecret != "" {
   198  		output = fmt.Sprintf("Opening URL %q with one-time token", url.String())
   199  		qp := url.Query()
   200  		qp.Add("ott", ottSecret)
   201  		url.RawQuery = qp.Encode()
   202  	} else {
   203  		output = fmt.Sprintf("Opening URL %q", url.String())
   204  	}
   205  
   206  	if showUrl {
   207  		c.Ui.Output(fmt.Sprintf("URL for web UI: %s", url.String()))
   208  		return 0
   209  	}
   210  
   211  	c.Ui.Output(output)
   212  	if err := open.Start(url.String()); err != nil {
   213  		c.Ui.Error(fmt.Sprintf("Error opening URL: %s", err))
   214  		return 1
   215  	}
   216  	return 0
   217  }
   218  
   219  // logMultiMatchError is used to log an error message when multiple matches are
   220  // found. The error message logged displays the matched IDs per context.
   221  func (c *UiCommand) logMultiMatchError(id string, matches map[contexts.Context][]string) {
   222  	c.Ui.Error(fmt.Sprintf("Multiple matches found for id %q", id))
   223  	for _, ctx := range uiContexts {
   224  		vers, ok := matches[ctx]
   225  		if !ok {
   226  			continue
   227  		}
   228  		if len(vers) == 0 {
   229  			continue
   230  		}
   231  
   232  		c.Ui.Error(fmt.Sprintf("\n%s:", strings.Title(string(ctx))))
   233  		c.Ui.Error(strings.Join(vers, ", "))
   234  	}
   235  }