Skip to content

Commit f5e37ea

Browse files
author
Anthony Shull
authored
Merge pull request coder#56 from cdr/update-dev-urls-for-api-changes
Update API request, print mult. envs in tabs, handle non 200 response
2 parents eb2c835 + 4c1d110 commit f5e37ea

File tree

7 files changed

+286
-73
lines changed

7 files changed

+286
-73
lines changed

cmd/coder/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func (r *rootCmd) Subcommands() []cli.Command {
3636
&logoutCmd{},
3737
&shellCmd{},
3838
&syncCmd{},
39-
&urlCmd{},
39+
&urlsCmd{},
4040
&versionCmd{},
4141
&configSSHCmd{},
4242
}

cmd/coder/sync.go

Lines changed: 2 additions & 12 deletions
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
}

cmd/coder/url.go

Lines changed: 0 additions & 59 deletions
This file was deleted.

cmd/coder/urls.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
"strconv"
9+
"strings"
10+
"text/tabwriter"
11+
12+
"github.com/spf13/pflag"
13+
14+
"go.coder.com/cli"
15+
"go.coder.com/flog"
16+
)
17+
18+
type urlsCmd struct{}
19+
20+
// DevURL is the parsed json response record for a devURL from cemanager
21+
type DevURL struct {
22+
ID string `json:"id"`
23+
URL string `json:"url"`
24+
Port string `json:"port"`
25+
Access string `json:"access"`
26+
}
27+
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 {
65+
return cli.CommandSpec{
66+
Name: "create",
67+
Usage: "<env name> <port> [--access <level>]",
68+
Desc: "create/update a devurl for external access",
69+
}
70+
}
71+
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)
79+
80+
if envName == "" {
81+
exitUsage(fl)
82+
}
83+
84+
if !portIsValid(port) {
85+
exitUsage(fl)
86+
}
87+
88+
access = strings.ToUpper(sub.access)
89+
if !accessLevelIsValid(access) {
90+
exitUsage(fl)
91+
}
92+
93+
entClient := requireAuth()
94+
95+
env := findEnv(entClient, envName)
96+
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+
174+
reqString := "%s/api/environments/%s/devurls?session_token=%s"
175+
reqURL := fmt.Sprintf(reqString, entClient.BaseURL, env.ID, entClient.Token)
176+
177+
resp, err := http.Get(reqURL)
178+
if err != nil {
179+
flog.Fatal("%v", err)
180+
}
181+
defer resp.Body.Close()
182+
183+
if resp.StatusCode != 200 {
184+
flog.Fatal("non-success status code: %d", resp.StatusCode)
185+
}
186+
187+
dec := json.NewDecoder(resp.Body)
188+
189+
devURLs := make([]DevURL, 0)
190+
err = dec.Decode(&devURLs)
191+
if err != nil {
192+
flog.Fatal("%v", err)
193+
}
194+
195+
if len(devURLs) == 0 {
196+
fmt.Printf("no dev urls were found for environment: %s\n", envName)
197+
}
198+
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+
208+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent)
209+
for _, devURL := range devURLs {
210+
fmt.Fprintf(w, "%s\t%s\t%s\n", devURL.URL, devURL.Port, devURL.Access)
211+
}
212+
w.Flush()
213+
}
214+
215+
func (cmd *urlsCmd) Subcommands() []cli.Command {
216+
return []cli.Command{
217+
&createSubCmd{},
218+
&delSubCmd{},
219+
}
220+
}

internal/entclient/activity.go

Lines changed: 3 additions & 1 deletion
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{

internal/entclient/devurl.go

Lines changed: 45 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)