This repository was archived by the owner on Aug 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 18
Add custom prefix devurl #74
Merged
Merged
Changes from 5 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
5d92778
Add custom prefix devurl
11f797a
Reverted err response
362ddc7
Enabled error response JSON body output
c71c3c9
Add urls ls subcmd; add -o [human | json] support matching users cmd
cc55a4a
Add future support for integration devurl tests
f50cfc0
Merge branch 'master' into 4411-coder-cli-named-devurls
1dc6c3b
use requireSuccess(); log.Fatal() where appropriate; change 'del' to …
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
|
||
} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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] <flags>", | ||
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") | ||
Russtopia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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) | ||
cmoog marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
cmoog marked this conversation as resolved.
Show resolved
Hide resolved
cmoog marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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: "<env> <flags>", | ||
Desc: "list all devurls", | ||
Russtopia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
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: "<env name> <port> [--access <level>]", | ||
Desc: "create/update a devurl for external access", | ||
Name: "create", | ||
Usage: "<env name> <port> [--access <level>] [--name <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}$") | ||
Russtopia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// 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,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 | ||
Russtopia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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()) | ||
Russtopia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} 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()) | ||
Russtopia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
|
||
|
@@ -117,9 +202,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,35 +223,27 @@ func (sub delSubCmd) Run(fl *pflag.FlagSet) { | |
exitUsage(fl) | ||
} | ||
|
||
if !portIsValid(port) { | ||
portNum, err := validatePort(port) | ||
if err != nil { | ||
exitUsage(fl) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's your preference on exit/fatal from 'helper' funcs? Current way here prints out usage and exits with nonzero status which is what we'd want, I guess. If I tucked in the |
||
|
||
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()) | ||
Russtopia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
func (cmd urlsCmd) Spec() cli.CommandSpec { | ||
return cli.CommandSpec{ | ||
Name: "urls", | ||
Usage: "<env name>", | ||
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() | ||
|
@@ -192,29 +270,5 @@ 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) | ||
} | ||
|
||
Russtopia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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{}, | ||
Russtopia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.