diff --git a/ci/integration/devurls_test.go b/ci/integration/devurls_test.go new file mode 100644 index 00000000..09f1c0e4 --- /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 rm should function + // c.Run(ctx, `coder urls rm 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 7626ae93..582fbdb1 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" @@ -17,12 +18,33 @@ 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"` URL string `json:"url"` - Port string `json:"port"` + Port int `json:"port"` Access string `json:"access"` + Name string `json:"name"` } var urlAccessLevel = map[string]string{ @@ -33,55 +55,110 @@ 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 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) + + if len(devURLs) == 0 { + return + } + + 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() + requireSuccess(err, "failed to flush writer: %v", err) + case "json": + err := json.NewEncoder(os.Stdout).Encode(devURLs) + requireSuccess(err, "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 for a given environment", + } +} + 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 +167,24 @@ func (sub createSubCmd) Run(fl *pflag.FlagSet) { exitUsage(fl) } + name = sub.urlname + if name != "" && !devURLNameValidRx.MatchString(name) { + flog.Fatal("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) + requireSuccess(err, "update devurl: %s", err) } 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) + requireSuccess(err, "insert devurl: %s", err) } } @@ -111,15 +192,16 @@ 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", } } -// 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,33 +219,23 @@ 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) - if err != nil { - flog.Error("delete devurl: %s", err.Error()) - } -} - -func (cmd urlsCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "urls", - Usage: "", - Desc: "get all development urls for external access", - } + err = entClient.DelDevURL(env.ID, urlID) + requireSuccess(err, "delete devurl: %s", err) } // urlList returns the list of active devURLs from the cemanager. @@ -175,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 { @@ -188,33 +258,7 @@ func urlList(envName string) []DevURL { devURLs := make([]DevURL, 0) err = dec.Decode(&devURLs) - if err != nil { - flog.Fatal("%v", err) - } - - if len(devURLs) == 0 { - fmt.Printf("no dev urls were found for environment: %s\n", envName) - } + requireSuccess(err, "%v", err) 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%s\t%s\n", devURL.URL, devURL.Port, devURL.Access) - } - w.Flush() -} - -func (cmd *urlsCmd) Subcommands() []cli.Command { - return []cli.Command{ - &createSubCmd{}, - &delSubCmd{}, - } -} 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..b2d34025 100644 --- a/internal/entclient/devurl.go +++ b/internal/entclient/devurl.go @@ -5,18 +5,33 @@ 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"` +} + // 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 } + defer res.Body.Close() if res.StatusCode != http.StatusOK { return bodyError(res) @@ -25,19 +40,53 @@ 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 err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return bodyError(res) + } + + return nil +} + +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 { + 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 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 877085f2..9170efa9 100644 --- a/internal/entclient/error.go +++ b/internal/entclient/error.go @@ -18,7 +18,7 @@ type apiError struct { } 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) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy