package codersdk import ( "context" "encoding/json" "net/http" "strings" "time" "golang.org/x/mod/semver" "golang.org/x/xerrors" ) // Entitlement represents whether a feature is licensed. type Entitlement string const ( EntitlementEntitled Entitlement = "entitled" EntitlementGracePeriod Entitlement = "grace_period" EntitlementNotEntitled Entitlement = "not_entitled" ) // FeatureName represents the internal name of a feature. // To add a new feature, add it to this set of enums as well as the FeatureNames // array below. type FeatureName string const ( FeatureUserLimit FeatureName = "user_limit" FeatureAuditLog FeatureName = "audit_log" FeatureBrowserOnly FeatureName = "browser_only" FeatureSCIM FeatureName = "scim" FeatureTemplateRBAC FeatureName = "template_rbac" FeatureHighAvailability FeatureName = "high_availability" FeatureMultipleGitAuth FeatureName = "multiple_git_auth" FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons" FeatureAppearance FeatureName = "appearance" ) // FeatureNames must be kept in-sync with the Feature enum above. var FeatureNames = []FeatureName{ FeatureUserLimit, FeatureAuditLog, FeatureBrowserOnly, FeatureSCIM, FeatureTemplateRBAC, FeatureHighAvailability, FeatureMultipleGitAuth, FeatureExternalProvisionerDaemons, FeatureAppearance, } // Humanize returns the feature name in a human-readable format. func (n FeatureName) Humanize() string { switch n { case FeatureTemplateRBAC: return "Template RBAC" case FeatureSCIM: return "SCIM" default: return strings.Title(strings.ReplaceAll(string(n), "_", " ")) } } // AlwaysEnable returns if the feature is always enabled if entitled. // Warning: We don't know if we need this functionality. // This method may disappear at any time. func (n FeatureName) AlwaysEnable() bool { return map[FeatureName]bool{ FeatureMultipleGitAuth: true, FeatureExternalProvisionerDaemons: true, FeatureAppearance: true, }[n] } type Feature struct { Entitlement Entitlement `json:"entitlement"` Enabled bool `json:"enabled"` Limit *int64 `json:"limit,omitempty"` Actual *int64 `json:"actual,omitempty"` } type Entitlements struct { Features map[FeatureName]Feature `json:"features"` Warnings []string `json:"warnings"` Errors []string `json:"errors"` HasLicense bool `json:"has_license"` Trial bool `json:"trial"` // DEPRECATED: use Experiments instead. Experimental bool `json:"experimental"` } func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/entitlements", nil) if err != nil { return Entitlements{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return Entitlements{}, ReadBodyAsError(res) } var ent Entitlements return ent, json.NewDecoder(res.Body).Decode(&ent) } // DeploymentConfig is the central configuration for the coder server. type DeploymentConfig struct { AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"` WildcardAccessURL *DeploymentConfigField[string] `json:"wildcard_access_url" typescript:",notnull"` HTTPAddress *DeploymentConfigField[string] `json:"http_address" typescript:",notnull"` AutobuildPollInterval *DeploymentConfigField[time.Duration] `json:"autobuild_poll_interval" typescript:",notnull"` DERP *DERP `json:"derp" typescript:",notnull"` GitAuth *DeploymentConfigField[[]GitAuthConfig] `json:"gitauth" typescript:",notnull"` Prometheus *PrometheusConfig `json:"prometheus" typescript:",notnull"` Pprof *PprofConfig `json:"pprof" typescript:",notnull"` ProxyTrustedHeaders *DeploymentConfigField[[]string] `json:"proxy_trusted_headers" typescript:",notnull"` ProxyTrustedOrigins *DeploymentConfigField[[]string] `json:"proxy_trusted_origins" typescript:",notnull"` CacheDirectory *DeploymentConfigField[string] `json:"cache_directory" typescript:",notnull"` InMemoryDatabase *DeploymentConfigField[bool] `json:"in_memory_database" typescript:",notnull"` PostgresURL *DeploymentConfigField[string] `json:"pg_connection_url" typescript:",notnull"` OAuth2 *OAuth2Config `json:"oauth2" typescript:",notnull"` OIDC *OIDCConfig `json:"oidc" typescript:",notnull"` Telemetry *TelemetryConfig `json:"telemetry" typescript:",notnull"` TLS *TLSConfig `json:"tls" typescript:",notnull"` Trace *TraceConfig `json:"trace" typescript:",notnull"` SecureAuthCookie *DeploymentConfigField[bool] `json:"secure_auth_cookie" typescript:",notnull"` SSHKeygenAlgorithm *DeploymentConfigField[string] `json:"ssh_keygen_algorithm" typescript:",notnull"` MetricsCacheRefreshInterval *DeploymentConfigField[time.Duration] `json:"metrics_cache_refresh_interval" typescript:",notnull"` AgentStatRefreshInterval *DeploymentConfigField[time.Duration] `json:"agent_stat_refresh_interval" typescript:",notnull"` AgentFallbackTroubleshootingURL *DeploymentConfigField[string] `json:"agent_fallback_troubleshooting_url" typescript:",notnull"` AuditLogging *DeploymentConfigField[bool] `json:"audit_logging" typescript:",notnull"` BrowserOnly *DeploymentConfigField[bool] `json:"browser_only" typescript:",notnull"` SCIMAPIKey *DeploymentConfigField[string] `json:"scim_api_key" typescript:",notnull"` Provisioner *ProvisionerConfig `json:"provisioner" typescript:",notnull"` RateLimit *RateLimitConfig `json:"rate_limit" typescript:",notnull"` Experiments *DeploymentConfigField[[]string] `json:"experiments" typescript:",notnull"` UpdateCheck *DeploymentConfigField[bool] `json:"update_check" typescript:",notnull"` MaxTokenLifetime *DeploymentConfigField[time.Duration] `json:"max_token_lifetime" typescript:",notnull"` Swagger *SwaggerConfig `json:"swagger" typescript:",notnull"` Logging *LoggingConfig `json:"logging" typescript:",notnull"` Dangerous *DangerousConfig `json:"dangerous" typescript:",notnull"` DisablePathApps *DeploymentConfigField[bool] `json:"disable_path_apps" typescript:",notnull"` // DEPRECATED: Use HTTPAddress or TLS.Address instead. Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"` // DEPRECATED: Use Experiments instead. Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"` } type DERP struct { Server *DERPServerConfig `json:"server" typescript:",notnull"` Config *DERPConfig `json:"config" typescript:",notnull"` } type DERPServerConfig struct { Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"` RegionID *DeploymentConfigField[int] `json:"region_id" typescript:",notnull"` RegionCode *DeploymentConfigField[string] `json:"region_code" typescript:",notnull"` RegionName *DeploymentConfigField[string] `json:"region_name" typescript:",notnull"` STUNAddresses *DeploymentConfigField[[]string] `json:"stun_addresses" typescript:",notnull"` RelayURL *DeploymentConfigField[string] `json:"relay_url" typescript:",notnull"` } type DERPConfig struct { URL *DeploymentConfigField[string] `json:"url" typescript:",notnull"` Path *DeploymentConfigField[string] `json:"path" typescript:",notnull"` } type PrometheusConfig struct { Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"` Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"` } type PprofConfig struct { Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"` Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"` } type OAuth2Config struct { Github *OAuth2GithubConfig `json:"github" typescript:",notnull"` } type OAuth2GithubConfig struct { ClientID *DeploymentConfigField[string] `json:"client_id" typescript:",notnull"` ClientSecret *DeploymentConfigField[string] `json:"client_secret" typescript:",notnull"` AllowedOrgs *DeploymentConfigField[[]string] `json:"allowed_orgs" typescript:",notnull"` AllowedTeams *DeploymentConfigField[[]string] `json:"allowed_teams" typescript:",notnull"` AllowSignups *DeploymentConfigField[bool] `json:"allow_signups" typescript:",notnull"` AllowEveryone *DeploymentConfigField[bool] `json:"allow_everyone" typescript:",notnull"` EnterpriseBaseURL *DeploymentConfigField[string] `json:"enterprise_base_url" typescript:",notnull"` } type OIDCConfig struct { AllowSignups *DeploymentConfigField[bool] `json:"allow_signups" typescript:",notnull"` ClientID *DeploymentConfigField[string] `json:"client_id" typescript:",notnull"` ClientSecret *DeploymentConfigField[string] `json:"client_secret" typescript:",notnull"` EmailDomain *DeploymentConfigField[[]string] `json:"email_domain" typescript:",notnull"` IssuerURL *DeploymentConfigField[string] `json:"issuer_url" typescript:",notnull"` Scopes *DeploymentConfigField[[]string] `json:"scopes" typescript:",notnull"` IgnoreEmailVerified *DeploymentConfigField[bool] `json:"ignore_email_verified" typescript:",notnull"` UsernameField *DeploymentConfigField[string] `json:"username_field" typescript:",notnull"` } type TelemetryConfig struct { Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"` Trace *DeploymentConfigField[bool] `json:"trace" typescript:",notnull"` URL *DeploymentConfigField[string] `json:"url" typescript:",notnull"` } type TLSConfig struct { Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"` Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"` RedirectHTTP *DeploymentConfigField[bool] `json:"redirect_http" typescript:",notnull"` CertFiles *DeploymentConfigField[[]string] `json:"cert_file" typescript:",notnull"` ClientAuth *DeploymentConfigField[string] `json:"client_auth" typescript:",notnull"` ClientCAFile *DeploymentConfigField[string] `json:"client_ca_file" typescript:",notnull"` KeyFiles *DeploymentConfigField[[]string] `json:"key_file" typescript:",notnull"` MinVersion *DeploymentConfigField[string] `json:"min_version" typescript:",notnull"` ClientCertFile *DeploymentConfigField[string] `json:"client_cert_file" typescript:",notnull"` ClientKeyFile *DeploymentConfigField[string] `json:"client_key_file" typescript:",notnull"` } type TraceConfig struct { Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"` HoneycombAPIKey *DeploymentConfigField[string] `json:"honeycomb_api_key" typescript:",notnull"` CaptureLogs *DeploymentConfigField[bool] `json:"capture_logs" typescript:",notnull"` } type GitAuthConfig struct { ID string `json:"id"` Type string `json:"type"` ClientID string `json:"client_id"` ClientSecret string `json:"-" yaml:"client_secret"` AuthURL string `json:"auth_url"` TokenURL string `json:"token_url"` ValidateURL string `json:"validate_url"` Regex string `json:"regex"` NoRefresh bool `json:"no_refresh"` Scopes []string `json:"scopes"` } type ProvisionerConfig struct { Daemons *DeploymentConfigField[int] `json:"daemons" typescript:",notnull"` DaemonPollInterval *DeploymentConfigField[time.Duration] `json:"daemon_poll_interval" typescript:",notnull"` DaemonPollJitter *DeploymentConfigField[time.Duration] `json:"daemon_poll_jitter" typescript:",notnull"` ForceCancelInterval *DeploymentConfigField[time.Duration] `json:"force_cancel_interval" typescript:",notnull"` } type RateLimitConfig struct { DisableAll *DeploymentConfigField[bool] `json:"disable_all" typescript:",notnull"` API *DeploymentConfigField[int] `json:"api" typescript:",notnull"` } type SwaggerConfig struct { Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"` } type LoggingConfig struct { Human *DeploymentConfigField[string] `json:"human" typescript:",notnull"` JSON *DeploymentConfigField[string] `json:"json" typescript:",notnull"` Stackdriver *DeploymentConfigField[string] `json:"stackdriver" typescript:",notnull"` } type DangerousConfig struct { AllowPathAppSharing *DeploymentConfigField[bool] `json:"allow_path_app_sharing" typescript:",notnull"` AllowPathAppSiteOwnerAccess *DeploymentConfigField[bool] `json:"allow_path_app_site_owner_access" typescript:",notnull"` } type Flaggable interface { string | time.Duration | bool | int | []string | []GitAuthConfig } type DeploymentConfigField[T Flaggable] struct { Name string `json:"name"` Usage string `json:"usage"` Flag string `json:"flag"` // EnvOverride will override the automatically generated environment // variable name. Useful if you're moving values around but need to keep // backwards compatibility with old environment variable names. // // NOTE: this is not supported for array flags. EnvOverride string `json:"-"` Shorthand string `json:"shorthand"` Enterprise bool `json:"enterprise"` Hidden bool `json:"hidden"` Secret bool `json:"secret"` Default T `json:"default"` Value T `json:"value"` } // MarshalJSON removes the Value field from the JSON output of any fields marked Secret. // nolint:revive func (f *DeploymentConfigField[T]) MarshalJSON() ([]byte, error) { copy := struct { Name string `json:"name"` Usage string `json:"usage"` Flag string `json:"flag"` Shorthand string `json:"shorthand"` Enterprise bool `json:"enterprise"` Hidden bool `json:"hidden"` Secret bool `json:"secret"` Default T `json:"default"` Value T `json:"value"` }{ Name: f.Name, Usage: f.Usage, Flag: f.Flag, Shorthand: f.Shorthand, Enterprise: f.Enterprise, Hidden: f.Hidden, Secret: f.Secret, } if !f.Secret { copy.Default = f.Default copy.Value = f.Value } return json.Marshal(copy) } // DeploymentConfig returns the deployment config for the coder server. func (c *Client) DeploymentConfig(ctx context.Context) (DeploymentConfig, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/config/deployment", nil) if err != nil { return DeploymentConfig{}, xerrors.Errorf("execute request: %w", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { return DeploymentConfig{}, ReadBodyAsError(res) } var df DeploymentConfig return df, json.NewDecoder(res.Body).Decode(&df) } type AppearanceConfig struct { LogoURL string `json:"logo_url"` ServiceBanner ServiceBannerConfig `json:"service_banner"` } type ServiceBannerConfig struct { Enabled bool `json:"enabled"` Message string `json:"message,omitempty"` BackgroundColor string `json:"background_color,omitempty"` } // Appearance returns the configuration that modifies the visual // display of the dashboard. func (c *Client) Appearance(ctx context.Context) (AppearanceConfig, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/appearance", nil) if err != nil { return AppearanceConfig{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return AppearanceConfig{}, ReadBodyAsError(res) } var cfg AppearanceConfig return cfg, json.NewDecoder(res.Body).Decode(&cfg) } func (c *Client) UpdateAppearance(ctx context.Context, appearance AppearanceConfig) error { res, err := c.Request(ctx, http.MethodPut, "/api/v2/appearance", appearance) if err != nil { return err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return ReadBodyAsError(res) } return nil } // BuildInfoResponse contains build information for this instance of Coder. type BuildInfoResponse struct { // ExternalURL references the current Coder version. // For production builds, this will link directly to a release. For development builds, this will link to a commit. ExternalURL string `json:"external_url"` // Version returns the semantic version of the build. Version string `json:"version"` } // CanonicalVersion trims build information from the version. // E.g. 'v0.7.4-devel+11573034' -> 'v0.7.4'. func (b BuildInfoResponse) CanonicalVersion() string { // We do a little hack here to massage the string into a form // that works well with semver. trimmed := strings.ReplaceAll(b.Version, "-devel+", "+devel-") return semver.Canonical(trimmed) } // BuildInfo returns build information for this instance of Coder. func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil) if err != nil { return BuildInfoResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return BuildInfoResponse{}, ReadBodyAsError(res) } var buildInfo BuildInfoResponse return buildInfo, json.NewDecoder(res.Body).Decode(&buildInfo) } type Experiment string const ( // ExperimentAuthzQuerier is an internal experiment that enables the ExperimentAuthzQuerier // interface for all RBAC operations. NOT READY FOR PRODUCTION USE. ExperimentAuthzQuerier Experiment = "authz_querier" // Add new experiments here! // ExperimentExample Experiment = "example" ) var ( // ExperimentsAll should include all experiments that are safe for // users to opt-in to via --experimental='*'. // Experiments that are not ready for consumption by all users should // not be included here and will be essentially hidden. ExperimentsAll = Experiments{} ) // Experiments is a list of experiments that are enabled for the deployment. // Multiple experiments may be enabled at the same time. // Experiments are not safe for production use, and are not guaranteed to // be backwards compatible. They may be removed or renamed at any time. type Experiments []Experiment func (e Experiments) Enabled(ex Experiment) bool { for _, v := range e { if v == ex { return true } } return false } func (c *Client) Experiments(ctx context.Context) (Experiments, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, ReadBodyAsError(res) } var exp []Experiment return exp, json.NewDecoder(res.Body).Decode(&exp) } type DeploymentDAUsResponse struct { Entries []DAUEntry `json:"entries"` } func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil) if err != nil { return nil, xerrors.Errorf("execute request: %w", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, ReadBodyAsError(res) } var resp DeploymentDAUsResponse return &resp, json.NewDecoder(res.Body).Decode(&resp) } type AppHostResponse struct { // Host is the externally accessible URL for the Coder instance. Host string `json:"host"` } // AppHost returns the site-wide application wildcard hostname without the // leading "*.", e.g. "apps.coder.com". Apps are accessible at: // "------.", e.g. // "my-app--agent--workspace--username.apps.coder.com". // // If the app host is not set, the response will contain an empty string. func (c *Client) AppHost(ctx context.Context) (AppHostResponse, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/applications/host", nil) if err != nil { return AppHostResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return AppHostResponse{}, ReadBodyAsError(res) } var host AppHostResponse return host, json.NewDecoder(res.Body).Decode(&host) }