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

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