Merge pull request #3348 from hashicorp/f-ui

Nomad UI Command
This commit is contained in:
Alex Dadgar 2017-10-11 15:52:29 -07:00 committed by GitHub
commit a2a543114e
11 changed files with 315 additions and 2 deletions

View file

@ -325,6 +325,11 @@ func NewClient(config *Config) (*Client, error) {
return client, nil
}
// Address return the address of the Nomad agent
func (c *Client) Address() string {
return c.config.Address
}
// SetRegion sets the region to forward API requests to.
func (c *Client) SetRegion(region string) {
c.config.Region = region

View file

@ -1,4 +1,4 @@
//+build linux
// +build linux
package driver

View file

@ -1,3 +1,5 @@
// +build linux
package driver
import (

View file

@ -33,7 +33,7 @@ func (c *StatusCommand) Synopsis() string {
}
func (c *StatusCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), nil)
return c.Meta.AutocompleteFlags(FlagSetClient)
}
func (c *StatusCommand) AutocompleteArgs() complete.Predictor {

184
command/ui.go Normal file
View file

@ -0,0 +1,184 @@
package command
import (
"fmt"
"net/url"
"strings"
"github.com/hashicorp/nomad/api/contexts"
"github.com/posener/complete"
"github.com/skratchdot/open-golang/open"
)
var (
// uiContexts is the contexts the ui can open automatically.
uiContexts = []contexts.Context{contexts.Jobs, contexts.Allocs, contexts.Nodes}
)
type UiCommand struct {
Meta
}
func (c *UiCommand) Help() string {
helpText := `
Usage: nomad ui [options] <identifier>
Open the Nomad Web UI in the default browser. An optional identifier may be
provided in which case the UI will be opened to view the details for that
object. Supported identifiers are jobs, allocations and nodes.
General Options:
` + generalOptionsUsage()
return strings.TrimSpace(helpText)
}
func (c *UiCommand) AutocompleteFlags() complete.Flags {
return c.Meta.AutocompleteFlags(FlagSetClient)
}
func (c *UiCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictFunc(func(a complete.Args) []string {
client, err := c.Meta.Client()
if err != nil {
return nil
}
resp, _, err := client.Search().PrefixSearch(a.Last, contexts.All, nil)
if err != nil {
return []string{}
}
final := make([]string, 0)
for _, allowed := range uiContexts {
matches, ok := resp.Matches[allowed]
if !ok {
continue
}
if len(matches) == 0 {
continue
}
final = append(final, matches...)
}
return final
})
}
func (c *UiCommand) Synopsis() string {
return "Open the Nomad Web UI"
}
func (c *UiCommand) Run(args []string) int {
flags := c.Meta.FlagSet("deployment list", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we got no more than one argument
args = flags.Args()
if l := len(args); l > 1 {
c.Ui.Error(c.Help())
return 1
}
// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
url, err := url.Parse(client.Address())
if err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing Nomad address %q: %s", client.Address(), err))
return 1
}
// We were given an id so look it up
if len(args) == 1 {
id := args[0]
// Query for the context associated with the id
res, _, err := client.Search().PrefixSearch(id, contexts.All, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying search with id: %q", err))
return 1
}
if res.Matches == nil {
c.Ui.Error(fmt.Sprintf("No matches returned for query: %q", err))
return 1
}
var match contexts.Context
var fullID string
matchCount := 0
for _, ctx := range uiContexts {
vers, ok := res.Matches[ctx]
if !ok {
continue
}
if l := len(vers); l == 1 {
match = ctx
fullID = vers[0]
matchCount++
} else if l > 0 && vers[0] == id {
// Exact match
match = ctx
fullID = vers[0]
break
}
// Only a single result should return, as this is a match against a full id
if matchCount > 1 || len(vers) > 1 {
c.logMultiMatchError(id, res.Matches)
return 1
}
}
switch match {
case contexts.Nodes:
url.Path = fmt.Sprintf("ui/nodes/%s", fullID)
case contexts.Allocs:
url.Path = fmt.Sprintf("ui/allocations/%s", fullID)
case contexts.Jobs:
url.Path = fmt.Sprintf("ui/jobs/%s", fullID)
default:
c.Ui.Error(fmt.Sprintf("Unable to resolve ID: %q", id))
return 1
}
}
c.Ui.Output(fmt.Sprintf("Opening URL %q", url.String()))
if err := open.Start(url.String()); err != nil {
c.Ui.Error(fmt.Sprintf("Error opening URL: %s", err))
return 1
}
return 0
}
// logMultiMatchError is used to log an error message when multiple matches are
// found. The error message logged displays the matched IDs per context.
func (c *UiCommand) logMultiMatchError(id string, matches map[contexts.Context][]string) {
c.Ui.Error(fmt.Sprintf("Multiple matches found for id %q", id))
for _, ctx := range uiContexts {
vers, ok := matches[ctx]
if !ok {
continue
}
if len(vers) == 0 {
continue
}
c.Ui.Error(fmt.Sprintf("\n%s:", strings.Title(string(ctx))))
c.Ui.Error(fmt.Sprintf("%s", strings.Join(vers, ", ")))
}
}

View file

@ -334,6 +334,11 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"ui": func() (cli.Command, error) {
return &command.UiCommand{
Meta: meta,
}, nil
},
"validate": func() (cli.Command, error) {
return &command.ValidateCommand{
Meta: meta,

18
vendor/github.com/skratchdot/open-golang/open/exec.go generated vendored Normal file
View file

@ -0,0 +1,18 @@
// +build !windows,!darwin
package open
import (
"os/exec"
)
// http://sources.debian.net/src/xdg-utils/1.1.0~rc1%2Bgit20111210-7.1/scripts/xdg-open/
// http://sources.debian.net/src/xdg-utils/1.1.0~rc1%2Bgit20111210-7.1/scripts/xdg-mime/
func open(input string) *exec.Cmd {
return exec.Command("xdg-open", input)
}
func openWith(input string, appName string) *exec.Cmd {
return exec.Command(appName, input)
}

View file

@ -0,0 +1,15 @@
// +build darwin
package open
import (
"os/exec"
)
func open(input string) *exec.Cmd {
return exec.Command("open", input)
}
func openWith(input string, appName string) *exec.Cmd {
return exec.Command("open", "-a", appName, input)
}

View file

@ -0,0 +1,28 @@
// +build windows
package open
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
var (
cmd = "url.dll,FileProtocolHandler"
runDll32 = filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "rundll32.exe")
)
func cleaninput(input string) string {
r := strings.NewReplacer("&", "^&")
return r.Replace(input)
}
func open(input string) *exec.Cmd {
return exec.Command(runDll32, cmd, input)
}
func openWith(input string, appName string) *exec.Cmd {
return exec.Command("cmd", "/C", "start", "", appName, cleaninput(input))
}

50
vendor/github.com/skratchdot/open-golang/open/open.go generated vendored Normal file
View file

@ -0,0 +1,50 @@
/*
Open a file, directory, or URI using the OS's default
application for that object type. Optionally, you can
specify an application to use.
This is a proxy for the following commands:
OSX: "open"
Windows: "start"
Linux/Other: "xdg-open"
This is a golang port of the node.js module: https://github.com/pwnall/node-open
*/
package open
/*
Open a file, directory, or URI using the OS's default
application for that object type. Wait for the open
command to complete.
*/
func Run(input string) error {
return open(input).Run()
}
/*
Open a file, directory, or URI using the OS's default
application for that object type. Don't wait for the
open command to complete.
*/
func Start(input string) error {
return open(input).Start()
}
/*
Open a file, directory, or URI using the specified application.
Wait for the open command to complete.
*/
func RunWith(input string, appName string) error {
return openWith(input, appName).Run()
}
/*
Open a file, directory, or URI using the specified application.
Don't wait for the open command to complete.
*/
func StartWith(input string, appName string) error {
return openWith(input, appName).Start()
}

6
vendor/vendor.json vendored
View file

@ -1375,6 +1375,12 @@
"revision": "bb4de0191aa41b5507caa14b0650cdbddcd9280b",
"revisionTime": "2016-09-30T03:27:40Z"
},
{
"checksumSHA1": "h/HMhokbQHTdLUbruoBBTee+NYw=",
"path": "github.com/skratchdot/open-golang/open",
"revision": "75fb7ed4208cf72d323d7d02fd1a5964a7a9073c",
"revisionTime": "2016-03-02T14:40:31Z"
},
{
"checksumSHA1": "Q52Y7t0lEtk/wcDn5q7tS7B+jqs=",
"path": "github.com/spf13/pflag",