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 }