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

Commit 4f09af1

Browse files
author
Russtopia
committed
Add subcommands 'create' and 'del' to manage devurls
1 parent 6450687 commit 4f09af1

File tree

5 files changed

+221
-22
lines changed

5 files changed

+221
-22
lines changed

Diff for: cmd/coder/sync.go

+2-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package main
22

33
import (
4-
"errors"
5-
"fmt"
64
"os"
75
"path/filepath"
86
"strings"
@@ -31,10 +29,6 @@ func (cmd *syncCmd) RegisterFlags(fl *pflag.FlagSet) {
3129
fl.BoolVarP(&cmd.init, "init", "i", false, "do initial transfer and exit")
3230
}
3331

34-
// See https://lxadm.com/Rsync_exit_codes#List_of_standard_rsync_exit_codes.
35-
var IncompatRsync = errors.New("rsync: exit status 2")
36-
var StreamErrRsync = errors.New("rsync: exit status 12")
37-
3832
func (cmd *syncCmd) Run(fl *pflag.FlagSet) {
3933
var (
4034
local = fl.Arg(0)
@@ -81,11 +75,7 @@ func (cmd *syncCmd) Run(fl *pflag.FlagSet) {
8175
err = s.Run()
8276
}
8377

84-
if fmt.Sprintf("%v", err) == fmt.Sprintf("%v", IncompatRsync) {
85-
flog.Fatal("no compatible rsync present on remote machine")
86-
} else if fmt.Sprintf("%v", err) == fmt.Sprintf("%v", StreamErrRsync) {
87-
flog.Fatal("error in rsync protocol datastream (no installed remote rsync?)")
88-
} else {
89-
flog.Fatal("sync: %v", err)
78+
if err != nil {
79+
flog.Fatal("%v", err)
9080
}
9181
}

Diff for: cmd/coder/urls.go

+156-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"net/http"
77
"os"
8+
"strconv"
9+
"strings"
810
"text/tabwriter"
911

1012
"github.com/spf13/pflag"
@@ -15,35 +17,164 @@ import (
1517

1618
type urlsCmd struct{}
1719

20+
// DevURL is the parsed json response record for a devURL from cemanager
1821
type DevURL struct {
22+
ID string `json:"id"`
1923
URL string `json:"url"`
2024
Port string `json:"port"`
2125
Access string `json:"access"`
2226
}
2327

24-
func (cmd urlsCmd) Spec() cli.CommandSpec {
28+
var urlAccessLevel = map[string]string{
29+
//Remote API endpoint requires these in uppercase
30+
"PRIVATE": "Only you can access",
31+
"ORG": "All members of your organization can access",
32+
"AUTHED": "Authenticated users can access",
33+
"PUBLIC": "Anyone on the internet can access this link",
34+
}
35+
36+
func portIsValid(port string) bool {
37+
p, err := strconv.ParseUint(port, 10, 16)
38+
if p < 1 {
39+
// port 0 means 'any free port', which we don't support
40+
err = strconv.ErrRange
41+
}
42+
if err != nil {
43+
fmt.Println("Invalid port")
44+
}
45+
return err == nil
46+
}
47+
48+
func accessLevelIsValid(level string) bool {
49+
_, ok := urlAccessLevel[level]
50+
if !ok {
51+
fmt.Println("Invalid access level")
52+
}
53+
return ok
54+
}
55+
56+
type createSubCmd struct {
57+
access string
58+
}
59+
60+
func (sub *createSubCmd) RegisterFlags(fl *pflag.FlagSet) {
61+
fl.StringVarP(&sub.access, "access", "a", "private", "[private | org | authed | public] set devurl access")
62+
}
63+
64+
func (sub createSubCmd) Spec() cli.CommandSpec {
2565
return cli.CommandSpec{
26-
Name: "urls",
27-
Usage: "<env name>",
28-
Desc: "get all development urls for external access",
66+
Name: "create",
67+
Usage: "<env name> <port> [--access <level>]",
68+
Desc: "create/update a devurl for external access",
2969
}
3070
}
3171

32-
func (cmd urlsCmd) Run(fl *pflag.FlagSet) {
33-
var envName = fl.Arg(0)
72+
// Run creates or updates a devURL, specified by env ID and port
73+
// (fl.Arg(0) and fl.Arg(1)), with access level (fl.Arg(2)) on
74+
// the cemanager.
75+
func (sub createSubCmd) Run(fl *pflag.FlagSet) {
76+
envName := fl.Arg(0)
77+
port := fl.Arg(1)
78+
access := fl.Arg(2)
3479

3580
if envName == "" {
3681
exitUsage(fl)
3782
}
3883

84+
if !portIsValid(port) {
85+
exitUsage(fl)
86+
}
87+
88+
access = strings.ToUpper(sub.access)
89+
if !accessLevelIsValid(access) {
90+
exitUsage(fl)
91+
}
92+
3993
entClient := requireAuth()
4094

4195
env := findEnv(entClient, envName)
4296

97+
_, found := devURLID(port, urlList(envName))
98+
if found {
99+
fmt.Printf("Updating devurl for port %v\n", port)
100+
} else {
101+
fmt.Printf("Adding devurl for port %v\n", port)
102+
}
103+
104+
err := entClient.UpsertDevURL(env.ID, port, access)
105+
if err != nil {
106+
flog.Error("upsert devurl: %s", err.Error())
107+
}
108+
}
109+
110+
type delSubCmd struct{}
111+
112+
func (sub delSubCmd) Spec() cli.CommandSpec {
113+
return cli.CommandSpec{
114+
Name: "del",
115+
Usage: "<env name> <port>",
116+
Desc: "delete a devurl",
117+
}
118+
}
119+
120+
// devURLID returns the ID of a devURL, given the env name and port.
121+
// ("", false) is returned if no match is found.
122+
func devURLID(port string, urls []DevURL) (string, bool) {
123+
for _, url := range urls {
124+
if url.Port == port {
125+
return url.ID, true
126+
}
127+
}
128+
return "", false
129+
}
130+
131+
// Run deletes a devURL, specified by env ID and port, from the cemanager.
132+
func (sub delSubCmd) Run(fl *pflag.FlagSet) {
133+
envName := fl.Arg(0)
134+
port := fl.Arg(1)
135+
136+
if envName == "" {
137+
exitUsage(fl)
138+
}
139+
140+
if !portIsValid(port) {
141+
exitUsage(fl)
142+
}
143+
144+
entClient := requireAuth()
145+
146+
env := findEnv(entClient, envName)
147+
148+
urlID, found := devURLID(port, urlList(envName))
149+
if found {
150+
fmt.Printf("Deleting devurl for port %v\n", port)
151+
} else {
152+
flog.Fatal("No devurl found for port %v", port)
153+
}
154+
155+
err := entClient.DelDevURL(env.ID, urlID)
156+
if err != nil {
157+
flog.Error("delete devurl: %s", err.Error())
158+
}
159+
}
160+
161+
func (cmd urlsCmd) Spec() cli.CommandSpec {
162+
return cli.CommandSpec{
163+
Name: "urls",
164+
Usage: "<env name>",
165+
Desc: "get all development urls for external access",
166+
}
167+
}
168+
169+
// urlList returns the list of active devURLs from the cemanager.
170+
func urlList(envName string) []DevURL {
171+
entClient := requireAuth()
172+
env := findEnv(entClient, envName)
173+
43174
reqString := "%s/api/environments/%s/devurls?session_token=%s"
44-
reqUrl := fmt.Sprintf(reqString, entClient.BaseURL, env.ID, entClient.Token)
175+
reqURL := fmt.Sprintf(reqString, entClient.BaseURL, env.ID, entClient.Token)
45176

46-
resp, err := http.Get(reqUrl)
177+
resp, err := http.Get(reqURL)
47178
if err != nil {
48179
flog.Fatal("%v", err)
49180
}
@@ -55,7 +186,7 @@ func (cmd urlsCmd) Run(fl *pflag.FlagSet) {
55186

56187
dec := json.NewDecoder(resp.Body)
57188

58-
var devURLs = make([]DevURL, 0)
189+
devURLs := make([]DevURL, 0)
59190
err = dec.Decode(&devURLs)
60191
if err != nil {
61192
flog.Fatal("%v", err)
@@ -65,9 +196,25 @@ func (cmd urlsCmd) Run(fl *pflag.FlagSet) {
65196
fmt.Printf("no dev urls were found for environment: %s\n", envName)
66197
}
67198

199+
return devURLs
200+
}
201+
202+
// Run gets the list of active devURLs from the cemanager for the
203+
// specified environment and outputs info to stdout.
204+
func (cmd urlsCmd) Run(fl *pflag.FlagSet) {
205+
envName := fl.Arg(0)
206+
devURLs := urlList(envName)
207+
68208
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent)
69209
for _, devURL := range devURLs {
70210
fmt.Fprintf(w, "%s\t%s\t%s\n", devURL.URL, devURL.Port, devURL.Access)
71211
}
72212
w.Flush()
73213
}
214+
215+
func (cmd *urlsCmd) Subcommands() []cli.Command {
216+
return []cli.Command{
217+
&createSubCmd{},
218+
&delSubCmd{},
219+
}
220+
}

Diff for: internal/entclient/activity.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package entclient
22

3-
import "net/http"
3+
import (
4+
"net/http"
5+
)
46

57
func (c Client) PushActivity(source string, envID string) error {
68
res, err := c.request("POST", "/api/metrics/usage/push", map[string]string{

Diff for: internal/entclient/devurl.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package entclient
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
)
7+
8+
func (c Client) DelDevURL(envID, urlID string) error {
9+
reqString := "/api/environments/%s/devurls/%s"
10+
reqUrl := fmt.Sprintf(reqString, envID, urlID)
11+
12+
res, err := c.request("DELETE", reqUrl, map[string]string{
13+
"environment_id": envID,
14+
"url_id": urlID,
15+
})
16+
if err != nil {
17+
return err
18+
}
19+
20+
if res.StatusCode != http.StatusOK {
21+
return bodyError(res)
22+
}
23+
24+
return nil
25+
}
26+
27+
func (c Client) UpsertDevURL(envID, port, access string) error {
28+
reqString := "/api/environments/%s/devurls"
29+
reqUrl := fmt.Sprintf(reqString, envID)
30+
31+
res, err := c.request("POST", reqUrl, map[string]string{
32+
"environment_id": envID,
33+
"port": port,
34+
"access": access,
35+
})
36+
if err != nil {
37+
return err
38+
}
39+
40+
if res.StatusCode != http.StatusOK {
41+
return bodyError(res)
42+
}
43+
44+
return nil
45+
}

Diff for: internal/sync/sync.go

+15
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ type Sync struct {
4141
Client *entclient.Client
4242
}
4343

44+
// See https://lxadm.com/Rsync_exit_codes#List_of_standard_rsync_exit_codes.
45+
const (
46+
rsyncExitCodeIncompat = 2
47+
rsyncExitCodeDataStream = 12
48+
)
49+
4450
func (s Sync) syncPaths(delete bool, local, remote string) error {
4551
self := os.Args[0]
4652

@@ -66,6 +72,15 @@ func (s Sync) syncPaths(delete bool, local, remote string) error {
6672
cmd.Stdin = os.Stdin
6773
err := cmd.Run()
6874
if err != nil {
75+
if exitError, ok := err.(*exec.ExitError); ok {
76+
if exitError.ExitCode() == rsyncExitCodeIncompat {
77+
return xerrors.Errorf("no compatible rsync on remote machine: rsync: %w", err)
78+
} else if exitError.ExitCode() == rsyncExitCodeDataStream {
79+
return xerrors.Errorf("protocol datastream error or no remote rsync found: %w", err)
80+
} else {
81+
return xerrors.Errorf("rsync: %w", err)
82+
}
83+
}
6984
return xerrors.Errorf("rsync: %w", err)
7085
}
7186
return nil

0 commit comments

Comments
 (0)