diff --git a/ci/integration/envs_test.go b/ci/integration/envs_test.go index 65101667..1a9de9ce 100644 --- a/ci/integration/envs_test.go +++ b/ci/integration/envs_test.go @@ -14,21 +14,27 @@ import ( "cdr.dev/slog/sloggers/slogtest" "cdr.dev/slog/sloggers/slogtest/assert" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/pkg/tcli" ) -func cleanupClient(ctx context.Context, t *testing.T) *coder.Client { +func cleanupClient(ctx context.Context, t *testing.T) coder.Client { creds := login(ctx, t) u, err := url.Parse(creds.url) assert.Success(t, "parse base url", err) - return &coder.Client{BaseURL: u, Token: creds.token} + client, err := coder.NewClient(coder.ClientOptions{ + BaseURL: u, + Token: creds.token, + }) + require.NoError(t, err, "failed to create coder.Client") + return client } -func cleanupEnv(t *testing.T, client *coder.Client, envID string) func() { +func cleanupEnv(t *testing.T, client coder.Client, envID string) func() { return func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -39,9 +45,9 @@ func cleanupEnv(t *testing.T, client *coder.Client, envID string) func() { } // this is a stopgap until we have support for a `coder images` subcommand -// until then, we want can use the *coder.Client to ensure our integration tests +// until then, we want can use the coder.Client to ensure our integration tests // work on fresh deployments. -func ensureImageImported(ctx context.Context, t *testing.T, client *coder.Client, img string) { +func ensureImageImported(ctx context.Context, t *testing.T, client coder.Client, img string) { orgs, err := client.Organizations(ctx) assert.Success(t, "get orgs", err) diff --git a/coder-sdk/activity.go b/coder-sdk/activity.go index 26034a59..c885f619 100644 --- a/coder-sdk/activity.go +++ b/coder-sdk/activity.go @@ -11,7 +11,7 @@ type activityRequest struct { } // PushActivity pushes CLI activity to Coder. -func (c Client) PushActivity(ctx context.Context, source, envID string) error { +func (c *DefaultClient) PushActivity(ctx context.Context, source, envID string) error { resp, err := c.request(ctx, http.MethodPost, "/api/private/metrics/usage/push", activityRequest{ Source: source, EnvironmentID: envID, diff --git a/coder-sdk/client.go b/coder-sdk/client.go index 3f859bde..0996dc17 100644 --- a/coder-sdk/client.go +++ b/coder-sdk/client.go @@ -1,46 +1,67 @@ package coder import ( + "errors" "net/http" - "net/http/cookiejar" "net/url" ) +// ensure that DefaultClient implements Client. +var _ = Client(&DefaultClient{}) + // Me is the user ID of the authenticated user. const Me = "me" -// Client wraps the Coder HTTP API. -type Client struct { +// ClientOptions contains options for the Coder SDK Client. +type ClientOptions struct { + // BaseURL is the root URL of the Coder installation. BaseURL *url.URL - Token string + + // Client is the http.Client to use for requests (optional). + // If omitted, the http.DefaultClient will be used. + HTTPClient *http.Client + + // Token is the API Token used to authenticate + Token string } -// newHTTPClient creates a default underlying http client and sets the auth cookie. -// -// NOTE: -// As we do not specify a custom transport, the default one from the stdlib will be used, -// resulting in a persistent connection pool. -// We do not set a timeout here as it could cause issue with the websocket. -// The caller is expected to set it when needed. -// -// WARNING: -// If the caller sets a custom transport to set TLS settings or a custom CA the default -// pool will not be used and it might result in a new dns lookup/tls session/socket begin -// established each time. -func (c Client) newHTTPClient() (*http.Client, error) { - jar, err := cookiejar.New(nil) - if err != nil { - return nil, err +// NewClient creates a new default Coder SDK client. +func NewClient(opts ClientOptions) (*DefaultClient, error) { + httpClient := opts.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + + if opts.BaseURL == nil { + return nil, errors.New("the BaseURL parameter is required") } - jar.SetCookies(c.BaseURL, []*http.Cookie{{ - Name: "session_token", - Value: c.Token, - MaxAge: 86400, - Path: "/", - HttpOnly: true, - Secure: c.BaseURL.Scheme == "https", - }}) + if opts.Token == "" { + return nil, errors.New("an API token is required") + } + + client := &DefaultClient{ + baseURL: opts.BaseURL, + httpClient: httpClient, + token: opts.Token, + } + + return client, nil +} + +// DefaultClient is the default implementation of the coder.Client +// interface. +// +// The empty value is meaningless and the fields are unexported; +// use NewClient to create an instance. +type DefaultClient struct { + // baseURL is the URL (scheme, hostname/IP address, port, + // path prefix of the Coder installation) + baseURL *url.URL + + // httpClient is the http.Client used to issue requests. + httpClient *http.Client - return &http.Client{Jar: jar}, nil + // token is the API Token credential. + token string } diff --git a/coder-sdk/client_test.go b/coder-sdk/client_test.go index 9c23bee0..232a6d67 100644 --- a/coder-sdk/client_test.go +++ b/coder-sdk/client_test.go @@ -41,9 +41,12 @@ func TestPushActivity(t *testing.T) { u, err := url.Parse(server.URL) require.NoError(t, err, "failed to parse test server URL") - client := coder.Client{ + client, err := coder.NewClient(coder.ClientOptions{ BaseURL: u, - } + Token: "SwdcSoq5Jc-0C1r8wfwm7h6h9i0RDk7JT", + }) + require.NoError(t, err, "failed to create coder.Client") + err = client.PushActivity(context.Background(), source, envID) require.NoError(t, err) } @@ -81,9 +84,12 @@ func TestUsers(t *testing.T) { u, err := url.Parse(server.URL) require.NoError(t, err, "failed to parse test server URL") - client := coder.Client{ + client, err := coder.NewClient(coder.ClientOptions{ BaseURL: u, - } + Token: "JcmErkJjju-KSrztst0IJX7xGJhKQPtfv", + }) + require.NoError(t, err, "failed to create coder.Client") + users, err := client.Users(context.Background()) require.NoError(t, err, "error getting Users") require.Len(t, users, 1, "users should return a single user") @@ -96,9 +102,8 @@ func TestAuthentication(t *testing.T) { const token = "g4mtIPUaKt-pPl9Q0xmgKs7acSypHt4Jf" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("session_token") - require.NoError(t, err, "error extracting session token") - require.Equal(t, token, cookie.Value, "token does not match") + gotToken := r.Header.Get("Session-Token") + require.Equal(t, token, gotToken, "token does not match") w.WriteHeader(http.StatusServiceUnavailable) })) @@ -109,11 +114,14 @@ func TestAuthentication(t *testing.T) { u, err := url.Parse(server.URL) require.NoError(t, err, "failed to parse test server URL") - client := coder.Client{ + client, err := coder.NewClient(coder.ClientOptions{ BaseURL: u, Token: token, - } - _, _ = client.APIVersion(context.Background()) + }) + require.NoError(t, err, "failed to create coder.Client") + + _, err = client.APIVersion(context.Background()) + require.NoError(t, err, "failed to get API version information") } func TestContextRoot(t *testing.T) { @@ -140,9 +148,13 @@ func TestContextRoot(t *testing.T) { for _, prefix := range contextRoots { u.Path = prefix - client := coder.Client{ + client, err := coder.NewClient(coder.ClientOptions{ BaseURL: u, - } - _, _ = client.Users(context.Background()) + Token: "FrOgA6xhpM-p5nTfsupmvzYJA6DJSOUoE", + }) + require.NoError(t, err, "failed to create coder.Client") + + _, err = client.Users(context.Background()) + require.Error(t, err, "expected 503 error") } } diff --git a/coder-sdk/config.go b/coder-sdk/config.go index fcf57920..e7c4c864 100644 --- a/coder-sdk/config.go +++ b/coder-sdk/config.go @@ -65,7 +65,7 @@ type ConfigOAuth struct { } // SiteConfigAuth fetches the sitewide authentication configuration. -func (c Client) SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) { +func (c *DefaultClient) SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) { var conf ConfigAuth if err := c.requestBody(ctx, http.MethodGet, "/api/private/auth/config", nil, &conf); err != nil { return nil, err @@ -74,12 +74,12 @@ func (c Client) SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) { } // PutSiteConfigAuth sets the sitewide authentication configuration. -func (c Client) PutSiteConfigAuth(ctx context.Context, req ConfigAuth) error { +func (c *DefaultClient) PutSiteConfigAuth(ctx context.Context, req ConfigAuth) error { return c.requestBody(ctx, http.MethodPut, "/api/private/auth/config", req, nil) } // SiteConfigOAuth fetches the sitewide git provider OAuth configuration. -func (c Client) SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) { +func (c *DefaultClient) SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) { var conf ConfigOAuth if err := c.requestBody(ctx, http.MethodGet, "/api/private/oauth/config", nil, &conf); err != nil { return nil, err @@ -88,7 +88,7 @@ func (c Client) SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) { } // PutSiteConfigOAuth sets the sitewide git provider OAuth configuration. -func (c Client) PutSiteConfigOAuth(ctx context.Context, req ConfigOAuth) error { +func (c *DefaultClient) PutSiteConfigOAuth(ctx context.Context, req ConfigOAuth) error { return c.requestBody(ctx, http.MethodPut, "/api/private/oauth/config", req, nil) } @@ -97,7 +97,7 @@ type configSetupMode struct { } // SiteSetupModeEnabled fetches the current setup_mode state of a Coder Enterprise deployment. -func (c Client) SiteSetupModeEnabled(ctx context.Context) (bool, error) { +func (c *DefaultClient) SiteSetupModeEnabled(ctx context.Context) (bool, error) { var conf configSetupMode if err := c.requestBody(ctx, http.MethodGet, "/api/private/config/setup-mode", nil, &conf); err != nil { return false, err @@ -125,7 +125,7 @@ type ConfigExtensionMarketplace struct { } // SiteConfigExtensionMarketplace fetches the extension marketplace configuration. -func (c Client) SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExtensionMarketplace, error) { +func (c *DefaultClient) SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExtensionMarketplace, error) { var conf ConfigExtensionMarketplace if err := c.requestBody(ctx, http.MethodGet, "/api/private/extensions/config", nil, &conf); err != nil { return nil, err @@ -134,6 +134,6 @@ func (c Client) SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExte } // PutSiteConfigExtensionMarketplace sets the extension marketplace configuration. -func (c Client) PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error { +func (c *DefaultClient) PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error { return c.requestBody(ctx, http.MethodPut, "/api/private/extensions/config", req, nil) } diff --git a/coder-sdk/devurl.go b/coder-sdk/devurl.go index c574c220..ea7e013e 100644 --- a/coder-sdk/devurl.go +++ b/coder-sdk/devurl.go @@ -22,7 +22,7 @@ type delDevURLRequest struct { } // DeleteDevURL deletes the specified devurl. -func (c Client) DeleteDevURL(ctx context.Context, envID, urlID string) error { +func (c *DefaultClient) DeleteDevURL(ctx context.Context, envID, urlID string) error { reqURL := fmt.Sprintf("/api/v0/environments/%s/devurls/%s", envID, urlID) return c.requestBody(ctx, http.MethodDelete, reqURL, delDevURLRequest{ @@ -41,12 +41,12 @@ type CreateDevURLReq struct { } // CreateDevURL inserts a new devurl for the authenticated user. -func (c Client) CreateDevURL(ctx context.Context, envID string, req CreateDevURLReq) error { +func (c *DefaultClient) CreateDevURL(ctx context.Context, envID string, req CreateDevURLReq) error { return c.requestBody(ctx, http.MethodPost, "/api/v0/environments/"+envID+"/devurls", req, nil) } // DevURLs fetches the Dev URLs for a given environment. -func (c Client) DevURLs(ctx context.Context, envID string) ([]DevURL, error) { +func (c *DefaultClient) DevURLs(ctx context.Context, envID string) ([]DevURL, error) { var devurls []DevURL if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments/"+envID+"/devurls", nil, &devurls); err != nil { return nil, err @@ -58,6 +58,6 @@ func (c Client) DevURLs(ctx context.Context, envID string) ([]DevURL, error) { type PutDevURLReq CreateDevURLReq // PutDevURL updates an existing devurl for the authenticated user. -func (c Client) PutDevURL(ctx context.Context, envID, urlID string, req PutDevURLReq) error { +func (c *DefaultClient) PutDevURL(ctx context.Context, envID, urlID string, req PutDevURLReq) error { return c.requestBody(ctx, http.MethodPut, "/api/v0/environments/"+envID+"/devurls/"+urlID, req, nil) } diff --git a/coder-sdk/env.go b/coder-sdk/env.go index 9ef54f11..b8b766d3 100644 --- a/coder-sdk/env.go +++ b/coder-sdk/env.go @@ -88,7 +88,7 @@ type CreateEnvironmentRequest struct { } // CreateEnvironment sends a request to create an environment. -func (c Client) CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) (*Environment, error) { +func (c *DefaultClient) CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) (*Environment, error) { var env Environment if err := c.requestBody(ctx, http.MethodPost, "/api/v0/environments", req, &env); err != nil { return nil, err @@ -126,7 +126,7 @@ type Resources struct { // ParseTemplate parses a template config. It support both remote repositories and local files. // If a local file is specified then all other values in the request are ignored. -func (c Client) ParseTemplate(ctx context.Context, req ParseTemplateRequest) (Template, error) { +func (c *DefaultClient) ParseTemplate(ctx context.Context, req ParseTemplateRequest) (Template, error) { const path = "/api/private/environments/template/parse" var ( tpl Template @@ -153,7 +153,7 @@ func (c Client) ParseTemplate(ctx context.Context, req ParseTemplateRequest) (Te } // CreateEnvironmentFromRepo sends a request to create an environment from a repository. -func (c Client) CreateEnvironmentFromRepo(ctx context.Context, orgID string, req Template) (*Environment, error) { +func (c *DefaultClient) CreateEnvironmentFromRepo(ctx context.Context, orgID string, req Template) (*Environment, error) { var env Environment if err := c.requestBody(ctx, http.MethodPost, "/api/private/orgs/"+orgID+"/environments/from-repo", req, &env); err != nil { return nil, err @@ -163,7 +163,7 @@ func (c Client) CreateEnvironmentFromRepo(ctx context.Context, orgID string, req // Environments lists environments returned by the given filter. // TODO: add the filter options, explore performance issue. -func (c Client) Environments(ctx context.Context) ([]Environment, error) { +func (c *DefaultClient) Environments(ctx context.Context) ([]Environment, error) { var envs []Environment if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments", nil, &envs); err != nil { return nil, err @@ -172,7 +172,7 @@ func (c Client) Environments(ctx context.Context) ([]Environment, error) { } // UserEnvironmentsByOrganization gets the list of environments owned by the given user. -func (c Client) UserEnvironmentsByOrganization(ctx context.Context, userID, orgID string) ([]Environment, error) { +func (c *DefaultClient) UserEnvironmentsByOrganization(ctx context.Context, userID, orgID string) ([]Environment, error) { var ( envs []Environment query = url.Values{} @@ -188,12 +188,12 @@ func (c Client) UserEnvironmentsByOrganization(ctx context.Context, userID, orgI } // DeleteEnvironment deletes the environment. -func (c Client) DeleteEnvironment(ctx context.Context, envID string) error { +func (c *DefaultClient) DeleteEnvironment(ctx context.Context, envID string) error { return c.requestBody(ctx, http.MethodDelete, "/api/v0/environments/"+envID, nil, nil) } -// StopEnvironment stops the stops. -func (c Client) StopEnvironment(ctx context.Context, envID string) error { +// StopEnvironment stops the environment. +func (c *DefaultClient) StopEnvironment(ctx context.Context, envID string) error { return c.requestBody(ctx, http.MethodPut, "/api/v0/environments/"+envID+"/stop", nil, nil) } @@ -211,23 +211,23 @@ type UpdateEnvironmentReq struct { } // RebuildEnvironment requests that the given envID is rebuilt with no changes to its specification. -func (c Client) RebuildEnvironment(ctx context.Context, envID string) error { +func (c *DefaultClient) RebuildEnvironment(ctx context.Context, envID string) error { return c.requestBody(ctx, http.MethodPatch, "/api/v0/environments/"+envID, UpdateEnvironmentReq{}, nil) } // EditEnvironment modifies the environment specification and initiates a rebuild. -func (c Client) EditEnvironment(ctx context.Context, envID string, req UpdateEnvironmentReq) error { +func (c *DefaultClient) EditEnvironment(ctx context.Context, envID string, req UpdateEnvironmentReq) error { return c.requestBody(ctx, http.MethodPatch, "/api/v0/environments/"+envID, req, nil) } // DialWsep dials an environments command execution interface // See https://github.com/cdr/wsep for details. -func (c Client) DialWsep(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) { +func (c *DefaultClient) DialWsep(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) { return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/wsep", withBaseURL(baseURL)) } // DialExecutor gives a remote execution interface for performing commands inside an environment. -func (c Client) DialExecutor(ctx context.Context, baseURL *url.URL, envID string) (wsep.Execer, error) { +func (c *DefaultClient) DialExecutor(ctx context.Context, baseURL *url.URL, envID string) (wsep.Execer, error) { ws, err := c.DialWsep(ctx, baseURL, envID) if err != nil { return nil, err @@ -236,12 +236,12 @@ func (c Client) DialExecutor(ctx context.Context, baseURL *url.URL, envID string } // DialIDEStatus opens a websocket connection for cpu load metrics on the environment. -func (c Client) DialIDEStatus(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) { +func (c *DefaultClient) DialIDEStatus(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) { return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/ide/api/status", withBaseURL(baseURL)) } // DialEnvironmentBuildLog opens a websocket connection for the environment build log messages. -func (c Client) DialEnvironmentBuildLog(ctx context.Context, envID string) (*websocket.Conn, error) { +func (c *DefaultClient) DialEnvironmentBuildLog(ctx context.Context, envID string) (*websocket.Conn, error) { return c.dialWebsocket(ctx, "/api/private/environments/"+envID+"/watch-update") } @@ -264,7 +264,7 @@ type BuildLogFollowMsg struct { } // FollowEnvironmentBuildLog trails the build log of a Coder environment. -func (c Client) FollowEnvironmentBuildLog(ctx context.Context, envID string) (<-chan BuildLogFollowMsg, error) { +func (c *DefaultClient) FollowEnvironmentBuildLog(ctx context.Context, envID string) (<-chan BuildLogFollowMsg, error) { ch := make(chan BuildLogFollowMsg) ws, err := c.DialEnvironmentBuildLog(ctx, envID) if err != nil { @@ -289,12 +289,12 @@ func (c Client) FollowEnvironmentBuildLog(ctx context.Context, envID string) (<- } // DialEnvironmentStats opens a websocket connection for environment stats. -func (c Client) DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) { +func (c *DefaultClient) DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) { return c.dialWebsocket(ctx, "/api/private/environments/"+envID+"/watch-stats") } // DialResourceLoad opens a websocket connection for cpu load metrics on the environment. -func (c Client) DialResourceLoad(ctx context.Context, envID string) (*websocket.Conn, error) { +func (c *DefaultClient) DialResourceLoad(ctx context.Context, envID string) (*websocket.Conn, error) { return c.dialWebsocket(ctx, "/api/private/environments/"+envID+"/watch-resource-load") } @@ -323,7 +323,7 @@ type buildLogMsg struct { } // WaitForEnvironmentReady will watch the build log and return when done. -func (c Client) WaitForEnvironmentReady(ctx context.Context, envID string) error { +func (c *DefaultClient) WaitForEnvironmentReady(ctx context.Context, envID string) error { conn, err := c.DialEnvironmentBuildLog(ctx, envID) if err != nil { return xerrors.Errorf("%s: dial build log: %w", envID, err) @@ -343,7 +343,7 @@ func (c Client) WaitForEnvironmentReady(ctx context.Context, envID string) error } // EnvironmentByID get the details of an environment by its id. -func (c Client) EnvironmentByID(ctx context.Context, id string) (*Environment, error) { +func (c *DefaultClient) EnvironmentByID(ctx context.Context, id string) (*Environment, error) { var env Environment if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments/"+id, nil, &env); err != nil { return nil, err diff --git a/coder-sdk/image.go b/coder-sdk/image.go index 0290b25e..d2840b24 100644 --- a/coder-sdk/image.go +++ b/coder-sdk/image.go @@ -58,7 +58,7 @@ type UpdateImageReq struct { } // ImportImage creates a new image and optionally a new registry. -func (c Client) ImportImage(ctx context.Context, req ImportImageReq) (*Image, error) { +func (c *DefaultClient) ImportImage(ctx context.Context, req ImportImageReq) (*Image, error) { var img Image if err := c.requestBody(ctx, http.MethodPost, "/api/v0/images", req, &img); err != nil { return nil, err @@ -67,7 +67,7 @@ func (c Client) ImportImage(ctx context.Context, req ImportImageReq) (*Image, er } // OrganizationImages returns all of the images imported for orgID. -func (c Client) OrganizationImages(ctx context.Context, orgID string) ([]Image, error) { +func (c *DefaultClient) OrganizationImages(ctx context.Context, orgID string) ([]Image, error) { var ( imgs []Image query = url.Values{} @@ -82,11 +82,11 @@ func (c Client) OrganizationImages(ctx context.Context, orgID string) ([]Image, } // UpdateImage applies a partial update to an image resource. -func (c Client) UpdateImage(ctx context.Context, imageID string, req UpdateImageReq) error { +func (c *DefaultClient) UpdateImage(ctx context.Context, imageID string, req UpdateImageReq) error { return c.requestBody(ctx, http.MethodPatch, "/api/v0/images/"+imageID, req, nil) } // UpdateImageTags refreshes the latest digests for all tags of the image. -func (c Client) UpdateImageTags(ctx context.Context, imageID string) error { +func (c *DefaultClient) UpdateImageTags(ctx context.Context, imageID string) error { return c.requestBody(ctx, http.MethodPost, "/api/v0/images/"+imageID+"/tags/update", nil, nil) } diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go new file mode 100644 index 00000000..b50a9a4e --- /dev/null +++ b/coder-sdk/interface.go @@ -0,0 +1,216 @@ +package coder + +import ( + "context" + "net/url" + + "cdr.dev/wsep" + "nhooyr.io/websocket" +) + +// Client wraps the Coder HTTP API. +// This is an interface to allow for mocking of coder-sdk client usage. +type Client interface { + // PushActivity pushes CLI activity to Coder. + PushActivity(ctx context.Context, source, envID string) error + + // Me gets the details of the authenticated user. + Me(ctx context.Context) (*User, error) + + // UserByID get the details of a user by their id. + UserByID(ctx context.Context, id string) (*User, error) + + // SSHKey gets the current SSH kepair of the authenticated user. + SSHKey(ctx context.Context) (*SSHKey, error) + + // Users gets the list of user accounts. + Users(ctx context.Context) ([]User, error) + + // UserByEmail gets a user by email. + UserByEmail(ctx context.Context, email string) (*User, error) + + // UpdateUser applyes the partial update to the given user. + UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error + + // UpdateUXState applies a partial update of the user's UX State. + UpdateUXState(ctx context.Context, userID string, uxsPartial map[string]interface{}) error + + // CreateUser creates a new user account. + CreateUser(ctx context.Context, req CreateUserReq) error + + // DeleteUser deletes a user account. + DeleteUser(ctx context.Context, userID string) error + + // SiteConfigAuth fetches the sitewide authentication configuration. + SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) + + // PutSiteConfigAuth sets the sitewide authentication configuration. + PutSiteConfigAuth(ctx context.Context, req ConfigAuth) error + + // SiteConfigOAuth fetches the sitewide git provider OAuth configuration. + SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) + + // PutSiteConfigOAuth sets the sitewide git provider OAuth configuration. + PutSiteConfigOAuth(ctx context.Context, req ConfigOAuth) error + + // SiteSetupModeEnabled fetches the current setup_mode state of a Coder Enterprise deployment. + SiteSetupModeEnabled(ctx context.Context) (bool, error) + + // SiteConfigExtensionMarketplace fetches the extension marketplace configuration. + SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExtensionMarketplace, error) + + // PutSiteConfigExtensionMarketplace sets the extension marketplace configuration. + PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error + + // DeleteDevURL deletes the specified devurl. + DeleteDevURL(ctx context.Context, envID, urlID string) error + + // CreateDevURL inserts a new devurl for the authenticated user. + CreateDevURL(ctx context.Context, envID string, req CreateDevURLReq) error + + // DevURLs fetches the Dev URLs for a given environment. + DevURLs(ctx context.Context, envID string) ([]DevURL, error) + + // PutDevURL updates an existing devurl for the authenticated user. + PutDevURL(ctx context.Context, envID, urlID string, req PutDevURLReq) error + + // CreateEnvironment sends a request to create an environment. + CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) (*Environment, error) + + // ParseTemplate parses a template config. It support both remote repositories and local files. + // If a local file is specified then all other values in the request are ignored. + ParseTemplate(ctx context.Context, req ParseTemplateRequest) (Template, error) + + // CreateEnvironmentFromRepo sends a request to create an environment from a repository. + CreateEnvironmentFromRepo(ctx context.Context, orgID string, req Template) (*Environment, error) + + // Environments lists environments returned by the given filter. + Environments(ctx context.Context) ([]Environment, error) + + // UserEnvironmentsByOrganization gets the list of environments owned by the given user. + UserEnvironmentsByOrganization(ctx context.Context, userID, orgID string) ([]Environment, error) + + // DeleteEnvironment deletes the environment. + DeleteEnvironment(ctx context.Context, envID string) error + + // StopEnvironment stops the environment. + StopEnvironment(ctx context.Context, envID string) error + + // RebuildEnvironment requests that the given envID is rebuilt with no changes to its specification. + RebuildEnvironment(ctx context.Context, envID string) error + + // EditEnvironment modifies the environment specification and initiates a rebuild. + EditEnvironment(ctx context.Context, envID string, req UpdateEnvironmentReq) error + + // DialWsep dials an environments command execution interface + // See https://github.com/cdr/wsep for details. + DialWsep(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) + + // DialExecutor gives a remote execution interface for performing commands inside an environment. + DialExecutor(ctx context.Context, baseURL *url.URL, envID string) (wsep.Execer, error) + + // DialIDEStatus opens a websocket connection for cpu load metrics on the environment. + DialIDEStatus(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) + + // DialEnvironmentBuildLog opens a websocket connection for the environment build log messages. + DialEnvironmentBuildLog(ctx context.Context, envID string) (*websocket.Conn, error) + + // FollowEnvironmentBuildLog trails the build log of a Coder environment. + FollowEnvironmentBuildLog(ctx context.Context, envID string) (<-chan BuildLogFollowMsg, error) + + // DialEnvironmentStats opens a websocket connection for environment stats. + DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) + + // DialResourceLoad opens a websocket connection for cpu load metrics on the environment. + DialResourceLoad(ctx context.Context, envID string) (*websocket.Conn, error) + + // WaitForEnvironmentReady will watch the build log and return when done. + WaitForEnvironmentReady(ctx context.Context, envID string) error + + // EnvironmentByID get the details of an environment by its id. + EnvironmentByID(ctx context.Context, id string) (*Environment, error) + + // ImportImage creates a new image and optionally a new registry. + ImportImage(ctx context.Context, req ImportImageReq) (*Image, error) + + // OrganizationImages returns all of the images imported for orgID. + OrganizationImages(ctx context.Context, orgID string) ([]Image, error) + + // UpdateImage applies a partial update to an image resource. + UpdateImage(ctx context.Context, imageID string, req UpdateImageReq) error + + // UpdateImageTags refreshes the latest digests for all tags of the image. + UpdateImageTags(ctx context.Context, imageID string) error + + // Organizations gets all Organizations. + Organizations(ctx context.Context) ([]Organization, error) + + // OrganizationByID get the Organization by its ID. + OrganizationByID(ctx context.Context, orgID string) (*Organization, error) + + // OrganizationMembers get all members of the given organization. + OrganizationMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) + + // UpdateOrganization applys a partial update of an Organization resource. + UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationReq) error + + // CreateOrganization creates a new Organization in Coder Enterprise. + CreateOrganization(ctx context.Context, req CreateOrganizationReq) error + + // DeleteOrganization deletes an organization. + DeleteOrganization(ctx context.Context, orgID string) error + + // Registries fetches all registries in an organization. + Registries(ctx context.Context, orgID string) ([]Registry, error) + + // RegistryByID fetches a registry resource by its ID. + RegistryByID(ctx context.Context, registryID string) (*Registry, error) + + // UpdateRegistry applies a partial update to a registry resource. + UpdateRegistry(ctx context.Context, registryID string, req UpdateRegistryReq) error + + // DeleteRegistry deletes a registry resource by its ID. + DeleteRegistry(ctx context.Context, registryID string) error + + // CreateImageTag creates a new image tag resource. + CreateImageTag(ctx context.Context, imageID string, req CreateImageTagReq) (*ImageTag, error) + + // DeleteImageTag deletes an image tag resource. + DeleteImageTag(ctx context.Context, imageID, tag string) error + + // ImageTags fetch all image tags. + ImageTags(ctx context.Context, imageID string) ([]ImageTag, error) + + // ImageTagByID fetch an image tag by ID. + ImageTagByID(ctx context.Context, imageID, tagID string) (*ImageTag, error) + + // CreateAPIToken creates a new APIToken for making authenticated requests to Coder Enterprise. + CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (string, error) + + // APITokens fetches all APITokens owned by the given user. + APITokens(ctx context.Context, userID string) ([]APIToken, error) + + // APITokenByID fetches the metadata for a given APIToken. + APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) + + // DeleteAPIToken deletes an APIToken. + DeleteAPIToken(ctx context.Context, userID, tokenID string) error + + // RegenerateAPIToken regenerates the given APIToken and returns the new value. + RegenerateAPIToken(ctx context.Context, userID, tokenID string) (string, error) + + // APIVersion parses the coder-version http header from an authenticated request. + APIVersion(ctx context.Context) (string, error) + + // WorkspaceProviderByID fetches a workspace provider entity by its unique ID. + WorkspaceProviderByID(ctx context.Context, id string) (*WorkspaceProvider, error) + + // WorkspaceProviders fetches all workspace providers known to the Coder control plane. + WorkspaceProviders(ctx context.Context) ([]WorkspaceProvider, error) + + // CreateWorkspaceProvider creates a new WorkspaceProvider entity. + CreateWorkspaceProvider(ctx context.Context, req CreateWorkspaceProviderReq) (*CreateWorkspaceProviderRes, error) + + // DeleteWorkspaceProviderByID deletes a workspace provider entity from the Coder control plane. + DeleteWorkspaceProviderByID(ctx context.Context, id string) error +} diff --git a/coder-sdk/org.go b/coder-sdk/org.go index 539b3b73..2ecb8d2a 100644 --- a/coder-sdk/org.go +++ b/coder-sdk/org.go @@ -37,7 +37,7 @@ const ( ) // Organizations gets all Organizations. -func (c Client) Organizations(ctx context.Context) ([]Organization, error) { +func (c *DefaultClient) Organizations(ctx context.Context) ([]Organization, error) { var orgs []Organization if err := c.requestBody(ctx, http.MethodGet, "/api/v0/orgs", nil, &orgs); err != nil { return nil, err @@ -46,7 +46,7 @@ func (c Client) Organizations(ctx context.Context) ([]Organization, error) { } // OrganizationByID get the Organization by its ID. -func (c Client) OrganizationByID(ctx context.Context, orgID string) (*Organization, error) { +func (c *DefaultClient) OrganizationByID(ctx context.Context, orgID string) (*Organization, error) { var org Organization err := c.requestBody(ctx, http.MethodGet, "/api/v0/orgs/"+orgID, nil, &org) if err != nil { @@ -56,7 +56,7 @@ func (c Client) OrganizationByID(ctx context.Context, orgID string) (*Organizati } // OrganizationMembers get all members of the given organization. -func (c Client) OrganizationMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) { +func (c *DefaultClient) OrganizationMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) { var members []OrganizationUser if err := c.requestBody(ctx, http.MethodGet, "/api/v0/orgs/"+orgID+"/members", nil, &members); err != nil { return nil, err @@ -75,7 +75,7 @@ type UpdateOrganizationReq struct { } // UpdateOrganization applys a partial update of an Organization resource. -func (c Client) UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationReq) error { +func (c *DefaultClient) UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationReq) error { return c.requestBody(ctx, http.MethodPatch, "/api/v0/orgs/"+orgID, req, nil) } @@ -91,11 +91,11 @@ type CreateOrganizationReq struct { } // CreateOrganization creates a new Organization in Coder Enterprise. -func (c Client) CreateOrganization(ctx context.Context, req CreateOrganizationReq) error { +func (c *DefaultClient) CreateOrganization(ctx context.Context, req CreateOrganizationReq) error { return c.requestBody(ctx, http.MethodPost, "/api/v0/orgs", req, nil) } // DeleteOrganization deletes an organization. -func (c Client) DeleteOrganization(ctx context.Context, orgID string) error { +func (c *DefaultClient) DeleteOrganization(ctx context.Context, orgID string) error { return c.requestBody(ctx, http.MethodDelete, "/api/v0/orgs/"+orgID, nil, nil) } diff --git a/coder-sdk/registries.go b/coder-sdk/registries.go index 820a97c8..074155b3 100644 --- a/coder-sdk/registries.go +++ b/coder-sdk/registries.go @@ -18,7 +18,7 @@ type Registry struct { } // Registries fetches all registries in an organization. -func (c Client) Registries(ctx context.Context, orgID string) ([]Registry, error) { +func (c *DefaultClient) Registries(ctx context.Context, orgID string) ([]Registry, error) { var ( r []Registry query = url.Values{} @@ -33,7 +33,7 @@ func (c Client) Registries(ctx context.Context, orgID string) ([]Registry, error } // RegistryByID fetches a registry resource by its ID. -func (c Client) RegistryByID(ctx context.Context, registryID string) (*Registry, error) { +func (c *DefaultClient) RegistryByID(ctx context.Context, registryID string) (*Registry, error) { var r Registry if err := c.requestBody(ctx, http.MethodGet, "/api/v0/registries/"+registryID, nil, &r); err != nil { return nil, err @@ -50,11 +50,11 @@ type UpdateRegistryReq struct { } // UpdateRegistry applies a partial update to a registry resource. -func (c Client) UpdateRegistry(ctx context.Context, registryID string, req UpdateRegistryReq) error { +func (c *DefaultClient) UpdateRegistry(ctx context.Context, registryID string, req UpdateRegistryReq) error { return c.requestBody(ctx, http.MethodPatch, "/api/v0/registries/"+registryID, req, nil) } // DeleteRegistry deletes a registry resource by its ID. -func (c Client) DeleteRegistry(ctx context.Context, registryID string) error { +func (c *DefaultClient) DeleteRegistry(ctx context.Context, registryID string) error { return c.requestBody(ctx, http.MethodDelete, "/api/v0/registries/"+registryID, nil, nil) } diff --git a/coder-sdk/request.go b/coder-sdk/request.go index 40bb2563..b24ffefa 100644 --- a/coder-sdk/request.go +++ b/coder-sdk/request.go @@ -48,13 +48,8 @@ func withBody(w io.Reader) func(o *requestOptions) { } // request is a helper to set the cookie, marshal the payload and execute the request. -func (c Client) request(ctx context.Context, method, path string, in interface{}, options ...requestOption) (*http.Response, error) { - // Create a default http client with the auth in the cookie. - client, err := c.newHTTPClient() - if err != nil { - return nil, xerrors.Errorf("new http client: %w", err) - } - url := *c.BaseURL +func (c *DefaultClient) request(ctx context.Context, method, path string, in interface{}, options ...requestOption) (*http.Response, error) { + url := *c.baseURL var config requestOptions for _, o := range options { @@ -88,17 +83,22 @@ func (c Client) request(ctx context.Context, method, path string, in interface{} return nil, xerrors.Errorf("create request: %w", err) } - if config.Headers != nil { + if config.Headers == nil { + req.Header = http.Header{} + } else { req.Header = config.Headers } + // Provide the session token in a header + req.Header.Set("Session-Token", c.token) + // Execute the request. - return client.Do(req) + return c.httpClient.Do(req) } // requestBody is a helper extending the Client.request helper, checking the response code // and decoding the response payload. -func (c Client) requestBody(ctx context.Context, method, path string, in, out interface{}, opts ...requestOption) error { +func (c *DefaultClient) requestBody(ctx context.Context, method, path string, in, out interface{}, opts ...requestOption) error { resp, err := c.request(ctx, method, path, in, opts...) if err != nil { return xerrors.Errorf("Execute request: %q", err) diff --git a/coder-sdk/tags.go b/coder-sdk/tags.go index 2af2cf76..7e4563a9 100644 --- a/coder-sdk/tags.go +++ b/coder-sdk/tags.go @@ -40,7 +40,7 @@ type CreateImageTagReq struct { } // CreateImageTag creates a new image tag resource. -func (c Client) CreateImageTag(ctx context.Context, imageID string, req CreateImageTagReq) (*ImageTag, error) { +func (c *DefaultClient) CreateImageTag(ctx context.Context, imageID string, req CreateImageTagReq) (*ImageTag, error) { var tag ImageTag if err := c.requestBody(ctx, http.MethodPost, "/api/v0/images/"+imageID+"/tags", req, tag); err != nil { return nil, err @@ -49,12 +49,12 @@ func (c Client) CreateImageTag(ctx context.Context, imageID string, req CreateIm } // DeleteImageTag deletes an image tag resource. -func (c Client) DeleteImageTag(ctx context.Context, imageID, tag string) error { +func (c *DefaultClient) DeleteImageTag(ctx context.Context, imageID, tag string) error { return c.requestBody(ctx, http.MethodDelete, "/api/v0/images/"+imageID+"/tags/"+tag, nil, nil) } // ImageTags fetch all image tags. -func (c Client) ImageTags(ctx context.Context, imageID string) ([]ImageTag, error) { +func (c *DefaultClient) ImageTags(ctx context.Context, imageID string) ([]ImageTag, error) { var tags []ImageTag if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images/"+imageID+"/tags", nil, &tags); err != nil { return nil, err @@ -63,7 +63,7 @@ func (c Client) ImageTags(ctx context.Context, imageID string) ([]ImageTag, erro } // ImageTagByID fetch an image tag by ID. -func (c Client) ImageTagByID(ctx context.Context, imageID, tagID string) (*ImageTag, error) { +func (c *DefaultClient) ImageTagByID(ctx context.Context, imageID, tagID string) (*ImageTag, error) { var tag ImageTag if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images/"+imageID+"/tags/"+tagID, nil, &tag); err != nil { return nil, err diff --git a/coder-sdk/tokens.go b/coder-sdk/tokens.go index d6d26c12..b707951d 100644 --- a/coder-sdk/tokens.go +++ b/coder-sdk/tokens.go @@ -25,7 +25,7 @@ type createAPITokenResp struct { } // CreateAPIToken creates a new APIToken for making authenticated requests to Coder Enterprise. -func (c Client) CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (token string, _ error) { +func (c *DefaultClient) CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (token string, _ error) { var resp createAPITokenResp err := c.requestBody(ctx, http.MethodPost, "/api/v0/api-keys/"+userID, req, &resp) if err != nil { @@ -35,7 +35,7 @@ func (c Client) CreateAPIToken(ctx context.Context, userID string, req CreateAPI } // APITokens fetches all APITokens owned by the given user. -func (c Client) APITokens(ctx context.Context, userID string) ([]APIToken, error) { +func (c *DefaultClient) APITokens(ctx context.Context, userID string) ([]APIToken, error) { var tokens []APIToken if err := c.requestBody(ctx, http.MethodGet, "/api/v0/api-keys/"+userID, nil, &tokens); err != nil { return nil, err @@ -44,7 +44,7 @@ func (c Client) APITokens(ctx context.Context, userID string) ([]APIToken, error } // APITokenByID fetches the metadata for a given APIToken. -func (c Client) APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) { +func (c *DefaultClient) APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) { var token APIToken if err := c.requestBody(ctx, http.MethodGet, "/api/v0/api-keys/"+userID+"/"+tokenID, nil, &token); err != nil { return nil, err @@ -53,12 +53,12 @@ func (c Client) APITokenByID(ctx context.Context, userID, tokenID string) (*APIT } // DeleteAPIToken deletes an APIToken. -func (c Client) DeleteAPIToken(ctx context.Context, userID, tokenID string) error { +func (c *DefaultClient) DeleteAPIToken(ctx context.Context, userID, tokenID string) error { return c.requestBody(ctx, http.MethodDelete, "/api/v0/api-keys/"+userID+"/"+tokenID, nil, nil) } // RegenerateAPIToken regenerates the given APIToken and returns the new value. -func (c Client) RegenerateAPIToken(ctx context.Context, userID, tokenID string) (token string, _ error) { +func (c *DefaultClient) RegenerateAPIToken(ctx context.Context, userID, tokenID string) (token string, _ error) { var resp createAPITokenResp if err := c.requestBody(ctx, http.MethodPost, "/api/v0/api-keys/"+userID+"/"+tokenID+"/regen", nil, &resp); err != nil { return "", err diff --git a/coder-sdk/users.go b/coder-sdk/users.go index af82f2ea..d53c6fab 100644 --- a/coder-sdk/users.go +++ b/coder-sdk/users.go @@ -42,12 +42,12 @@ const ( ) // Me gets the details of the authenticated user. -func (c Client) Me(ctx context.Context) (*User, error) { +func (c *DefaultClient) Me(ctx context.Context) (*User, error) { return c.UserByID(ctx, Me) } // UserByID get the details of a user by their id. -func (c Client) UserByID(ctx context.Context, id string) (*User, error) { +func (c *DefaultClient) UserByID(ctx context.Context, id string) (*User, error) { var u User if err := c.requestBody(ctx, http.MethodGet, "/api/v0/users/"+id, nil, &u); err != nil { return nil, err @@ -62,7 +62,7 @@ type SSHKey struct { } // SSHKey gets the current SSH kepair of the authenticated user. -func (c Client) SSHKey(ctx context.Context) (*SSHKey, error) { +func (c *DefaultClient) SSHKey(ctx context.Context) (*SSHKey, error) { var key SSHKey if err := c.requestBody(ctx, http.MethodGet, "/api/v0/users/me/sshkey", nil, &key); err != nil { return nil, err @@ -71,7 +71,7 @@ func (c Client) SSHKey(ctx context.Context) (*SSHKey, error) { } // Users gets the list of user accounts. -func (c Client) Users(ctx context.Context) ([]User, error) { +func (c *DefaultClient) Users(ctx context.Context) ([]User, error) { var u []User if err := c.requestBody(ctx, http.MethodGet, "/api/v0/users", nil, &u); err != nil { return nil, err @@ -80,7 +80,7 @@ func (c Client) Users(ctx context.Context) ([]User, error) { } // UserByEmail gets a user by email. -func (c Client) UserByEmail(ctx context.Context, email string) (*User, error) { +func (c *DefaultClient) UserByEmail(ctx context.Context, email string) (*User, error) { if email == Me { return c.Me(ctx) } @@ -110,12 +110,12 @@ type UpdateUserReq struct { } // UpdateUser applyes the partial update to the given user. -func (c Client) UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error { +func (c *DefaultClient) UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error { return c.requestBody(ctx, http.MethodPatch, "/api/v0/users/"+userID, req, nil) } // UpdateUXState applies a partial update of the user's UX State. -func (c Client) UpdateUXState(ctx context.Context, userID string, uxsPartial map[string]interface{}) error { +func (c *DefaultClient) UpdateUXState(ctx context.Context, userID string, uxsPartial map[string]interface{}) error { if err := c.requestBody(ctx, http.MethodPut, "/api/private/users/"+userID+"/ux-state", uxsPartial, nil); err != nil { return err } @@ -134,11 +134,11 @@ type CreateUserReq struct { } // CreateUser creates a new user account. -func (c Client) CreateUser(ctx context.Context, req CreateUserReq) error { +func (c *DefaultClient) CreateUser(ctx context.Context, req CreateUserReq) error { return c.requestBody(ctx, http.MethodPost, "/api/v0/users", req, nil) } // DeleteUser deletes a user account. -func (c Client) DeleteUser(ctx context.Context, userID string) error { +func (c *DefaultClient) DeleteUser(ctx context.Context, userID string) error { return c.requestBody(ctx, http.MethodDelete, "/api/v0/users/"+userID, nil, nil) } diff --git a/coder-sdk/version.go b/coder-sdk/version.go index 5407fec0..dd1ae9ec 100644 --- a/coder-sdk/version.go +++ b/coder-sdk/version.go @@ -6,7 +6,7 @@ import ( ) // APIVersion parses the coder-version http header from an authenticated request. -func (c Client) APIVersion(ctx context.Context) (string, error) { +func (c *DefaultClient) APIVersion(ctx context.Context) (string, error) { const coderVersionHeaderKey = "coder-version" resp, err := c.request(ctx, http.MethodGet, "/api", nil) if err != nil { diff --git a/coder-sdk/workspace_providers.go b/coder-sdk/workspace_providers.go index d397c9f5..e834d79b 100644 --- a/coder-sdk/workspace_providers.go +++ b/coder-sdk/workspace_providers.go @@ -32,7 +32,7 @@ const ( ) // WorkspaceProviderByID fetches a workspace provider entity by its unique ID. -func (c *Client) WorkspaceProviderByID(ctx context.Context, id string) (*WorkspaceProvider, error) { +func (c *DefaultClient) WorkspaceProviderByID(ctx context.Context, id string) (*WorkspaceProvider, error) { var wp WorkspaceProvider err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+id, nil, &wp) if err != nil { @@ -42,7 +42,7 @@ func (c *Client) WorkspaceProviderByID(ctx context.Context, id string) (*Workspa } // WorkspaceProviders fetches all workspace providers known to the Coder control plane. -func (c *Client) WorkspaceProviders(ctx context.Context) ([]WorkspaceProvider, error) { +func (c *DefaultClient) WorkspaceProviders(ctx context.Context) ([]WorkspaceProvider, error) { var providers []WorkspaceProvider err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools", nil, &providers) if err != nil { @@ -65,7 +65,7 @@ type CreateWorkspaceProviderRes struct { } // CreateWorkspaceProvider creates a new WorkspaceProvider entity. -func (c *Client) CreateWorkspaceProvider(ctx context.Context, req CreateWorkspaceProviderReq) (*CreateWorkspaceProviderRes, error) { +func (c *DefaultClient) CreateWorkspaceProvider(ctx context.Context, req CreateWorkspaceProviderReq) (*CreateWorkspaceProviderRes, error) { var res CreateWorkspaceProviderRes err := c.requestBody(ctx, http.MethodPost, "/api/private/resource-pools", req, &res) if err != nil { @@ -75,6 +75,6 @@ func (c *Client) CreateWorkspaceProvider(ctx context.Context, req CreateWorkspac } // DeleteWorkspaceProviderByID deletes a workspace provider entity from the Coder control plane. -func (c *Client) DeleteWorkspaceProviderByID(ctx context.Context, id string) error { +func (c *DefaultClient) DeleteWorkspaceProviderByID(ctx context.Context, id string) error { return c.requestBody(ctx, http.MethodDelete, "/api/private/resource-pools/"+id, nil, nil) } diff --git a/coder-sdk/ws.go b/coder-sdk/ws.go index 2a27ed99..6f6a920f 100644 --- a/coder-sdk/ws.go +++ b/coder-sdk/ws.go @@ -8,9 +8,9 @@ import ( ) // dialWebsocket establish the websocket connection while setting the authentication header. -func (c Client) dialWebsocket(ctx context.Context, path string, options ...requestOption) (*websocket.Conn, error) { +func (c *DefaultClient) dialWebsocket(ctx context.Context, path string, options ...requestOption) (*websocket.Conn, error) { // Make a copy of the url so we can update the scheme to ws(s) without mutating the state. - url := *c.BaseURL + url := *c.baseURL var config requestOptions for _, o := range options { o(&config) @@ -20,7 +20,10 @@ func (c Client) dialWebsocket(ctx context.Context, path string, options ...reque } url.Path = path - conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{HTTPHeader: http.Header{"Session-Token": {c.Token}}}) + headers := http.Header{} + headers.Set("Session-Token", c.token) + + conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{HTTPHeader: headers}) if err != nil { if resp != nil { return nil, bodyError(resp) diff --git a/internal/activity/pusher.go b/internal/activity/pusher.go index e54786e7..f59630a1 100644 --- a/internal/activity/pusher.go +++ b/internal/activity/pusher.go @@ -19,12 +19,12 @@ type Pusher struct { envID string source string - client *coder.Client + client coder.Client rate *rate.Limiter // Use a rate limiter to control the sampling rate. } // NewPusher instantiates a new instance of Pusher. -func NewPusher(c *coder.Client, envID, source string) *Pusher { +func NewPusher(c coder.Client, envID, source string) *Pusher { return &Pusher{ envID: envID, source: source, diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index 0322f4ab..a6e73d16 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -23,7 +23,7 @@ var errNeedLogin = clog.Fatal( const tokenEnv = "CODER_TOKEN" const urlEnv = "CODER_URL" -func newClient(ctx context.Context) (*coder.Client, error) { +func newClient(ctx context.Context) (coder.Client, error) { var ( err error sessionToken = os.Getenv(tokenEnv) @@ -47,9 +47,12 @@ func newClient(ctx context.Context) (*coder.Client, error) { return nil, xerrors.Errorf("url malformed: %w try running \"coder login\" with a valid URL", err) } - c := &coder.Client{ + c, err := coder.NewClient(coder.ClientOptions{ BaseURL: u, Token: sessionToken, + }) + if err != nil { + return nil, xerrors.Errorf("failed to create new coder.Client: %w", err) } apiVersion, err := c.APIVersion(ctx) diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go index 964d1857..964a7c79 100644 --- a/internal/cmd/ceapi.go +++ b/internal/cmd/ceapi.go @@ -32,7 +32,7 @@ func lookupUserOrgs(user *coder.User, orgs []coder.Organization) []coder.Organiz } // getEnvs returns all environments for the user. -func getEnvs(ctx context.Context, client *coder.Client, email string) ([]coder.Environment, error) { +func getEnvs(ctx context.Context, client coder.Client, email string) ([]coder.Environment, error) { user, err := client.UserByEmail(ctx, email) if err != nil { return nil, xerrors.Errorf("get user: %w", err) @@ -61,7 +61,7 @@ func getEnvs(ctx context.Context, client *coder.Client, email string) ([]coder.E // searchForEnv searches a user's environments to find the specified envName. If none is found, the haystack of // environment names is returned. -func searchForEnv(ctx context.Context, client *coder.Client, envName, userEmail string) (_ *coder.Environment, haystack []string, _ error) { +func searchForEnv(ctx context.Context, client coder.Client, envName, userEmail string) (_ *coder.Environment, haystack []string, _ error) { envs, err := getEnvs(ctx, client, userEmail) if err != nil { return nil, nil, xerrors.Errorf("get environments: %w", err) @@ -79,7 +79,7 @@ func searchForEnv(ctx context.Context, client *coder.Client, envName, userEmail } // findEnv returns a single environment by name (if it exists.). -func findEnv(ctx context.Context, client *coder.Client, envName, userEmail string) (*coder.Environment, error) { +func findEnv(ctx context.Context, client coder.Client, envName, userEmail string) (*coder.Environment, error) { env, haystack, err := searchForEnv(ctx, client, envName, userEmail) if err != nil { return nil, clog.Fatal( @@ -98,7 +98,7 @@ type findImgConf struct { orgName string } -func findImg(ctx context.Context, client *coder.Client, conf findImgConf) (*coder.Image, error) { +func findImg(ctx context.Context, client coder.Client, conf findImgConf) (*coder.Image, error) { switch { case conf.email == "": return nil, xerrors.New("user email unset") @@ -150,7 +150,7 @@ type getImgsConf struct { orgName string } -func getImgs(ctx context.Context, client *coder.Client, conf getImgsConf) ([]coder.Image, error) { +func getImgs(ctx context.Context, client coder.Client, conf getImgsConf) ([]coder.Image, error) { u, err := client.UserByEmail(ctx, conf.email) if err != nil { return nil, err @@ -182,7 +182,7 @@ func getImgs(ctx context.Context, client *coder.Client, conf getImgsConf) ([]cod return nil, xerrors.Errorf("org name %q not found", conf.orgName) } -func isMultiOrgMember(ctx context.Context, client *coder.Client, email string) (bool, error) { +func isMultiOrgMember(ctx context.Context, client coder.Client, email string) (bool, error) { u, err := client.UserByEmail(ctx, email) if err != nil { return false, xerrors.New("email not found") diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index a63d2b3a..b8db5687 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -166,7 +166,7 @@ func sshAvailable(envs []coderutil.EnvWithWorkspaceProvider) bool { return false } -func writeSSHKey(ctx context.Context, client *coder.Client, privateKeyPath string) error { +func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string) error { key, err := client.SSHKey(ctx) if err != nil { return err @@ -235,7 +235,7 @@ func readStr(filename string) (string, error) { return string(contents), nil } -func writeSSHUXState(ctx context.Context, client *coder.Client, userID string, envs []coder.Environment) { +func writeSSHUXState(ctx context.Context, client coder.Client, userID string, envs []coder.Environment) { // Create a map of env.ID -> true to indicate to the web client that all // current environments have SSH configured cliSSHConfigured := make(map[string]bool) diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index db755509..8c68a0e1 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -531,7 +531,7 @@ type updateConf struct { orgName string } -func buildUpdateReq(ctx context.Context, client *coder.Client, conf updateConf) (*coder.UpdateEnvironmentReq, error) { +func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) (*coder.UpdateEnvironmentReq, error) { var ( updateReq coder.UpdateEnvironmentReq defaultCPUCores float32 diff --git a/internal/cmd/login.go b/internal/cmd/login.go index ce3a7d41..7c782310 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -66,13 +66,20 @@ func newLocalListener() (net.Listener, error) { // pingAPI creates a client from the given url/token and try to exec an api call. // Not using the SDK as we want to verify the url/token pair before storing the config files. func pingAPI(ctx context.Context, envURL *url.URL, token string) error { - client := &coder.Client{BaseURL: envURL, Token: token} + client, err := coder.NewClient(coder.ClientOptions{ + BaseURL: envURL, + Token: token, + }) + if err != nil { + return xerrors.Errorf("failed to create coder.Client: %w", err) + } + if apiVersion, err := client.APIVersion(ctx); err == nil { if apiVersion != "" && !version.VersionsMatch(apiVersion) { logVersionMismatchError(apiVersion) } } - _, err := client.Me(ctx) + _, err = client.Me(ctx) if err != nil { return xerrors.Errorf("call api: %w", err) } diff --git a/internal/cmd/rebuild.go b/internal/cmd/rebuild.go index 7b39d39d..628825c4 100644 --- a/internal/cmd/rebuild.go +++ b/internal/cmd/rebuild.go @@ -81,7 +81,7 @@ coder envs rebuild backend-env --force`, // trailBuildLogs follows the build log for a given environment and prints the staged // output with loaders and success/failure indicators for each stage. -func trailBuildLogs(ctx context.Context, client *coder.Client, envID string) error { +func trailBuildLogs(ctx context.Context, client coder.Client, envID string) error { const check = "✅" const failure = "❌" diff --git a/internal/cmd/shell.go b/internal/cmd/shell.go index e3a2ba2e..35df969d 100644 --- a/internal/cmd/shell.go +++ b/internal/cmd/shell.go @@ -214,7 +214,7 @@ func rebuildPrompt(env *coder.Environment) (prompt func() error) { // Conditions for rebuilding are: // - Environment is offline // - Environment has rebuild messages requiring a rebuild -func checkAndRebuildEnvironment(ctx context.Context, client *coder.Client, env *coder.Environment) error { +func checkAndRebuildEnvironment(ctx context.Context, client coder.Client, env *coder.Environment) error { var err error rebuildPrompt := rebuildPrompt(env) // Fetch the prompt for rebuilding envs w/ reason @@ -309,7 +309,7 @@ func sendResizeEvents(ctx context.Context, termFD uintptr, process wsep.Process) } } -func runCommand(ctx context.Context, client *coder.Client, env *coder.Environment, command string, args []string) error { +func runCommand(ctx context.Context, client coder.Client, env *coder.Environment, command string, args []string) error { termFD := os.Stdout.Fd() isInteractive := terminal.IsTerminal(int(termFD)) diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go index 98b2ea26..8c3308ff 100644 --- a/internal/cmd/urls.go +++ b/internal/cmd/urls.go @@ -260,7 +260,7 @@ func removeDevURL(cmd *cobra.Command, args []string) error { } // urlList returns the list of active devURLs from the cemanager. -func urlList(ctx context.Context, client *coder.Client, envName string) ([]coder.DevURL, error) { +func urlList(ctx context.Context, client coder.Client, envName string) ([]coder.DevURL, error) { env, err := findEnv(ctx, client, envName, coder.Me) if err != nil { return nil, err diff --git a/internal/coderutil/env.go b/internal/coderutil/env.go index 784b606d..fff6072d 100644 --- a/internal/coderutil/env.go +++ b/internal/coderutil/env.go @@ -12,7 +12,7 @@ import ( // DialEnvWsep dials the executor endpoint using the https://github.com/cdr/wsep message protocol. // The proper workspace provider envproxy access URL is used. -func DialEnvWsep(ctx context.Context, client *coder.Client, env *coder.Environment) (*websocket.Conn, error) { +func DialEnvWsep(ctx context.Context, client coder.Client, env *coder.Environment) (*websocket.Conn, error) { workspaceProvider, err := client.WorkspaceProviderByID(ctx, env.ResourcePoolID) if err != nil { return nil, xerrors.Errorf("get env workspace provider: %w", err) @@ -36,7 +36,7 @@ type EnvWithWorkspaceProvider struct { } // EnvsWithProvider performs the composition of each Environment with its associated WorkspaceProvider. -func EnvsWithProvider(ctx context.Context, client *coder.Client, envs []coder.Environment) ([]EnvWithWorkspaceProvider, error) { +func EnvsWithProvider(ctx context.Context, client coder.Client, envs []coder.Environment) ([]EnvWithWorkspaceProvider, error) { pooledEnvs := make([]EnvWithWorkspaceProvider, 0, len(envs)) providers, err := client.WorkspaceProviders(ctx) if err != nil { diff --git a/internal/sync/singlefile.go b/internal/sync/singlefile.go index 5cb91f7f..0e15e354 100644 --- a/internal/sync/singlefile.go +++ b/internal/sync/singlefile.go @@ -18,7 +18,7 @@ import ( ) // SingleFile copies the given file into the remote dir or remote path of the given coder.Environment. -func SingleFile(ctx context.Context, local, remoteDir string, env *coder.Environment, client *coder.Client) error { +func SingleFile(ctx context.Context, local, remoteDir string, env *coder.Environment, client coder.Client) error { conn, err := coderutil.DialEnvWsep(ctx, client, env) if err != nil { return xerrors.Errorf("dial remote execer: %w", err) diff --git a/internal/sync/sync.go b/internal/sync/sync.go index 076d4124..e9f16be8 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -42,7 +42,7 @@ type Sync struct { DisableMetrics bool Env coder.Environment - Client *coder.Client + Client coder.Client } // See https://lxadm.com/Rsync_exit_codes#List_of_standard_rsync_exit_codes.