diff --git a/cmd/coder/sync.go b/cmd/coder/sync.go index 9aa0a0ca..0e26af96 100644 --- a/cmd/coder/sync.go +++ b/cmd/coder/sync.go @@ -1,7 +1,6 @@ package main import ( - "errors" "os" "path/filepath" "strings" @@ -30,9 +29,6 @@ func (cmd *syncCmd) RegisterFlags(fl *pflag.FlagSet) { fl.BoolVarP(&cmd.init, "init", "i", false, "do initial transfer and exit") } -// See https://lxadm.com/Rsync_exit_codes#List_of_standard_rsync_exit_codes. -var NoRsync = errors.New("rsync: exit status 2") - func (cmd *syncCmd) Run(fl *pflag.FlagSet) { var ( local = fl.Arg(0) @@ -79,9 +75,7 @@ func (cmd *syncCmd) Run(fl *pflag.FlagSet) { err = s.Run() } - if err == NoRsync { - flog.Fatal("no compatible rsync present on remote machine") - } else { - flog.Fatal("sync: %v", err) + if err != nil { + flog.Fatal("%v", err) } } diff --git a/cmd/coder/urls.go b/cmd/coder/urls.go index 6b10375b..7626ae93 100644 --- a/cmd/coder/urls.go +++ b/cmd/coder/urls.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" "os" + "strconv" + "strings" "text/tabwriter" "github.com/spf13/pflag" @@ -15,35 +17,164 @@ import ( type urlsCmd struct{} +// 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"` Access string `json:"access"` } -func (cmd urlsCmd) Spec() cli.CommandSpec { +var urlAccessLevel = map[string]string{ + //Remote API endpoint requires these in uppercase + "PRIVATE": "Only you can access", + "ORG": "All members of your organization can access", + "AUTHED": "Authenticated users can access", + "PUBLIC": "Anyone on the internet can access this link", +} + +func portIsValid(port string) bool { + p, err := strconv.ParseUint(port, 10, 16) + if p < 1 { + // port 0 means 'any free port', which we don't support + err = strconv.ErrRange + } + if err != nil { + fmt.Println("Invalid port") + } + return err == nil +} + +func accessLevelIsValid(level string) bool { + _, ok := urlAccessLevel[level] + if !ok { + fmt.Println("Invalid access level") + } + return ok +} + +type createSubCmd struct { + access string +} + +func (sub *createSubCmd) RegisterFlags(fl *pflag.FlagSet) { + fl.StringVarP(&sub.access, "access", "a", "private", "[private | org | authed | public] set devurl access") +} + +func (sub createSubCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ - Name: "urls", - Usage: "", - Desc: "get all development urls for external access", + Name: "create", + Usage: " [--access ]", + Desc: "create/update a devurl for external access", } } -func (cmd urlsCmd) Run(fl *pflag.FlagSet) { - var envName = fl.Arg(0) +// 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) if envName == "" { exitUsage(fl) } + if !portIsValid(port) { + exitUsage(fl) + } + + access = strings.ToUpper(sub.access) + if !accessLevelIsValid(access) { + exitUsage(fl) + } + entClient := requireAuth() env := findEnv(entClient, envName) + _, found := devURLID(port, urlList(envName)) + if found { + fmt.Printf("Updating devurl for port %v\n", port) + } 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()) + } +} + +type delSubCmd struct{} + +func (sub delSubCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "del", + Usage: " ", + Desc: "delete a devurl", + } +} + +// devURLID returns the ID of a devURL, given the env name and port. +// ("", false) is returned if no match is found. +func devURLID(port string, urls []DevURL) (string, bool) { + for _, url := range urls { + if url.Port == port { + return url.ID, true + } + } + return "", false +} + +// Run deletes a devURL, specified by env ID and port, from the cemanager. +func (sub delSubCmd) Run(fl *pflag.FlagSet) { + envName := fl.Arg(0) + port := fl.Arg(1) + + if envName == "" { + exitUsage(fl) + } + + if !portIsValid(port) { + exitUsage(fl) + } + + entClient := requireAuth() + + env := findEnv(entClient, envName) + + urlID, found := devURLID(port, urlList(envName)) + if found { + fmt.Printf("Deleting devurl for port %v\n", 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", + } +} + +// urlList returns the list of active devURLs from the cemanager. +func urlList(envName string) []DevURL { + entClient := requireAuth() + env := findEnv(entClient, envName) + reqString := "%s/api/environments/%s/devurls?session_token=%s" - reqUrl := fmt.Sprintf(reqString, entClient.BaseURL, env.ID, entClient.Token) + reqURL := fmt.Sprintf(reqString, entClient.BaseURL, env.ID, entClient.Token) - resp, err := http.Get(reqUrl) + resp, err := http.Get(reqURL) if err != nil { flog.Fatal("%v", err) } @@ -55,7 +186,7 @@ func (cmd urlsCmd) Run(fl *pflag.FlagSet) { dec := json.NewDecoder(resp.Body) - var devURLs = make([]DevURL, 0) + devURLs := make([]DevURL, 0) err = dec.Decode(&devURLs) if err != nil { flog.Fatal("%v", err) @@ -65,9 +196,25 @@ func (cmd urlsCmd) Run(fl *pflag.FlagSet) { 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{}, + } +} diff --git a/internal/entclient/activity.go b/internal/entclient/activity.go index 64e9e82a..92c0afed 100644 --- a/internal/entclient/activity.go +++ b/internal/entclient/activity.go @@ -1,6 +1,8 @@ package entclient -import "net/http" +import ( + "net/http" +) func (c Client) PushActivity(source string, envID string) error { res, err := c.request("POST", "/api/metrics/usage/push", map[string]string{ diff --git a/internal/entclient/devurl.go b/internal/entclient/devurl.go new file mode 100644 index 00000000..57925372 --- /dev/null +++ b/internal/entclient/devurl.go @@ -0,0 +1,45 @@ +package entclient + +import ( + "fmt" + "net/http" +) + +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, + }) + if err != nil { + return err + } + + if res.StatusCode != http.StatusOK { + return bodyError(res) + } + + return nil +} + +func (c Client) UpsertDevURL(envID, port, 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, + }) + if err != nil { + return err + } + + if res.StatusCode != http.StatusOK { + return bodyError(res) + } + + return nil +} diff --git a/internal/sync/sync.go b/internal/sync/sync.go index 4e9c6f8e..32086896 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -41,6 +41,12 @@ type Sync struct { Client *entclient.Client } +// See https://lxadm.com/Rsync_exit_codes#List_of_standard_rsync_exit_codes. +const ( + rsyncExitCodeIncompat = 2 + rsyncExitCodeDataStream = 12 +) + func (s Sync) syncPaths(delete bool, local, remote string) error { self := os.Args[0] @@ -66,6 +72,15 @@ func (s Sync) syncPaths(delete bool, local, remote string) error { cmd.Stdin = os.Stdin err := cmd.Run() if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.ExitCode() == rsyncExitCodeIncompat { + return xerrors.Errorf("no compatible rsync on remote machine: rsync: %w", err) + } else if exitError.ExitCode() == rsyncExitCodeDataStream { + return xerrors.Errorf("protocol datastream error or no remote rsync found: %w", err) + } else { + return xerrors.Errorf("rsync: %w", err) + } + } return xerrors.Errorf("rsync: %w", err) } return nil