From 5d92778ffc637b821ef9a883333a5db07d65d237 Mon Sep 17 00:00:00 2001 From: Russtopia Date: Thu, 30 Jul 2020 08:24:02 -0700 Subject: [PATCH 1/6] Add custom prefix devurl --- cmd/coder/urls.go | 85 ++++++++++++++++++++++-------------- go.mod | 2 +- go.sum | 6 +++ internal/entclient/devurl.go | 64 ++++++++++++++++++++++----- 4 files changed, 111 insertions(+), 46 deletions(-) diff --git a/cmd/coder/urls.go b/cmd/coder/urls.go index 7626ae93..21596055 100644 --- a/cmd/coder/urls.go +++ b/cmd/coder/urls.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "os" + "regexp" "strconv" "strings" "text/tabwriter" @@ -21,7 +22,7 @@ type urlsCmd struct{} type DevURL struct { ID string `json:"id"` URL string `json:"url"` - Port string `json:"port"` + Port int `json:"port"` Access string `json:"access"` } @@ -33,55 +34,68 @@ var urlAccessLevel = map[string]string{ "PUBLIC": "Anyone on the internet can access this link", } -func portIsValid(port string) bool { +func validatePort(port string) (int, error) { p, err := strconv.ParseUint(port, 10, 16) + if err != nil { + flog.Error("Invalid port") + return 0, err + } if p < 1 { // port 0 means 'any free port', which we don't support err = strconv.ErrRange + flog.Error("Port must be > 0") + return 0, err } - if err != nil { - fmt.Println("Invalid port") - } - return err == nil + return int(p), nil } func accessLevelIsValid(level string) bool { _, ok := urlAccessLevel[level] if !ok { - fmt.Println("Invalid access level") + flog.Error("Invalid access level") } return ok } type createSubCmd struct { - access string + access string + urlname string } func (sub *createSubCmd) RegisterFlags(fl *pflag.FlagSet) { fl.StringVarP(&sub.access, "access", "a", "private", "[private | org | authed | public] set devurl access") + fl.StringVarP(&sub.urlname, "name", "n", "", "devurl name") } func (sub createSubCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ - Name: "create", - Usage: " [--access ]", - Desc: "create/update a devurl for external access", + Name: "create", + Usage: " [--access ] [--name ]", + Aliases: []string{"edit"}, + Desc: "create or update a devurl for external access", } } +// devURLNameValidRx is the regex used to validate devurl names specified +// via the --name subcommand. Named devurls must begin with a letter, and +// consist solely of letters and digits, with a max length of 64 chars. +var devURLNameValidRx = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]{0,63}$") + // Run creates or updates a devURL, specified by env ID and port // (fl.Arg(0) and fl.Arg(1)), with access level (fl.Arg(2)) on // the cemanager. func (sub createSubCmd) Run(fl *pflag.FlagSet) { envName := fl.Arg(0) port := fl.Arg(1) - access := fl.Arg(2) + name := fl.Arg(2) + access := fl.Arg(3) if envName == "" { exitUsage(fl) } - if !portIsValid(port) { + portNum, err := validatePort(port) + if err != nil { exitUsage(fl) } @@ -90,20 +104,28 @@ func (sub createSubCmd) Run(fl *pflag.FlagSet) { exitUsage(fl) } + name = sub.urlname + if name != "" && !devURLNameValidRx.MatchString(name) { + flog.Error("update devurl: name must be < 64 chars in length, begin with a letter and only contain letters or digits.") + return + } entClient := requireAuth() env := findEnv(entClient, envName) - _, found := devURLID(port, urlList(envName)) + urlID, found := devURLID(portNum, urlList(envName)) if found { - fmt.Printf("Updating devurl for port %v\n", port) + flog.Info("Updating devurl for port %v", port) + err := entClient.UpdateDevURL(env.ID, urlID, portNum, name, access) + if err != nil { + flog.Error("update devurl: %s", err.Error()) + } } else { - fmt.Printf("Adding devurl for port %v\n", port) - } - - err := entClient.UpsertDevURL(env.ID, port, access) - if err != nil { - flog.Error("upsert devurl: %s", err.Error()) + flog.Info("Adding devurl for port %v", port) + err := entClient.InsertDevURL(env.ID, portNum, name, access) + if err != nil { + flog.Error("insert devurl: %s", err.Error()) + } } } @@ -117,9 +139,10 @@ func (sub delSubCmd) Spec() cli.CommandSpec { } } -// devURLID returns the ID of a devURL, given the env name and port. +// devURLID returns the ID of a devURL, given the env name and port +// from a list of DevURL records. // ("", false) is returned if no match is found. -func devURLID(port string, urls []DevURL) (string, bool) { +func devURLID(port int, urls []DevURL) (string, bool) { for _, url := range urls { if url.Port == port { return url.ID, true @@ -137,22 +160,22 @@ func (sub delSubCmd) Run(fl *pflag.FlagSet) { exitUsage(fl) } - if !portIsValid(port) { + portNum, err := validatePort(port) + if err != nil { exitUsage(fl) } entClient := requireAuth() - env := findEnv(entClient, envName) - urlID, found := devURLID(port, urlList(envName)) + urlID, found := devURLID(portNum, urlList(envName)) if found { - fmt.Printf("Deleting devurl for port %v\n", port) + flog.Info("Deleting devurl for port %v", port) } else { flog.Fatal("No devurl found for port %v", port) } - err := entClient.DelDevURL(env.ID, urlID) + err = entClient.DelDevURL(env.ID, urlID) if err != nil { flog.Error("delete devurl: %s", err.Error()) } @@ -192,10 +215,6 @@ func urlList(envName string) []DevURL { flog.Fatal("%v", err) } - if len(devURLs) == 0 { - fmt.Printf("no dev urls were found for environment: %s\n", envName) - } - return devURLs } @@ -207,7 +226,7 @@ func (cmd urlsCmd) Run(fl *pflag.FlagSet) { w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) for _, devURL := range devURLs { - fmt.Fprintf(w, "%s\t%s\t%s\n", devURL.URL, devURL.Port, devURL.Access) + fmt.Fprintf(w, "%s\t%d\t%s\n", devURL.URL, devURL.Port, devURL.Access) } w.Flush() } diff --git a/go.mod b/go.mod index 4f496165..71f90aa2 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/pflag v1.0.5 - go.coder.com/cli v0.4.0 + go.coder.com/cli v0.5.0 go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 golang.org/x/crypto v0.0.0-20200422194213-44a606286825 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a diff --git a/go.sum b/go.sum index 37756a28..cc2cac0b 100644 --- a/go.sum +++ b/go.sum @@ -163,6 +163,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= @@ -171,6 +173,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= go.coder.com/cli v0.4.0 h1:PruDGwm/CPFndyK/eMowZG3vzg5CgohRWeXWCTr3zi8= go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= +go.coder.com/cli v0.5.0 h1:7W9ECtZdVKaAc0Oe2uk5J/c0LCtsWufQz4NeX6YwP0g= +go.coder.com/cli v0.5.0/go.mod h1:h6091Eox0VdgJw2CDBvTyx7SnhduTm8qYM2bR2pewls= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 h1:DjCS6dRQh+1PlfiBmnabxfdrzenb0tAwJqFxDEH/s9g= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -300,6 +304,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/entclient/devurl.go b/internal/entclient/devurl.go index 9260dcce..e181bec4 100644 --- a/internal/entclient/devurl.go +++ b/internal/entclient/devurl.go @@ -3,16 +3,23 @@ package entclient import ( "fmt" "net/http" + + "golang.org/x/xerrors" ) +type delDevURLRequest struct { + EnvID string `json:"environment_id"` + DevURLID string `json:"url_id"` +} + // DelDevURL deletes the specified devurl func (c Client) DelDevURL(envID, urlID string) error { reqString := "/api/environments/%s/devurls/%s" reqURL := fmt.Sprintf(reqString, envID, urlID) - res, err := c.request("DELETE", reqURL, map[string]string{ - "environment_id": envID, - "url_id": urlID, + res, err := c.request("DELETE", reqURL, delDevURLRequest{ + EnvID: envID, + DevURLID: urlID, }) if err != nil { return err @@ -25,23 +32,56 @@ func (c Client) DelDevURL(envID, urlID string) error { return nil } -// UpsertDevURL upserts the specified devurl for the authenticated user -func (c Client) UpsertDevURL(envID, port, access string) error { +type createDevURLRequest struct { + EnvID string `json:"environment_id"` + Port int `json:"port"` + Access string `json:"access"` + Name string `json:"name"` +} + +// InsertDevURL inserts a new devurl for the authenticated user +func (c Client) InsertDevURL(envID string, port int, name, access string) error { reqString := "/api/environments/%s/devurls" reqURL := fmt.Sprintf(reqString, envID) - res, err := c.request("POST", reqURL, map[string]string{ - "environment_id": envID, - "port": port, - "access": access, + res, err := c.request("POST", reqURL, createDevURLRequest{ + EnvID: envID, + Port: port, + Access: access, + Name: name, }) + if res != nil && res.StatusCode == http.StatusConflict { + return xerrors.Errorf("Failed to create devurl. Check that the port and name are unique.") + } if err != nil { return err } + return nil +} - if res.StatusCode != http.StatusOK { - return bodyError(res) - } +type updateDevURLRequest struct { + EnvID string `json:"environment_id"` + Port int `json:"port"` + Access string `json:"access"` + Name string `json:"name"` +} + +// UpdateDevURL updates an existing devurl for the authenticated user +func (c Client) UpdateDevURL(envID, urlID string, port int, name, access string) error { + reqString := "/api/environments/%s/devurls/%s" + reqURL := fmt.Sprintf(reqString, envID, urlID) + res, err := c.request("PUT", reqURL, updateDevURLRequest{ + EnvID: envID, + Port: port, + Access: access, + Name: name, + }) + if res != nil && res.StatusCode == http.StatusConflict { + return xerrors.Errorf("Failed to update devurl. Check that the port and name are unique.") + } + if err != nil { + return err + } return nil } From 11f797a7f7a5124aaae913b07236f3775e44a5da Mon Sep 17 00:00:00 2001 From: Russtopia Date: Thu, 30 Jul 2020 08:48:24 -0700 Subject: [PATCH 2/6] Reverted err response --- internal/entclient/devurl.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/entclient/devurl.go b/internal/entclient/devurl.go index e181bec4..ee7ecce7 100644 --- a/internal/entclient/devurl.go +++ b/internal/entclient/devurl.go @@ -3,8 +3,6 @@ package entclient import ( "fmt" "net/http" - - "golang.org/x/xerrors" ) type delDevURLRequest struct { @@ -50,12 +48,14 @@ func (c Client) InsertDevURL(envID string, port int, name, access string) error Access: access, Name: name, }) - if res != nil && res.StatusCode == http.StatusConflict { - return xerrors.Errorf("Failed to create devurl. Check that the port and name are unique.") - } if err != nil { return err } + + if res.StatusCode != http.StatusOK { + return bodyError(res) + } + return nil } @@ -77,11 +77,13 @@ func (c Client) UpdateDevURL(envID, urlID string, port int, name, access string) Access: access, Name: name, }) - if res != nil && res.StatusCode == http.StatusConflict { - return xerrors.Errorf("Failed to update devurl. Check that the port and name are unique.") - } if err != nil { return err } + + if res.StatusCode != http.StatusOK { + return bodyError(res) + } + return nil } From 362ddc7cab9256c9574b61d82fe8b7c165dcf90a Mon Sep 17 00:00:00 2001 From: Russtopia Date: Thu, 30 Jul 2020 10:02:20 -0700 Subject: [PATCH 3/6] Enabled error response JSON body output --- internal/entclient/devurl.go | 3 +++ internal/entclient/error.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/entclient/devurl.go b/internal/entclient/devurl.go index ee7ecce7..65febfff 100644 --- a/internal/entclient/devurl.go +++ b/internal/entclient/devurl.go @@ -22,6 +22,7 @@ func (c Client) DelDevURL(envID, urlID string) error { if err != nil { return err } + defer res.Body.Close() if res.StatusCode != http.StatusOK { return bodyError(res) @@ -51,6 +52,7 @@ func (c Client) InsertDevURL(envID string, port int, name, access string) error if err != nil { return err } + defer res.Body.Close() if res.StatusCode != http.StatusOK { return bodyError(res) @@ -80,6 +82,7 @@ func (c Client) UpdateDevURL(envID, urlID string, port int, name, access string) if err != nil { return err } + defer res.Body.Close() if res.StatusCode != http.StatusOK { return bodyError(res) diff --git a/internal/entclient/error.go b/internal/entclient/error.go index 49f58669..25b31fde 100644 --- a/internal/entclient/error.go +++ b/internal/entclient/error.go @@ -8,7 +8,7 @@ import ( ) func bodyError(resp *http.Response) error { - byt, err := httputil.DumpResponse(resp, false) + byt, err := httputil.DumpResponse(resp, true) if err != nil { return xerrors.Errorf("dump response: %w", err) } From c71c3c9f5c645e97241d8a6ff953ea2db569f2b8 Mon Sep 17 00:00:00 2001 From: Russtopia Date: Thu, 30 Jul 2020 14:17:56 -0700 Subject: [PATCH 4/6] Add urls ls subcmd; add -o [human | json] support matching users cmd --- cmd/coder/urls.go | 90 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/cmd/coder/urls.go b/cmd/coder/urls.go index 21596055..8ebc9e58 100644 --- a/cmd/coder/urls.go +++ b/cmd/coder/urls.go @@ -18,6 +18,26 @@ import ( type urlsCmd struct{} +func (cmd *urlsCmd) Subcommands() []cli.Command { + return []cli.Command{ + &listSubCmd{}, + &createSubCmd{}, + &delSubCmd{}, + } +} + +func (cmd urlsCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "urls", + Usage: "[subcommand] ", + Desc: "interact with environment devurls", + } +} + +func (cmd urlsCmd) Run(fl *pflag.FlagSet) { + exitUsage(fl) +} + // DevURL is the parsed json response record for a devURL from cemanager type DevURL struct { ID string `json:"id"` @@ -57,6 +77,48 @@ func accessLevelIsValid(level string) bool { return ok } +type listSubCmd struct { + outputFmt string +} + +// Run gets the list of active devURLs from the cemanager for the +// specified environment and outputs info to stdout. +func (sub listSubCmd) Run(fl *pflag.FlagSet) { + envName := fl.Arg(0) + devURLs := urlList(envName) + + switch sub.outputFmt { + case "human": + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) + for _, devURL := range devURLs { + fmt.Fprintf(w, "%s\t%d\t%s\n", devURL.URL, devURL.Port, devURL.Access) + } + err := w.Flush() + if err != nil { + flog.Fatal("failed to flush writer: %v", err) + } + case "json": + err := json.NewEncoder(os.Stdout).Encode(devURLs) + if err != nil { + flog.Fatal("failed to encode devurls to json: %v", err) + } + default: + exitUsage(fl) + } +} + +func (sub *listSubCmd) RegisterFlags(fl *pflag.FlagSet) { + fl.StringVarP(&sub.outputFmt, "output", "o", "human", "output format (human | json)") +} + +func (sub *listSubCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "ls", + Usage: " ", + Desc: "list all devurls", + } +} + type createSubCmd struct { access string urlname string @@ -181,14 +243,6 @@ func (sub delSubCmd) Run(fl *pflag.FlagSet) { } } -func (cmd urlsCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "urls", - Usage: "", - Desc: "get all development urls for external access", - } -} - // urlList returns the list of active devURLs from the cemanager. func urlList(envName string) []DevURL { entClient := requireAuth() @@ -217,23 +271,3 @@ func urlList(envName string) []DevURL { return devURLs } - -// Run gets the list of active devURLs from the cemanager for the -// specified environment and outputs info to stdout. -func (cmd urlsCmd) Run(fl *pflag.FlagSet) { - envName := fl.Arg(0) - devURLs := urlList(envName) - - w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) - for _, devURL := range devURLs { - fmt.Fprintf(w, "%s\t%d\t%s\n", devURL.URL, devURL.Port, devURL.Access) - } - w.Flush() -} - -func (cmd *urlsCmd) Subcommands() []cli.Command { - return []cli.Command{ - &createSubCmd{}, - &delSubCmd{}, - } -} From cc55a4aa905c50facd2ecdf777c47194f1c640e0 Mon Sep 17 00:00:00 2001 From: Russtopia Date: Thu, 30 Jul 2020 15:04:07 -0700 Subject: [PATCH 5/6] Add future support for integration devurl tests --- ci/integration/devurls_test.go | 94 ++++++++++++++++++++++++++++++++++ cmd/coder/urls.go | 1 + internal/entclient/devurl.go | 16 +++--- 3 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 ci/integration/devurls_test.go diff --git a/ci/integration/devurls_test.go b/ci/integration/devurls_test.go new file mode 100644 index 00000000..558a305d --- /dev/null +++ b/ci/integration/devurls_test.go @@ -0,0 +1,94 @@ +package integration + +import ( + "context" + "testing" + "time" + + "cdr.dev/coder-cli/ci/tcli" + "cdr.dev/slog/sloggers/slogtest/assert" +) + +func TestDevURLCLI(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() + + c, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ + Image: "codercom/enterprise-dev", + Name: "coder-cli-devurl-tests", + BindMounts: map[string]string{ + binpath: "/bin/coder", + }, + }) + assert.Success(t, "new run container", err) + defer c.Close() + + c.Run(ctx, "which coder").Assert(t, + tcli.Success(), + tcli.StdoutMatches("/usr/sbin/coder"), + tcli.StderrEmpty(), + ) + + c.Run(ctx, "coder urls ls").Assert(t, + tcli.Error(), + ) + + // The following cannot be enabled nor verified until either the + // integration testing dogfood target has environments created, or + // we implement the 'env create' command for coder-cli to create our + // own here. + + // If we were to create an env ourselves ... we could test devurls something like + + // // == Login + // headlessLogin(ctx, t, c) + + // // == urls ls should fail w/o supplying an envname + // c.Run(ctx, "coder urls ls").Assert(t, + // tcli.Error(), + // ) + + // // == env creation should succeed + // c.Run(ctx, "coder envs create env1 --from image1 --cores 1 --ram 2gb --disk 10gb --nogpu").Assert(t, + // tcli.Success()) + + // // == urls ls should succeed for a newly-created environment + // var durl entclient.DevURL + // c.Run(ctx, `coder urls ls -o json`).Assert(t, + // tcli.Success(), + // jsonUnmarshals(&durl), // though if a new env, durl should be empty + // ) + + // // == devurl creation w/default PRIVATE access + // c.Run(ctx, `coder urls create env1 3000`).Assert(t, + // tcli.Success()) + + // // == devurl create w/access == AUTHED + // c.Run(ctx, `coder urls create env1 3001 --access=AUTHED`).Assert(t, + // tcli.Success()) + + // // == devurl create with name + // c.Run(ctx, `coder urls create env1 3002 --access=PUBLIC --name=foobar`).Assert(t, + // tcli.Success()) + + // // == devurl ls should return well-formed entries incl. one with AUTHED access + // c.Run(ctx, `coder urls ls env1 -o json | jq -c '.[] | select( .access == "AUTHED")'`).Assert(t, + // tcli.Success(), + // jsonUnmarshals(&durl)) + + // // == devurl ls should return well-formed entries incl. one with name 'foobar' + // c.Run(ctx, `coder urls ls env1 -o json | jq -c '.[] | select( .name == "foobar")'`).Assert(t, + // tcli.Success(), + // jsonUnmarshals(&durl)) + + // // == devurl del should function + // c.Run(ctx, `coder urls del env1 3002`).Assert(t, + // tcli.Success()) + + // // == removed devurl should no longer be there + // c.Run(ctx, `coder urls ls env1 -o json | jq -c '.[] | select( .name == "foobar")'`).Assert(t, + // tcli.Error(), + // jsonUnmarshals(&durl)) + +} diff --git a/cmd/coder/urls.go b/cmd/coder/urls.go index 8ebc9e58..7f06e9e0 100644 --- a/cmd/coder/urls.go +++ b/cmd/coder/urls.go @@ -44,6 +44,7 @@ type DevURL struct { URL string `json:"url"` Port int `json:"port"` Access string `json:"access"` + Name string `json:"name"` } var urlAccessLevel = map[string]string{ diff --git a/internal/entclient/devurl.go b/internal/entclient/devurl.go index 65febfff..b2d34025 100644 --- a/internal/entclient/devurl.go +++ b/internal/entclient/devurl.go @@ -5,6 +5,15 @@ import ( "net/http" ) +// DevURL is the parsed json response record for a devURL from cemanager +type DevURL struct { + ID string `json:"id"` + URL string `json:"url"` + Port int `json:"port"` + Access string `json:"access"` + Name string `json:"name"` +} + type delDevURLRequest struct { EnvID string `json:"environment_id"` DevURLID string `json:"url_id"` @@ -61,12 +70,7 @@ func (c Client) InsertDevURL(envID string, port int, name, access string) error return nil } -type updateDevURLRequest struct { - EnvID string `json:"environment_id"` - Port int `json:"port"` - Access string `json:"access"` - Name string `json:"name"` -} +type updateDevURLRequest createDevURLRequest // UpdateDevURL updates an existing devurl for the authenticated user func (c Client) UpdateDevURL(envID, urlID string, port int, name, access string) error { From 1dc6c3ba86ee57a3036267aa0e3552c0eabebb81 Mon Sep 17 00:00:00 2001 From: Russtopia Date: Fri, 31 Jul 2020 10:51:35 -0700 Subject: [PATCH 6/6] use requireSuccess(); log.Fatal() where appropriate; change 'del' to 'rm' --- ci/integration/devurls_test.go | 4 ++-- cmd/coder/urls.go | 40 +++++++++++++--------------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/ci/integration/devurls_test.go b/ci/integration/devurls_test.go index 558a305d..09f1c0e4 100644 --- a/ci/integration/devurls_test.go +++ b/ci/integration/devurls_test.go @@ -82,8 +82,8 @@ func TestDevURLCLI(t *testing.T) { // tcli.Success(), // jsonUnmarshals(&durl)) - // // == devurl del should function - // c.Run(ctx, `coder urls del env1 3002`).Assert(t, + // // == devurl rm should function + // c.Run(ctx, `coder urls rm env1 3002`).Assert(t, // tcli.Success()) // // == removed devurl should no longer be there diff --git a/cmd/coder/urls.go b/cmd/coder/urls.go index 7f06e9e0..582fbdb1 100644 --- a/cmd/coder/urls.go +++ b/cmd/coder/urls.go @@ -88,6 +88,10 @@ func (sub listSubCmd) Run(fl *pflag.FlagSet) { envName := fl.Arg(0) devURLs := urlList(envName) + if len(devURLs) == 0 { + return + } + switch sub.outputFmt { case "human": w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) @@ -95,14 +99,10 @@ func (sub listSubCmd) Run(fl *pflag.FlagSet) { fmt.Fprintf(w, "%s\t%d\t%s\n", devURL.URL, devURL.Port, devURL.Access) } err := w.Flush() - if err != nil { - flog.Fatal("failed to flush writer: %v", err) - } + requireSuccess(err, "failed to flush writer: %v", err) case "json": err := json.NewEncoder(os.Stdout).Encode(devURLs) - if err != nil { - flog.Fatal("failed to encode devurls to json: %v", err) - } + requireSuccess(err, "failed to encode devurls to json: %v", err) default: exitUsage(fl) } @@ -116,7 +116,7 @@ func (sub *listSubCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ Name: "ls", Usage: " ", - Desc: "list all devurls", + Desc: "list all devurls for a given environment", } } @@ -169,7 +169,7 @@ func (sub createSubCmd) Run(fl *pflag.FlagSet) { name = sub.urlname if name != "" && !devURLNameValidRx.MatchString(name) { - flog.Error("update devurl: name must be < 64 chars in length, begin with a letter and only contain letters or digits.") + flog.Fatal("update devurl: name must be < 64 chars in length, begin with a letter and only contain letters or digits.") return } entClient := requireAuth() @@ -180,15 +180,11 @@ func (sub createSubCmd) Run(fl *pflag.FlagSet) { if found { flog.Info("Updating devurl for port %v", port) err := entClient.UpdateDevURL(env.ID, urlID, portNum, name, access) - if err != nil { - flog.Error("update devurl: %s", err.Error()) - } + requireSuccess(err, "update devurl: %s", err) } else { flog.Info("Adding devurl for port %v", port) err := entClient.InsertDevURL(env.ID, portNum, name, access) - if err != nil { - flog.Error("insert devurl: %s", err.Error()) - } + requireSuccess(err, "insert devurl: %s", err) } } @@ -196,9 +192,9 @@ type delSubCmd struct{} func (sub delSubCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ - Name: "del", + Name: "rm", Usage: " ", - Desc: "delete a devurl", + Desc: "remove a devurl", } } @@ -239,9 +235,7 @@ func (sub delSubCmd) Run(fl *pflag.FlagSet) { } err = entClient.DelDevURL(env.ID, urlID) - if err != nil { - flog.Error("delete devurl: %s", err.Error()) - } + requireSuccess(err, "delete devurl: %s", err) } // urlList returns the list of active devURLs from the cemanager. @@ -253,9 +247,7 @@ func urlList(envName string) []DevURL { reqURL := fmt.Sprintf(reqString, entClient.BaseURL, env.ID, entClient.Token) resp, err := http.Get(reqURL) - if err != nil { - flog.Fatal("%v", err) - } + requireSuccess(err, "%v", err) defer resp.Body.Close() if resp.StatusCode != 200 { @@ -266,9 +258,7 @@ func urlList(envName string) []DevURL { devURLs := make([]DevURL, 0) err = dec.Decode(&devURLs) - if err != nil { - flog.Fatal("%v", err) - } + requireSuccess(err, "%v", err) return devURLs }