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 }