Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Add custom prefix devurl #74

Merged
merged 7 commits into from
Aug 4, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions ci/integration/devurls_test.go
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))

}
174 changes: 114 additions & 60 deletions cmd/coder/urls.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"text/tabwriter"
Expand All @@ -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{
Expand All @@ -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)

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: "<env> <flags>",
Desc: "list all devurls",
}
}

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}$")

// 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)
}

Expand All @@ -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
}
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())
}
}
}

Expand All @@ -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
Expand All @@ -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)
}
Copy link
Author

@Russtopia Russtopia Jul 31, 2020

Choose a reason for hiding this comment

The 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 if err != nil { exitUsage(f1) } right into those two helpers it would simplify the flow here. However, to call exitUsage(fl) within, I'd have to pass fl to both helpers which seems icky so perhaps I'll just leave these alone.


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())
}
}

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()
Expand All @@ -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)
}

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{},
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
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