1package command
2
3import (
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
13var (
14	// uiContexts is the contexts the ui can open automatically.
15	uiContexts = []contexts.Context{contexts.Jobs, contexts.Allocs, contexts.Nodes}
16)
17
18type UiCommand struct {
19	Meta
20}
21
22func (c *UiCommand) Help() string {
23	helpText := `
24Usage: nomad ui [options] <identifier>
25
26Open the Nomad Web UI in the default browser. An optional identifier may be
27provided, in which case the UI will be opened to view the details for that
28object. Supported identifiers are jobs, allocations and nodes.
29
30General Options:
31
32  ` + generalOptionsUsage()
33
34	return strings.TrimSpace(helpText)
35}
36
37func (c *UiCommand) AutocompleteFlags() complete.Flags {
38	return c.Meta.AutocompleteFlags(FlagSetClient)
39}
40
41func (c *UiCommand) AutocompleteArgs() complete.Predictor {
42	return complete.PredictFunc(func(a complete.Args) []string {
43		client, err := c.Meta.Client()
44		if err != nil {
45			return nil
46		}
47
48		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.All, nil)
49		if err != nil {
50			return []string{}
51		}
52
53		final := make([]string, 0)
54
55		for _, allowed := range uiContexts {
56			matches, ok := resp.Matches[allowed]
57			if !ok {
58				continue
59			}
60			if len(matches) == 0 {
61				continue
62			}
63
64			final = append(final, matches...)
65		}
66
67		return final
68	})
69}
70
71func (c *UiCommand) Synopsis() string {
72	return "Open the Nomad Web UI"
73}
74
75func (c *UiCommand) Name() string { return "ui" }
76
77func (c *UiCommand) Run(args []string) int {
78	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
79	flags.Usage = func() { c.Ui.Output(c.Help()) }
80
81	if err := flags.Parse(args); err != nil {
82		return 1
83	}
84
85	// Check that we got no more than one argument
86	args = flags.Args()
87	if l := len(args); l > 1 {
88		c.Ui.Error("This command takes no or one optional argument, [<identifier>]")
89		c.Ui.Error(commandErrorText(c))
90		return 1
91	}
92
93	// Get the HTTP client
94	client, err := c.Meta.Client()
95	if err != nil {
96		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
97		return 1
98	}
99
100	url, err := url.Parse(client.Address())
101	if err != nil {
102		c.Ui.Error(fmt.Sprintf("Error parsing Nomad address %q: %s", client.Address(), err))
103		return 1
104	}
105
106	// We were given an id so look it up
107	if len(args) == 1 {
108		id := args[0]
109
110		// Query for the context associated with the id
111		res, _, err := client.Search().PrefixSearch(id, contexts.All, nil)
112		if err != nil {
113			c.Ui.Error(fmt.Sprintf("Error querying search with id: %q", err))
114			return 1
115		}
116
117		if res.Matches == nil {
118			c.Ui.Error(fmt.Sprintf("No matches returned for query: %q", err))
119			return 1
120		}
121
122		var match contexts.Context
123		var fullID string
124		matchCount := 0
125		for _, ctx := range uiContexts {
126			vers, ok := res.Matches[ctx]
127			if !ok {
128				continue
129			}
130
131			if l := len(vers); l == 1 {
132				match = ctx
133				fullID = vers[0]
134				matchCount++
135			} else if l > 0 && vers[0] == id {
136				// Exact match
137				match = ctx
138				fullID = vers[0]
139				break
140			}
141
142			// Only a single result should return, as this is a match against a full id
143			if matchCount > 1 || len(vers) > 1 {
144				c.logMultiMatchError(id, res.Matches)
145				return 1
146			}
147		}
148
149		switch match {
150		case contexts.Nodes:
151			url.Path = fmt.Sprintf("ui/nodes/%s", fullID)
152		case contexts.Allocs:
153			url.Path = fmt.Sprintf("ui/allocations/%s", fullID)
154		case contexts.Jobs:
155			url.Path = fmt.Sprintf("ui/jobs/%s", fullID)
156		default:
157			c.Ui.Error(fmt.Sprintf("Unable to resolve ID: %q", id))
158			return 1
159		}
160	}
161
162	c.Ui.Output(fmt.Sprintf("Opening URL %q", url.String()))
163	if err := open.Start(url.String()); err != nil {
164		c.Ui.Error(fmt.Sprintf("Error opening URL: %s", err))
165		return 1
166	}
167
168	return 0
169}
170
171// logMultiMatchError is used to log an error message when multiple matches are
172// found. The error message logged displays the matched IDs per context.
173func (c *UiCommand) logMultiMatchError(id string, matches map[contexts.Context][]string) {
174	c.Ui.Error(fmt.Sprintf("Multiple matches found for id %q", id))
175	for _, ctx := range uiContexts {
176		vers, ok := matches[ctx]
177		if !ok {
178			continue
179		}
180		if len(vers) == 0 {
181			continue
182		}
183
184		c.Ui.Error(fmt.Sprintf("\n%s:", strings.Title(string(ctx))))
185		c.Ui.Error(fmt.Sprintf("%s", strings.Join(vers, ", ")))
186	}
187}
188