diff --git a/internal/client/client.go b/internal/client/client.go index e4032bb..24a8318 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -2,33 +2,9 @@ package client import ( "net/url" - - "go.coder.com/cloud-agent/internal/config" ) type Client struct { - token string - baseURL *url.URL -} - -func FromEnv() (*Client, error) { - token, err := config.SessionToken.Read() - if err != nil { - return nil, err - } - - u, err := config.URL.Read() - if err != nil { - return nil, err - } - - parsed, err := url.Parse(u) - if err != nil { - return nil, err - } - - return &Client{ - token: token, - baseURL: parsed, - }, nil + Token string + BaseURL *url.URL } diff --git a/internal/client/request.go b/internal/client/request.go index 91203c7..f77e1a9 100644 --- a/internal/client/request.go +++ b/internal/client/request.go @@ -16,12 +16,12 @@ func (c *Client) request(method, path string, body interface{}) (*http.Response, return nil, xerrors.Errorf("marshal body: %w", err) } - req, err := http.NewRequest(method, c.baseURL.String()+path, bytes.NewReader(b)) + req, err := http.NewRequest(method, c.BaseURL.String()+path, bytes.NewReader(b)) if err != nil { return nil, xerrors.Errorf("new request: %w", err) } - req.Header.Set(sessionHeader, c.token) + req.Header.Set(sessionHeader, c.Token) return http.DefaultClient.Do(req) } diff --git a/internal/cmd/bind.go b/internal/cmd/bind.go new file mode 100644 index 0000000..3a46e1a --- /dev/null +++ b/internal/cmd/bind.go @@ -0,0 +1,152 @@ +package cmd + +import ( + "context" + "net/url" + "os" + "regexp" + "strings" + "time" + + "cdr.dev/slog/sloggers/sloghuman" + "github.com/spf13/pflag" + "golang.org/x/xerrors" + + "go.coder.com/cli" + "go.coder.com/cloud-agent/internal/client" + "go.coder.com/cloud-agent/internal/config" + "go.coder.com/cloud-agent/internal/ideproxy" + "go.coder.com/flog" +) + +var ( + DefaultCloudURL = "https://cloud.coder.com" +) + +var codeServerNameRx = regexp.MustCompile("^[a-z][a-z0-9_]{0,50}$") + +type bindCmd struct { + cloudURL string + codeServerAddr string +} + +func (c *bindCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "bind", + Usage: "[NAME]", + Desc: "Bind a server to Coder Cloud. A name will be generated from the hostname if one is not provided.", + } +} + +func (c *bindCmd) RegisterFlags(fl *pflag.FlagSet) { + fl.StringVar(&c.cloudURL, "cloud-url", DefaultCloudURL, "The Coder Cloud URL to connect to.") + fl.StringVar(&c.codeServerAddr, + "code-server-addr", + "localhost:8080", + "The address of the code-server instance to proxy.", + ) +} + +func (c *bindCmd) Run(fl *pflag.FlagSet) { + var ( + err error + ctx = context.Background() + ) + + name := fl.Arg(0) + if name == "" { + // Generate a name based on the hostname if one is not provided. + name, err = genServerName() + if err != nil { + flog.Fatal("Failed to generate server name: %v", err.Error()) + } + } + + if !codeServerNameRx.MatchString(name) { + flog.Fatal("Name must conform to regex %s", codeServerNameRx.String()) + } + + cloudURL, err := url.Parse(c.cloudURL) + if err != nil { + flog.Fatal("Invalid Cloud URL: %v", err.Error()) + } + + token, err := config.SessionToken.Read() + if xerrors.Is(err, os.ErrNotExist) { + token, err = login(cloudURL.String(), name) + } + if err != nil { + flog.Fatal("Failed to login: %v", err) + } + + cli := client.Client{ + Token: token, + BaseURL: cloudURL, + } + + // Register the server with Coder Cloud. This is an idempotent + // operation. + cs, err := cli.RegisterCodeServer(name) + if err != nil { + flog.Fatal("Failed to register server: %v", err) + } + + // Get the Access URL for the user. + url, err := cli.AccessURL(cs.ID) + if err != nil { + flog.Fatal("Failed to query server: %v", err) + } + + agent := &ideproxy.Agent{ + Log: sloghuman.Make(os.Stderr), + CodeServerID: cs.ID, + SessionToken: token, + CloudProxyURL: c.cloudURL, + CodeServerAddr: os.Getenv(c.codeServerAddr), + } + + proxy := func() { + err = agent.Proxy(ctx) + if err != nil { + flog.Error("Connection to Coder-Cloud disrupted, re-establishing connection: %v", err.Error()) + } + } + + flog.Info("Proxying code-server to Coder Cloud, you can access your IDE at %v", url) + + proxy() + + // Avoid a super tight loop. + ticker := time.NewTicker(time.Second) + for range ticker.C { + proxy() + } +} + +func login(url, serverName string) (string, error) { + token, err := client.Login(url, serverName) + if err != nil { + return "", xerrors.Errorf("unable to login: %w", err) + } + + err = config.SessionToken.Write(token) + if err != nil { + return "", xerrors.Errorf("write session token to file: %w", err) + } + + return token, nil +} + +func genServerName() (string, error) { + hostname, err := os.Hostname() + if err != nil { + xerrors.Errorf("get hostname: %w", err) + } + + hostname = strings.ToLower(hostname) + + // Only use the first token. + hostname = strings.Split(hostname, ".")[0] + // '-' are not allowed, convert them to '_'. + return strings.Replace(hostname, "-", "_", -1), nil +} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 1977655..6f43082 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -27,8 +27,7 @@ func (c *rootCmd) Spec() cli.CommandSpec { func (c *rootCmd) Subcommands() []cli.Command { return []cli.Command{ - &linkCmd{}, - &proxyCmd{}, + &bindCmd{}, &versionCmd{}, } } diff --git a/internal/cmd/link.go b/internal/cmd/link.go deleted file mode 100644 index dc19357..0000000 --- a/internal/cmd/link.go +++ /dev/null @@ -1,91 +0,0 @@ -package cmd - -import ( - "os" - "regexp" - - "github.com/spf13/pflag" - "golang.org/x/xerrors" - - "go.coder.com/cli" - "go.coder.com/cloud-agent/internal/client" - "go.coder.com/cloud-agent/internal/config" - "go.coder.com/flog" -) - -var DefaultCloudURL string = "https://cloud.coder.com" - -var codeServerNameRx = regexp.MustCompile("^[a-z][a-z0-9_]{0,50}$") - -type linkCmd struct { - cloudURL string -} - -func (c *linkCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "link", - Usage: "NAME [FLAGS]", - Desc: "Link a server with Coder Cloud", - } -} - -func (c *linkCmd) RegisterFlags(fl *pflag.FlagSet) { - // TODO: this should be updated whenever we figure out the domain we're - // using. - fl.StringVar(&c.cloudURL, "cloud-url", DefaultCloudURL, "The Coder Cloud URL to connect to.") -} - -func (c *linkCmd) Run(fl *pflag.FlagSet) { - name := fl.Arg(0) - if name == "" { - flog.Fatal("Must provide a name") - } - - if !codeServerNameRx.MatchString(name) { - flog.Fatal("Name must conform to regex %s", codeServerNameRx.String()) - } - - _, err := config.ServerID.Read() - if err == nil { - flog.Info("Server already registered!") - return - } - - cli, err := client.FromEnv() - if xerrors.Is(err, os.ErrNotExist) { - cli, err = loginClient(c.cloudURL, name) - } - if err != nil { - flog.Fatal("Failed to login: %v", err) - } - - cs, err := cli.RegisterCodeServer(name) - if err != nil { - flog.Fatal("Failed to register server: %v", err) - } - - err = config.ServerID.Write(cs.ID) - if err != nil { - flog.Fatal("Failed to store server id: %v", err) - } - flog.Success("Successfully registered server!") -} - -func loginClient(url, serverName string) (*client.Client, error) { - token, err := client.Login(url, serverName) - if err != nil { - return nil, xerrors.Errorf("unable to login: %w", err) - } - - err = config.SessionToken.Write(token) - if err != nil { - return nil, xerrors.Errorf("write session token to file: %w", err) - } - - err = config.URL.Write(url) - if err != nil { - return nil, xerrors.Errorf("write coder-cloud url to file: %w", err) - } - - return client.FromEnv() -} diff --git a/internal/cmd/proxy.go b/internal/cmd/proxy.go deleted file mode 100644 index 87e1f5f..0000000 --- a/internal/cmd/proxy.go +++ /dev/null @@ -1,96 +0,0 @@ -package cmd - -import ( - "context" - "os" - "time" - - "cdr.dev/slog/sloggers/sloghuman" - "github.com/spf13/pflag" - "golang.org/x/xerrors" - - "go.coder.com/cli" - "go.coder.com/cloud-agent/internal/client" - "go.coder.com/cloud-agent/internal/config" - "go.coder.com/cloud-agent/internal/ideproxy" - "go.coder.com/flog" -) - -var codeserverPasswordEnv = "CODESERVER_PASSWORD" - -type proxyCmd struct { - codeServerAddr string -} - -func (c *proxyCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "proxy", - Usage: "[FLAGS]", - Desc: "Proxy code-server to Coder-Cloud", - } -} - -func (c *proxyCmd) RegisterFlags(fl *pflag.FlagSet) { - fl.StringVar(&c.codeServerAddr, - "code-server-addr", - "localhost:8080", - "The address of the code-server instance to proxy.", - ) -} - -func (c *proxyCmd) Run(fl *pflag.FlagSet) { - var ( - ctx = context.Background() - ) - - cli, err := client.FromEnv() - if err != nil { - flog.Fatal("Failed to get client, have you logged in?") - } - - csid, err := config.ServerID.Read() - if err != nil { - flog.Fatal("Failed to get server id: %v", err) - } - - url, err := cli.AccessURL(csid) - if err != nil { - flog.Fatal("Failed to query server: %v", err) - } - - conf, err := config.ReadFiles(config.ServerID, config.SessionToken, config.URL) - if xerrors.Is(err, os.ErrNotExist) { - flog.Fatal("Failed to read configuration files, have you logged in?") - } - if err != nil { - flog.Fatal("Failed to read configuration file: %v", err.Error()) - } - - password := os.Getenv(codeserverPasswordEnv) - - agent := &ideproxy.Agent{ - Log: sloghuman.Make(os.Stderr), - CodeServerID: conf[config.ServerID], - SessionToken: conf[config.SessionToken], - CloudProxyURL: conf[config.URL], - CodeServerAddr: c.codeServerAddr, - CodeServerPassword: password, - } - - proxy := func() { - err = agent.Proxy(ctx) - if err != nil { - flog.Error("Connection to Coder-Cloud distrupted, re-establishing connection: %v", err.Error()) - } - } - - flog.Info("Proxying code-server to Coder Cloud, you can access your IDE at %v", url) - - proxy() - - // Avoid a super tight loop. - ticker := time.NewTicker(time.Second) - for range ticker.C { - proxy() - } -} diff --git a/internal/config/file.go b/internal/config/file.go index cb3cf7f..fd58ea9 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -5,10 +5,6 @@ import "golang.org/x/xerrors" var ( // SessionToken is the file containing the session token. SessionToken File = "session" - // ServerID is the file containing the server ID. - ServerID File = "server" - // URL is the file containing the url to a Coder-Cloud instance. - URL File = "url" ) // File is a thin wrapper around os.File for conveniently interacting