diff --git a/README.md b/README.md index 5bed407..af9c883 100644 --- a/README.md +++ b/README.md @@ -57,14 +57,18 @@ helm dashboard The command above will launch the local Web server and will open the UI in new browser tab. The command will hang waiting for you to terminate it in command-line or web UI. +You can see the list of available command-line flags by running `helm dashboard --help`. + By default, the web server is only available locally. You can change that by specifying `HD_BIND` environment variable to the desired value. For example, `0.0.0.0` would bind to all IPv4 addresses or `[::0]` would be all IPv6 addresses. -If your port 8080 is busy, you can specify a different port to use via `HD_PORT` environment variable. +If your port 8080 is busy, you can specify a different port to use via `--port ` command-line flag. -If you don't want browser tab to automatically open, set `HD_NOBROWSER=1` in your environment variables. +If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line. -If you want to increase the logging verbosity and see all the debug info, set `DEBUG=1` environment variable. +If you don't want browser tab to automatically open, add `--no-browser` flag in your command line. + +If you want to increase the logging verbosity and see all the debug info, use the `--verbose` flag. ## Scanner Integrations diff --git a/go.mod b/go.mod index 7232196..489b82f 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/gin-gonic/gin v1.8.1 github.com/hashicorp/go-version v1.6.0 github.com/hexops/gotextdiff v1.0.3 + github.com/jessevdk/go-flags v1.5.0 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/sirupsen/logrus v1.9.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index dd64460..72bf46a 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -107,6 +109,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index b60f5df..72990fe 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,9 @@ package main import ( + "fmt" "github.com/gin-gonic/gin" + "github.com/jessevdk/go-flags" "github.com/komodorio/helm-dashboard/pkg/dashboard" "github.com/pkg/browser" log "github.com/sirupsen/logrus" @@ -14,37 +16,70 @@ var ( date = "unknown" ) +type options struct { + Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"` + NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"` + Version bool `long:"version" description:"Show tool version"` + + Port uint `short:"p" long:"port" description:"Port to start server on" default:"8080"` // TODO: better default port to clash less? + + Namespace string `short:"n" long:"namespace" description:"Limit operations to a specific namespace"` +} + func main() { - setupLogging() + opts := parseFlags() - // TODO: proper command-line parsing - if len(os.Args) > 1 { // dirty thing to allow --help to work - os.Exit(0) - } + setupLogging(opts.Verbose) - address, webServerDone := dashboard.StartServer(version) + address, webServerDone := dashboard.StartServer(version, int(opts.Port), opts.Namespace, opts.Verbose) - if os.Getenv("HD_NOBROWSER") == "" { + if opts.NoBrowser { + log.Infof("Access web UI at: %s", address) + } else { log.Infof("Opening web UI: %s", address) err := browser.OpenURL(address) if err != nil { log.Warnf("Failed to open Web browser for URL: %s", err) } - } else { - log.Infof("Access web UI at: %s", address) } <-webServerDone log.Infof("Done.") } -func setupLogging() { - if os.Getenv("DEBUG") == "" { - log.SetLevel(log.InfoLevel) - gin.SetMode(gin.ReleaseMode) - } else { +func parseFlags() options { + opts := options{} + args, err := flags.Parse(&opts) + if err != nil { + if e, ok := err.(*flags.Error); ok { + if e.Type == flags.ErrHelp { + os.Exit(0) + } + } + + // we rely on default behavior to print the problem inside `flags` library + os.Exit(1) + } + + if opts.Version { + fmt.Print(version) + os.Exit(0) + } + + if len(args) > 0 { + panic("The program does not take argumants, see --help for usage") + } + return opts +} + +func setupLogging(verbose bool) { + if verbose { log.SetLevel(log.DebugLevel) gin.SetMode(gin.DebugMode) + log.Debugf("Debug logging is enabled") + } else { + log.SetLevel(log.InfoLevel) + gin.SetMode(gin.ReleaseMode) } log.Infof("Helm Dashboard by Komodor, version %s (%s @ %s)", version, commit, date) } diff --git a/pkg/dashboard/api.go b/pkg/dashboard/api.go index 6f75830..3fbd836 100644 --- a/pkg/dashboard/api.go +++ b/pkg/dashboard/api.go @@ -44,9 +44,9 @@ func contextSetter(data *subproc.DataLayer) gin.HandlerFunc { } } -func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer) *gin.Engine { +func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, debug bool) *gin.Engine { var api *gin.Engine - if os.Getenv("DEBUG") == "" { + if debug { api = gin.New() api.Use(gin.Recovery()) } else { diff --git a/pkg/dashboard/server.go b/pkg/dashboard/server.go index 2f239c8..0fb64e6 100644 --- a/pkg/dashboard/server.go +++ b/pkg/dashboard/server.go @@ -11,11 +11,14 @@ import ( log "github.com/sirupsen/logrus" "net/http" "os" + "strconv" "time" ) -func StartServer(version string) (string, utils.ControlChan) { - data := subproc.DataLayer{} +func StartServer(version string, port int, ns string, debug bool) (string, utils.ControlChan) { + data := subproc.DataLayer{ + Namespace: ns, + } err := data.CheckConnectivity() if err != nil { log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err) @@ -32,14 +35,10 @@ func StartServer(version string) (string, utils.ControlChan) { address = "localhost" } - if os.Getenv("HD_PORT") == "" { - address += ":8080" // TODO: better default port to clash less? - } else { - address += ":" + os.Getenv("HD_PORT") - } + address += ":" + strconv.Itoa(port) abort := make(utils.ControlChan) - api := NewRouter(abort, &data) + api := NewRouter(abort, &data, debug) done := startBackgroundServer(address, api, abort) return "http://" + address, done diff --git a/pkg/dashboard/subproc/data.go b/pkg/dashboard/subproc/data.go index 888ace2..17bb12d 100644 --- a/pkg/dashboard/subproc/data.go +++ b/pkg/dashboard/subproc/data.go @@ -26,6 +26,7 @@ type DataLayer struct { Kubectl string Scanners []Scanner VersionInfo *VersionInfo + Namespace string } type VersionInfo struct { @@ -34,7 +35,11 @@ type VersionInfo struct { } func (d *DataLayer) runCommand(cmd ...string) (string, error) { - log.Debugf("Starting command: %s", cmd) + for i, c := range cmd { + if c == "--namespace" && i < len(cmd) { // TODO: in case it's not found - add it? + d.forceNamespace(&cmd[i+1]) + } + } return utils.RunCommand(cmd, map[string]string{"HELM_KUBECONTEXT": d.KubeContext}) } @@ -67,6 +72,12 @@ func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) { return d.runCommand(cmd...) } +func (d *DataLayer) forceNamespace(s *string) { + if d.Namespace != "" { + *s = d.Namespace + } +} + func (d *DataLayer) CheckConnectivity() error { contexts, err := d.ListContexts() if err != nil { @@ -128,8 +139,16 @@ func (d *DataLayer) ListContexts() (res []KubeContext, err error) { } func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) { + cmd := []string{"ls", "--all", "--output", "json", "--time-format", time.RFC3339} + // TODO: filter by namespace - out, err := d.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339) + if d.Namespace == "" { + cmd = append(cmd, "--all-namespaces") + } else { + cmd = append(cmd, "--namespace", d.Namespace) + } + + out, err := d.runCommandHelm(cmd...) if err != nil { return nil, err } @@ -218,6 +237,7 @@ func enrichRepoChartsWithInstalled(charts []*RepoChartElement, installed []Relea pieces := strings.Split(chart.Name, "/") if pieces[1] == c { + // TODO: there can be more than one chart.InstalledNamespace = rel.Namespace chart.InstalledName = rel.Name } diff --git a/pkg/dashboard/utils/utils.go b/pkg/dashboard/utils/utils.go index f4840cf..a43b95d 100644 --- a/pkg/dashboard/utils/utils.go +++ b/pkg/dashboard/utils/utils.go @@ -34,7 +34,7 @@ func TempFile(txt string) (string, func(), error) { return "", nil, err } - return file.Name(), func() { os.Remove(file.Name()) }, nil + return file.Name(), func() { _ = os.Remove(file.Name()) }, nil } type CmdError struct { @@ -49,6 +49,7 @@ func (e CmdError) Error() string { } func RunCommand(cmd []string, env map[string]string) (string, error) { + log.Debugf("Starting command: %s", cmd) prog := exec.Command(cmd[0], cmd[1:]...) prog.Env = os.Environ()