Compare commits

...

17 Commits

Author SHA1 Message Date
Stephen Kirby
d65eea8132 fix: fix workspace actions options (#13572) (#14071)
* fix: fix workspace actions options (#13572)

(cherry picked from commit 07cd9acb2c)

* fix: change time format string from 15:40 to 15:04 (#14033)

* Change string format to constant value

(cherry picked from commit eacdfb9f9c)

---------

Co-authored-by: Kayla Washburn-Love <mckayla@hey.com>
Co-authored-by: Charlie Voiselle <464492+angrycub@users.noreply.github.com>
2024-08-01 13:35:28 -05:00
Steven Masley
2f0c2d77dc chore: keep active users active in scim (#13955) (#13974)
* chore: scim should keep active users active
 * chore: add a unit test to excercise dormancy bug

  (cherry picked from commit 03c5d42233)
2024-07-22 16:24:56 -05:00
Stephen Kirby
82ed9e4dc7 chore: patch 2.12.4 (#13925)
* fix(site): enable dormant workspace to be deleted (#13850)

(cherry picked from commit 01b30eaa32)

* chore: add SVG desktop icon (#13765)

* chore: add SVG desktop icon

* fix: add desktop icon to to icons.json

(cherry picked from commit 21a923a7a0)

* fix: update import order for Storybook

---------

Co-authored-by: Bruno Quaresma <bruno@coder.com>
Co-authored-by: Michael Smith <throwawayclover@gmail.com>
2024-07-17 20:36:52 -04:00
Steven Masley
534d4ea752 chore: external auth validate response "Forbidden" should return invalid, not an error (#13446)
* chore: add unit test to delete workspace from suspended user
* chore: account for forbidden as well as unauthorized response codes

(cherry picked from commit 27f26910b6)
2024-06-24 17:38:32 +00:00
Stephen Kirby
8ce8700424 fixed changelog script release channel flag (#13649)
(cherry picked from commit b9d83c75de)
2024-06-24 17:38:12 +00:00
Stephen Kirby
b9779af5b2 fixed script ref (#13647)
(cherry picked from commit 3d6c9799e3)
2024-06-24 17:38:02 +00:00
Mathias Fredriksson
e54ff57a9a chore(scripts): fix release promote stable to set latest tag (#13471)
(cherry picked from commit 9a757f8e74)
2024-06-21 19:37:12 +00:00
Mathias Fredriksson
ae220f52e7 chore(scripts): fix dry run for autoversion in release.sh (#13470)
(cherry picked from commit 3b7f9534fb)
2024-06-21 19:37:06 +00:00
Kyle Carberry
90f82da311 fix: write server config to telemetry (#13590)
* fix: add external auth configs to telemetry

* Refactor telemetry to send the entire config

* gen

* Fix linting

(cherry picked from commit 3a1fa04590)
2024-06-21 19:36:53 +00:00
Kyle Carberry
201cb1cbed fix: display trial errors in the dashboard (#13601)
* fix: display trial errors in the dashboard

The error was essentially being ignored before!

* Remove day mention in product of trial

* fmt

(cherry picked from commit 7049d7a881)
2024-06-21 19:36:37 +00:00
Kyle Carberry
b701620a01 feat: add cross-origin reporting for telemetry in the dashboard (#13612)
* feat: add cross-origin reporting for telemetry in the dashboard

* Respect the telemetry flag

* Fix embedded metadata

* Fix compilation error

* Fix linting

(cherry picked from commit 0793a4b35b)
2024-06-21 19:36:31 +00:00
Kyle Carberry
0703fc6888 fix: track login page correctly (#13618)
(cherry picked from commit 495eea452f)
2024-06-21 19:36:25 +00:00
Kyle Carberry
a9e5648557 fix: remove connected button (#13625)
It didn't make a lot of sense in current form. It will when we improve autostop.

(cherry picked from commit 3ef12ac284)
2024-06-21 19:36:19 +00:00
Jon Ayers
3fbfb534d0 fix: only render tooltip when require_active_version enabled (#13484)
(cherry picked from commit 7995d7c3d6)
2024-06-06 02:53:47 +00:00
Colin Adler
5e69a9d18b fix(site): show workspace start button when require active version is enabled (#13482)
(cherry picked from commit f1b42a15fa)
2024-06-06 02:35:33 +00:00
Mathias Fredriksson
ba0bf43de4 chore(scripts): fix unbound variable in tag_version.sh (#13428)
(cherry picked from commit a51076a4cd)
2024-06-04 16:15:21 +00:00
Colin Adler
40af6206cc chore: upgrade terraform to v1.8.5 (#13429)
(cherry picked from commit b723da9e91)
2024-06-04 16:14:45 +00:00
81 changed files with 894 additions and 502 deletions

View File

@@ -7,5 +7,5 @@ runs:
- name: Install Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.5
terraform_version: 1.8.4
terraform_wrapper: false

View File

@@ -33,4 +33,5 @@ extend-exclude = [
"**/pnpm-lock.yaml",
"tailnet/testdata/**",
"site/src/pages/SetupPage/countries.tsx",
"provisioner/terraform/testdata/**",
]

View File

@@ -239,7 +239,7 @@ func (r *RootCmd) login() *serpent.Command {
if !inv.ParsedFlags().Changed("first-user-trial") && os.Getenv(firstUserTrialEnv) == "" {
v, _ := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Start a 30-day trial of Enterprise?",
Text: "Start a trial of Enterprise?",
IsConfirm: true,
Default: "yes",
})

View File

@@ -796,31 +796,18 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):")
if vals.Telemetry.Enable {
gitAuth := make([]telemetry.GitAuth, 0)
// TODO:
var gitAuthConfigs []codersdk.ExternalAuthConfig
for _, cfg := range gitAuthConfigs {
gitAuth = append(gitAuth, telemetry.GitAuth{
Type: cfg.Type,
})
vals, err := vals.WithoutSecrets()
if err != nil {
return xerrors.Errorf("remove secrets from deployment values: %w", err)
}
options.Telemetry, err = telemetry.New(telemetry.Options{
BuiltinPostgres: builtinPostgres,
DeploymentID: deploymentID,
Database: options.Database,
Logger: logger.Named("telemetry"),
URL: vals.Telemetry.URL.Value(),
Wildcard: vals.WildcardAccessURL.String() != "",
DERPServerRelayURL: vals.DERP.Server.RelayURL.String(),
GitAuth: gitAuth,
GitHubOAuth: vals.OAuth2.Github.ClientID != "",
OIDCAuth: vals.OIDC.ClientID != "",
OIDCIssuerURL: vals.OIDC.IssuerURL.String(),
Prometheus: vals.Prometheus.Enable.Value(),
STUN: len(vals.DERP.Server.STUNAddresses) != 0,
Tunnel: tunnel != nil,
Experiments: vals.Experiments.Value(),
BuiltinPostgres: builtinPostgres,
DeploymentID: deploymentID,
Database: options.Database,
Logger: logger.Named("telemetry"),
URL: vals.Telemetry.URL.Value(),
Tunnel: tunnel != nil,
DeploymentConfig: vals,
ParseLicenseJWT: func(lic *telemetry.License) error {
// This will be nil when running in AGPL-only mode.
if options.ParseLicenseClaims == nil {

10
coderd/apidoc/docs.go generated
View File

@@ -8717,6 +8717,10 @@ const docTemplate = `{
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"
},
"telemetry": {
"description": "Telemetry is a boolean that indicates whether telemetry is enabled.",
"type": "boolean"
},
"upgrade_message": {
"description": "UpgradeMessage is the message displayed to users when an outdated client\nis detected.",
"type": "string"
@@ -9782,12 +9786,6 @@ const docTemplate = `{
"description": "DisplayName is shown in the UI to identify the auth config.",
"type": "string"
},
"extra_token_keys": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"description": "ID is a unique identifier for the auth config.\nIt defaults to ` + "`" + `type` + "`" + ` when not provided.",
"type": "string"

View File

@@ -7761,6 +7761,10 @@
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"
},
"telemetry": {
"description": "Telemetry is a boolean that indicates whether telemetry is enabled.",
"type": "boolean"
},
"upgrade_message": {
"description": "UpgradeMessage is the message displayed to users when an outdated client\nis detected.",
"type": "string"
@@ -8769,12 +8773,6 @@
"description": "DisplayName is shown in the UI to identify the auth config.",
"type": "string"
},
"extra_token_keys": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"description": "ID is a unique identifier for the auth config.\nIt defaults to `type` when not provided.",
"type": "string"

View File

@@ -233,6 +233,26 @@ func requireOrgID[T Auditable](ctx context.Context, id uuid.UUID, log slog.Logge
return id
}
// InitRequestWithCancel returns a commit function with a boolean arg.
// If the arg is false, future calls to commit() will not create an audit log
// entry.
func InitRequestWithCancel[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request[T], func(commit bool)) {
req, commitF := InitRequest[T](w, p)
cancelled := false
return req, func(commit bool) {
// Once 'commit=false' is called, block
// any future commit attempts.
if !commit {
cancelled = true
return
}
// If it was ever cancelled, block any commits
if !cancelled {
commitF()
}
}
}
// InitRequest initializes an audit log for a request. It returns a function
// that should be deferred, causing the audit log to be committed when the
// handler returns.

View File

@@ -450,6 +450,7 @@ func New(options *Options) *API {
WorkspaceProxy: false,
UpgradeMessage: api.DeploymentValues.CLIUpgradeMessage.String(),
DeploymentID: api.DeploymentID,
Telemetry: api.Telemetry.Enabled(),
}
api.SiteHandler = site.New(&site.Options{
BinFS: binFS,

View File

@@ -1255,7 +1255,9 @@ type ExternalAuthConfigOptions struct {
// ValidatePayload is the payload that is used when the user calls the
// equivalent of "userinfo" for oauth2. This is not standardized, so is
// different for each provider type.
ValidatePayload func(email string) interface{}
//
// The int,error payload can control the response if set.
ValidatePayload func(email string) (interface{}, int, error)
// routes is more advanced usage. This allows the caller to
// completely customize the response. It captures all routes under the /external-auth-validate/*
@@ -1292,7 +1294,20 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu
case "/user", "/", "":
var payload interface{} = "OK"
if custom.ValidatePayload != nil {
payload = custom.ValidatePayload(email)
var err error
var code int
payload, code, err = custom.ValidatePayload(email)
if code == 0 && err == nil {
code = http.StatusOK
}
if code == 0 && err != nil {
code = http.StatusUnauthorized
}
if err != nil {
http.Error(rw, fmt.Sprintf("failed validation via custom method: %s", err.Error()), code)
return
}
rw.WriteHeader(code)
}
_ = json.NewEncoder(rw).Encode(payload)
default:

View File

@@ -202,7 +202,7 @@ func (c *Config) ValidateToken(ctx context.Context, link *oauth2.Token) (bool, *
return false, nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusUnauthorized {
if res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusForbidden {
// The token is no longer valid!
return false, nil, nil
}

View File

@@ -79,11 +79,11 @@ func TestExternalAuthByID(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{
ExternalAuthConfigs: []*externalauth.Config{
fake.ExternalAuthConfig(t, providerID, &oidctest.ExternalAuthConfigOptions{
ValidatePayload: func(_ string) interface{} {
ValidatePayload: func(_ string) (interface{}, int, error) {
return github.User{
Login: github.String("kyle"),
AvatarURL: github.String("https://avatars.githubusercontent.com/u/12345678?v=4"),
}
}, 0, nil
},
}, func(cfg *externalauth.Config) {
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
@@ -108,11 +108,11 @@ func TestExternalAuthByID(t *testing.T) {
// routes includes a route for /install that returns a list of installations
routes := (&oidctest.ExternalAuthConfigOptions{
ValidatePayload: func(_ string) interface{} {
ValidatePayload: func(_ string) (interface{}, int, error) {
return github.User{
Login: github.String("kyle"),
AvatarURL: github.String("https://avatars.githubusercontent.com/u/12345678?v=4"),
}
}, 0, nil
},
}).AddRoute("/installs", func(_ string, rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, struct {
@@ -556,7 +556,7 @@ func TestExternalAuthCallback(t *testing.T) {
// If the validation URL gives a non-OK status code, this
// should be treated as an internal server error.
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Something went wrong!"))
})
_, err = agentClient.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
@@ -565,7 +565,7 @@ func TestExternalAuthCallback(t *testing.T) {
var apiError *codersdk.Error
require.ErrorAs(t, err, &apiError)
require.Equal(t, http.StatusInternalServerError, apiError.StatusCode())
require.Equal(t, "validate external auth token: status 403: body: Something went wrong!", apiError.Detail)
require.Equal(t, "validate external auth token: status 400: body: Something went wrong!", apiError.Detail)
})
t.Run("ExpiredNoRefresh", func(t *testing.T) {

View File

@@ -41,20 +41,13 @@ type Options struct {
// URL is an endpoint to direct telemetry towards!
URL *url.URL
BuiltinPostgres bool
DeploymentID string
GitHubOAuth bool
OIDCAuth bool
OIDCIssuerURL string
Wildcard bool
DERPServerRelayURL string
GitAuth []GitAuth
Prometheus bool
STUN bool
SnapshotFrequency time.Duration
Tunnel bool
ParseLicenseJWT func(lic *License) error
Experiments []string
DeploymentID string
DeploymentConfig *codersdk.DeploymentValues
BuiltinPostgres bool
Tunnel bool
SnapshotFrequency time.Duration
ParseLicenseJWT func(lic *License) error
}
// New constructs a reporter for telemetry data.
@@ -100,6 +93,7 @@ type Reporter interface {
// database. For example, if a new user is added, a snapshot can
// contain just that user entry.
Report(snapshot *Snapshot)
Enabled() bool
Close()
}
@@ -116,6 +110,10 @@ type remoteReporter struct {
shutdownAt *time.Time
}
func (*remoteReporter) Enabled() bool {
return true
}
func (r *remoteReporter) Report(snapshot *Snapshot) {
go r.reportSync(snapshot)
}
@@ -242,31 +240,24 @@ func (r *remoteReporter) deployment() error {
}
data, err := json.Marshal(&Deployment{
ID: r.options.DeploymentID,
Architecture: sysInfo.Architecture,
BuiltinPostgres: r.options.BuiltinPostgres,
Containerized: containerized,
Wildcard: r.options.Wildcard,
DERPServerRelayURL: r.options.DERPServerRelayURL,
GitAuth: r.options.GitAuth,
Kubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
GitHubOAuth: r.options.GitHubOAuth,
OIDCAuth: r.options.OIDCAuth,
OIDCIssuerURL: r.options.OIDCIssuerURL,
Prometheus: r.options.Prometheus,
InstallSource: installSource,
STUN: r.options.STUN,
Tunnel: r.options.Tunnel,
OSType: sysInfo.OS.Type,
OSFamily: sysInfo.OS.Family,
OSPlatform: sysInfo.OS.Platform,
OSName: sysInfo.OS.Name,
OSVersion: sysInfo.OS.Version,
CPUCores: runtime.NumCPU(),
MemoryTotal: mem.Total,
MachineID: sysInfo.UniqueID,
StartedAt: r.startedAt,
ShutdownAt: r.shutdownAt,
ID: r.options.DeploymentID,
Architecture: sysInfo.Architecture,
BuiltinPostgres: r.options.BuiltinPostgres,
Containerized: containerized,
Config: r.options.DeploymentConfig,
Kubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
InstallSource: installSource,
Tunnel: r.options.Tunnel,
OSType: sysInfo.OS.Type,
OSFamily: sysInfo.OS.Family,
OSPlatform: sysInfo.OS.Platform,
OSName: sysInfo.OS.Name,
OSVersion: sysInfo.OS.Version,
CPUCores: runtime.NumCPU(),
MemoryTotal: mem.Total,
MachineID: sysInfo.UniqueID,
StartedAt: r.startedAt,
ShutdownAt: r.shutdownAt,
})
if err != nil {
return xerrors.Errorf("marshal deployment: %w", err)
@@ -481,10 +472,6 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
}
return nil
})
eg.Go(func() error {
snapshot.Experiments = ConvertExperiments(r.options.Experiments)
return nil
})
err := eg.Wait()
if err != nil {
@@ -745,16 +732,6 @@ func ConvertExternalProvisioner(id uuid.UUID, tags map[string]string, provisione
}
}
func ConvertExperiments(experiments []string) []Experiment {
var out []Experiment
for _, exp := range experiments {
out = append(out, Experiment{Name: exp})
}
return out
}
// Snapshot represents a point-in-time anonymized database dump.
// Data is aggregated by latest on the server-side, so partial data
// can be sent without issue.
@@ -777,40 +754,28 @@ type Snapshot struct {
WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"`
WorkspaceResources []WorkspaceResource `json:"workspace_resources"`
Workspaces []Workspace `json:"workspaces"`
Experiments []Experiment `json:"experiments"`
}
// Deployment contains information about the host running Coder.
type Deployment struct {
ID string `json:"id"`
Architecture string `json:"architecture"`
BuiltinPostgres bool `json:"builtin_postgres"`
Containerized bool `json:"containerized"`
Kubernetes bool `json:"kubernetes"`
Tunnel bool `json:"tunnel"`
Wildcard bool `json:"wildcard"`
DERPServerRelayURL string `json:"derp_server_relay_url"`
GitAuth []GitAuth `json:"git_auth"`
GitHubOAuth bool `json:"github_oauth"`
OIDCAuth bool `json:"oidc_auth"`
OIDCIssuerURL string `json:"oidc_issuer_url"`
Prometheus bool `json:"prometheus"`
InstallSource string `json:"install_source"`
STUN bool `json:"stun"`
OSType string `json:"os_type"`
OSFamily string `json:"os_family"`
OSPlatform string `json:"os_platform"`
OSName string `json:"os_name"`
OSVersion string `json:"os_version"`
CPUCores int `json:"cpu_cores"`
MemoryTotal uint64 `json:"memory_total"`
MachineID string `json:"machine_id"`
StartedAt time.Time `json:"started_at"`
ShutdownAt *time.Time `json:"shutdown_at"`
}
type GitAuth struct {
Type string `json:"type"`
ID string `json:"id"`
Architecture string `json:"architecture"`
BuiltinPostgres bool `json:"builtin_postgres"`
Containerized bool `json:"containerized"`
Kubernetes bool `json:"kubernetes"`
Config *codersdk.DeploymentValues `json:"config"`
Tunnel bool `json:"tunnel"`
InstallSource string `json:"install_source"`
OSType string `json:"os_type"`
OSFamily string `json:"os_family"`
OSPlatform string `json:"os_platform"`
OSName string `json:"os_name"`
OSVersion string `json:"os_version"`
CPUCores int `json:"cpu_cores"`
MemoryTotal uint64 `json:"memory_total"`
MachineID string `json:"machine_id"`
StartedAt time.Time `json:"started_at"`
ShutdownAt *time.Time `json:"shutdown_at"`
}
type APIKey struct {
@@ -985,11 +950,8 @@ type ExternalProvisioner struct {
ShutdownAt *time.Time `json:"shutdown_at"`
}
type Experiment struct {
Name string `json:"name"`
}
type noopReporter struct{}
func (*noopReporter) Report(_ *Snapshot) {}
func (*noopReporter) Enabled() bool { return false }
func (*noopReporter) Close() {}

View File

@@ -114,17 +114,6 @@ func TestTelemetry(t *testing.T) {
require.Len(t, snapshot.Users, 1)
require.Equal(t, snapshot.Users[0].EmailHashed, "bb44bf07cf9a2db0554bba63a03d822c927deae77df101874496df5a6a3e896d@coder.com")
})
t.Run("Experiments", func(t *testing.T) {
t.Parallel()
const expName = "my-experiment"
exps := []string{expName}
_, snapshot := collectSnapshot(t, dbmem.New(), func(opts telemetry.Options) telemetry.Options {
opts.Experiments = exps
return opts
})
require.Equal(t, []telemetry.Experiment{{Name: expName}}, snapshot.Experiments)
})
}
// nolint:paralleltest

View File

@@ -20,9 +20,11 @@ import (
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
@@ -711,6 +713,78 @@ func TestWorkspaceBuildStatus(t *testing.T) {
require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status)
}
func TestWorkspaceDeleteSuspendedUser(t *testing.T) {
t.Parallel()
const providerID = "fake-github"
fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
validateCalls := 0
userSuspended := false
owner := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
ExternalAuthConfigs: []*externalauth.Config{
fake.ExternalAuthConfig(t, providerID, &oidctest.ExternalAuthConfigOptions{
ValidatePayload: func(email string) (interface{}, int, error) {
validateCalls++
if userSuspended {
// Simulate the user being suspended from the IDP too.
return "", http.StatusForbidden, fmt.Errorf("user is suspended")
}
return "OK", 0, nil
},
}),
},
})
first := coderdtest.CreateFirstUser(t, owner)
// New user that we will suspend when we try to delete the workspace.
client, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleTemplateAdmin())
fake.ExternalLogin(t, client)
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Error: "",
Resources: nil,
Parameters: nil,
ExternalAuthProviders: []*proto.ExternalAuthProviderResource{
{
Id: providerID,
Optional: false,
},
},
},
},
}},
})
validateCalls = 0 // Reset
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
require.Equal(t, 1, validateCalls) // Ensure the external link is working
// Suspend the user
ctx := testutil.Context(t, testutil.WaitLong)
_, err := owner.UpdateUserStatus(ctx, user.ID.String(), codersdk.UserStatusSuspended)
require.NoError(t, err, "suspend user")
// Now delete the workspace build
userSuspended = true
build, err := owner.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionDelete,
})
require.NoError(t, err)
build = coderdtest.AwaitWorkspaceBuildJobCompleted(t, owner, build.ID)
require.Equal(t, 2, validateCalls)
require.Equal(t, codersdk.WorkspaceStatusDeleted, build.Status)
}
func TestWorkspaceBuildDebugMode(t *testing.T) {
t.Parallel()

View File

@@ -392,7 +392,7 @@ type ExternalAuthConfig struct {
AppInstallationsURL string `json:"app_installations_url" yaml:"app_installations_url"`
NoRefresh bool `json:"no_refresh" yaml:"no_refresh"`
Scopes []string `json:"scopes" yaml:"scopes"`
ExtraTokenKeys []string `json:"extra_token_keys" yaml:"extra_token_keys"`
ExtraTokenKeys []string `json:"-" yaml:"extra_token_keys"`
DeviceFlow bool `json:"device_flow" yaml:"device_flow"`
DeviceCodeURL string `json:"device_code_url" yaml:"device_code_url"`
// Regex allows API requesters to match an auth config by
@@ -2162,11 +2162,12 @@ type BuildInfoResponse struct {
ExternalURL string `json:"external_url"`
// Version returns the semantic version of the build.
Version string `json:"version"`
// DashboardURL is the URL to hit the deployment's dashboard.
// For external workspace proxies, this is the coderd they are connected
// to.
DashboardURL string `json:"dashboard_url"`
// Telemetry is a boolean that indicates whether telemetry is enabled.
Telemetry bool `json:"telemetry"`
WorkspaceProxy bool `json:"workspace_proxy"`

2
docs/api/general.md generated
View File

@@ -57,6 +57,7 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
"telemetry": true,
"upgrade_message": "string",
"version": "string",
"workspace_proxy": true
@@ -227,7 +228,6 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"device_flow": true,
"display_icon": "string",
"display_name": "string",
"extra_token_keys": ["string"],
"id": "string",
"no_refresh": true,
"regex": "string",

7
docs/api/schemas.md generated
View File

@@ -1234,6 +1234,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
"telemetry": true,
"upgrade_message": "string",
"version": "string",
"workspace_proxy": true
@@ -1248,6 +1249,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `dashboard_url` | string | false | | Dashboard URL is the URL to hit the deployment's dashboard. For external workspace proxies, this is the coderd they are connected to. |
| `deployment_id` | string | false | | Deployment ID is the unique identifier for this deployment. |
| `external_url` | string | false | | External URL references the current Coder version. For production builds, this will link directly to a release. For development builds, this will link to a commit. |
| `telemetry` | boolean | false | | Telemetry is a boolean that indicates whether telemetry is enabled. |
| `upgrade_message` | string | false | | Upgrade message is the message displayed to users when an outdated client is detected. |
| `version` | string | false | | Version returns the semantic version of the build. |
| `workspace_proxy` | boolean | false | | |
@@ -2009,7 +2011,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"device_flow": true,
"display_icon": "string",
"display_name": "string",
"extra_token_keys": ["string"],
"id": "string",
"no_refresh": true,
"regex": "string",
@@ -2382,7 +2383,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"device_flow": true,
"display_icon": "string",
"display_name": "string",
"extra_token_keys": ["string"],
"id": "string",
"no_refresh": true,
"regex": "string",
@@ -2801,7 +2801,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"device_flow": true,
"display_icon": "string",
"display_name": "string",
"extra_token_keys": ["string"],
"id": "string",
"no_refresh": true,
"regex": "string",
@@ -2824,7 +2823,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `device_flow` | boolean | false | | |
| `display_icon` | string | false | | Display icon is a URL to an icon to display in the UI. |
| `display_name` | string | false | | Display name is shown in the UI to identify the auth config. |
| `extra_token_keys` | array of string | false | | |
| `id` | string | false | | ID is a unique identifier for the auth config. It defaults to `type` when not provided. |
| `no_refresh` | boolean | false | | |
| `regex` | string | false | | Regex allows API requesters to match an auth config by a string (e.g. coder.com) instead of by it's type. |
@@ -8844,7 +8842,6 @@ _None_
"device_flow": true,
"display_icon": "string",
"display_name": "string",
"extra_token_keys": ["string"],
"id": "string",
"no_refresh": true,
"regex": "string",

View File

@@ -54,7 +54,7 @@ RUN mkdir -p /opt/terraform
# The below step is optional if you wish to keep the existing version.
# See https://github.com/coder/coder/blob/main/provisioner/terraform/install.go#L23-L24
# for supported Terraform versions.
ARG TERRAFORM_VERSION=1.7.5
ARG TERRAFORM_VERSION=1.8.4
RUN apk update && \
apk del terraform && \
curl -LOs https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip \

View File

@@ -171,7 +171,7 @@ RUN apt-get update --quiet && apt-get install --yes \
# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.7.5.
# Installing the same version here to match.
RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_linux_amd64.zip" && \
RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.8.4/terraform_1.8.4_linux_amd64.zip" && \
unzip /tmp/terraform.zip -d /usr/local/bin && \
rm -f /tmp/terraform.zip && \
chmod +x /usr/local/bin/terraform && \

View File

@@ -273,21 +273,34 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
var status database.UserStatus
if sUser.Active {
// The user will get transitioned to Active after logging in.
status = database.UserStatusDormant
switch dbUser.Status {
case database.UserStatusActive:
// Keep the user active
status = database.UserStatusActive
case database.UserStatusDormant, database.UserStatusSuspended:
// Move (or keep) as dormant
status = database.UserStatusDormant
default:
// If the status is unknown, just move them to dormant.
// The user will get transitioned to Active after logging in.
status = database.UserStatusDormant
}
} else {
status = database.UserStatusSuspended
}
//nolint:gocritic // needed for SCIM
_, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
ID: dbUser.ID,
Status: status,
UpdatedAt: dbtime.Now(),
})
if err != nil {
_ = handlerutil.WriteError(rw, err)
return
if dbUser.Status != status {
//nolint:gocritic // needed for SCIM
userNew, err := api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
ID: dbUser.ID,
Status: status,
UpdatedAt: dbtime.Now(),
})
if err != nil {
_ = handlerutil.WriteError(rw, err)
return
}
dbUser = userNew
}
httpapi.Write(ctx, rw, http.StatusOK, sUser)

View File

@@ -8,9 +8,12 @@ import (
"net/http"
"testing"
"github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/enterprise/coderd"
@@ -338,5 +341,68 @@ func TestScim(t *testing.T) {
require.Len(t, userRes.Users, 1)
assert.Equal(t, codersdk.UserStatusSuspended, userRes.Users[0].Status)
})
// Create a user via SCIM, which starts as dormant.
// Log in as the user, making them active.
// Then patch the user again and the user should still be active.
t.Run("ActiveIsActive", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
scimAPIKey := []byte("hi")
fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
OIDCConfig: fake.OIDCConfig(t, []string{}),
},
SCIMAPIKey: scimAPIKey,
AuditLogging: true,
LicenseOptions: &coderdenttest.LicenseOptions{
AccountID: "coolin",
Features: license.Features{
codersdk.FeatureSCIM: 1,
},
},
})
// User is dormant on create
sUser := makeScimUser(t)
res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey))
require.NoError(t, err)
defer res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode)
err = json.NewDecoder(res.Body).Decode(&sUser)
require.NoError(t, err)
// Verify the user is dormant
scimUser, err := client.User(ctx, sUser.UserName)
require.NoError(t, err)
require.Equal(t, codersdk.UserStatusDormant, scimUser.Status, "user starts as dormant")
// Log in as the user, making them active
//nolint:bodyclose
scimUserClient, _ := fake.Login(t, client, jwt.MapClaims{
"email": sUser.Emails[0].Value,
})
scimUser, err = scimUserClient.User(ctx, codersdk.Me)
require.NoError(t, err)
require.Equal(t, codersdk.UserStatusActive, scimUser.Status, "user should now be active")
// Patch the user
res, err = client.Request(ctx, "PATCH", "/scim/v2/Users/"+sUser.ID, sUser, setScimAuth(scimAPIKey))
require.NoError(t, err)
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode)
// Verify the user is still active.
scimUser, err = client.User(ctx, sUser.UserName)
require.NoError(t, err)
require.Equal(t, codersdk.UserStatusActive, scimUser.Status, "user is still active")
})
})
}

View File

@@ -14,6 +14,8 @@ import (
"github.com/coder/coder/v2/codersdk"
)
const TimeFormatHHMM = "15:04"
func (api *API) autostopRequirementEnabledMW(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Entitlement must be enabled.
@@ -66,7 +68,7 @@ func (api *API) userQuietHoursSchedule(rw http.ResponseWriter, r *http.Request)
RawSchedule: opts.Schedule.String(),
UserSet: opts.UserSet,
UserCanSet: opts.UserCanSet,
Time: opts.Schedule.TimeParsed().Format("15:40"),
Time: opts.Schedule.TimeParsed().Format(TimeFormatHHMM),
Timezone: opts.Schedule.Location().String(),
Next: opts.Schedule.Next(time.Now().In(opts.Schedule.Location())),
})
@@ -118,7 +120,7 @@ func (api *API) putUserQuietHoursSchedule(rw http.ResponseWriter, r *http.Reques
RawSchedule: opts.Schedule.String(),
UserSet: opts.UserSet,
UserCanSet: opts.UserCanSet,
Time: opts.Schedule.TimeParsed().Format("15:40"),
Time: opts.Schedule.TimeParsed().Format(TimeFormatHHMM),
Timezone: opts.Schedule.Location().String(),
Next: opts.Schedule.Next(time.Now().In(opts.Schedule.Location())),
})

View File

@@ -11,11 +11,14 @@ import (
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/schedule/cron"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
)
const TimeFormatHHMM = coderd.TimeFormatHHMM
func TestUserQuietHours(t *testing.T) {
t.Parallel()
@@ -41,15 +44,17 @@ func TestUserQuietHours(t *testing.T) {
t.Run("OK", func(t *testing.T) {
t.Parallel()
defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 0 1 * * *"
// Using 10 for minutes lets us test a format bug in which values greater
// than 5 were causing the API to explode because the time was returned
// incorrectly
defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 10 1 * * *"
defaultScheduleParsed, err := cron.Daily(defaultQuietHoursSchedule)
require.NoError(t, err)
nextTime := defaultScheduleParsed.Next(time.Now().In(defaultScheduleParsed.Location()))
if time.Until(nextTime) < time.Hour {
// Use a different default schedule instead, because we want to avoid
// the schedule "ticking over" during this test run.
defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 0 13 * * *"
defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 10 13 * * *"
defaultScheduleParsed, err = cron.Daily(defaultQuietHoursSchedule)
require.NoError(t, err)
}
@@ -78,7 +83,7 @@ func TestUserQuietHours(t *testing.T) {
require.NoError(t, err)
require.Equal(t, defaultScheduleParsed.String(), sched1.RawSchedule)
require.False(t, sched1.UserSet)
require.Equal(t, defaultScheduleParsed.TimeParsed().Format("15:40"), sched1.Time)
require.Equal(t, defaultScheduleParsed.TimeParsed().Format(TimeFormatHHMM), sched1.Time)
require.Equal(t, defaultScheduleParsed.Location().String(), sched1.Timezone)
require.WithinDuration(t, defaultScheduleParsed.Next(time.Now()), sched1.Next, 15*time.Second)
@@ -101,7 +106,7 @@ func TestUserQuietHours(t *testing.T) {
require.NoError(t, err)
require.Equal(t, customScheduleParsed.String(), sched2.RawSchedule)
require.True(t, sched2.UserSet)
require.Equal(t, customScheduleParsed.TimeParsed().Format("15:40"), sched2.Time)
require.Equal(t, customScheduleParsed.TimeParsed().Format(TimeFormatHHMM), sched2.Time)
require.Equal(t, customScheduleParsed.Location().String(), sched2.Timezone)
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched2.Next, 15*time.Second)
@@ -110,7 +115,7 @@ func TestUserQuietHours(t *testing.T) {
require.NoError(t, err)
require.Equal(t, customScheduleParsed.String(), sched3.RawSchedule)
require.True(t, sched3.UserSet)
require.Equal(t, customScheduleParsed.TimeParsed().Format("15:40"), sched3.Time)
require.Equal(t, customScheduleParsed.TimeParsed().Format(TimeFormatHHMM), sched3.Time)
require.Equal(t, customScheduleParsed.Location().String(), sched3.Timezone)
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched3.Next, 15*time.Second)

View File

@@ -39,6 +39,22 @@ func New(db database.Store, url string, keys map[string]ed25519.PublicKey) func(
return xerrors.Errorf("perform license request: %w", err)
}
defer res.Body.Close()
if res.StatusCode > 300 {
body, err := io.ReadAll(res.Body)
if err != nil {
return xerrors.Errorf("read license response: %w", err)
}
// This is the format of the error response from
// the license server.
var msg struct {
Error string `json:"error"`
}
err = json.Unmarshal(body, &msg)
if err != nil {
return xerrors.Errorf("unmarshal error: %w", err)
}
return xerrors.New(msg.Error)
}
raw, err := io.ReadAll(res.Body)
if err != nil {
return xerrors.Errorf("read license: %w", err)

View File

@@ -216,7 +216,7 @@ func TestDialCoordinator(t *testing.T) {
Node: &proto.Node{
Id: 55,
AsOf: timestamppb.New(time.Unix(1689653252, 0)),
Key: peerNodeKey[:],
Key: peerNodeKey,
Disco: string(peerDiscoKey),
PreferredDerp: 0,
DerpLatency: map[string]float64{

View File

@@ -250,7 +250,7 @@ EOF
main() {
MAINLINE=1
STABLE=0
TERRAFORM_VERSION="1.7.5"
TERRAFORM_VERSION="1.8.4"
if [ "${TRACE-}" ]; then
set -x

View File

@@ -20,10 +20,10 @@ var (
// when Terraform is not available on the system.
// NOTE: Keep this in sync with the version in scripts/Dockerfile.base.
// NOTE: Keep this in sync with the version in install.sh.
TerraformVersion = version.Must(version.NewVersion("1.7.5"))
TerraformVersion = version.Must(version.NewVersion("1.8.4"))
minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
maxTerraformVersion = version.Must(version.NewVersion("1.7.9")) // use .9 to automatically allow patch releases
maxTerraformVersion = version.Must(version.NewVersion("1.8.9")) // use .9 to automatically allow patch releases
terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")
)

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
}
],
@@ -259,6 +260,8 @@
]
}
],
"timestamp": "2024-05-22T17:02:40Z",
"timestamp": "2024-05-31T22:25:19Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "f26b1d53-799e-4fbb-9fd3-71e60b37eacd",
"id": "2941e1eb-40f5-41cf-9e08-8f0f1a80d430",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -38,7 +38,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "ce663074-ebea-44cb-b6d1-321f590f7982",
"token": "3105121f-9b54-4c91-b497-9da9bb05c5b6",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -69,7 +69,7 @@
"outputs": {
"script": ""
},
"random": "8031375470547649400"
"random": "3895262600016319159"
},
"sensitive_values": {
"inputs": {},
@@ -84,7 +84,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "3370916843136140681",
"id": "5027788252939043492",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -204,6 +205,8 @@
]
}
},
"timestamp": "2024-05-22T17:02:43Z",
"timestamp": "2024-05-31T22:25:20Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "9d869fc3-c185-4278-a5d2-873f809a4449",
"id": "da093356-6550-4e76-bb9e-0269cede7e31",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -38,7 +38,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "418bb1d6-49d8-4340-ac84-ed6991457ff9",
"token": "ebcb7f0e-4b80-4972-b434-1a42aa650d78",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -57,7 +57,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "3681188688307687011",
"id": "2686005653093770315",
"triggers": null
},
"sensitive_values": {},
@@ -74,7 +74,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "6055360096088266226",
"id": "1732714319726388691",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -204,6 +205,8 @@
]
}
},
"timestamp": "2024-05-22T17:02:45Z",
"timestamp": "2024-05-31T22:25:22Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "d9c497fe-1dc4-4551-b46d-282f775e9509",
"id": "e56c4e1a-6b1a-4007-880c-875dc6400b73",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -38,7 +38,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "6fa01f69-de93-4610-b942-b787118146f8",
"token": "b3666f42-cc88-454e-93bd-553f71306dbe",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -57,7 +57,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "2012753940926517215",
"id": "8818573993093135925",
"triggers": null
},
"sensitive_values": {},
@@ -73,7 +73,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "2163283012438694669",
"id": "2487290649323445841",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -42,7 +42,8 @@
"display_apps": [
{}
],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -203,6 +204,8 @@
]
}
},
"timestamp": "2024-05-22T17:02:50Z",
"timestamp": "2024-05-31T22:25:26Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "c55cfcad-5422-46e5-a144-e933660bacd3",
"id": "cd49cbe2-97f4-4980-9b13-4e4008f4d594",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -38,7 +38,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "e170615d-a3a2-4dc4-a65e-4990ceeb79e5",
"token": "4b1c44cb-d960-42ef-b19e-60d169085657",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -57,7 +57,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "3512108359019802900",
"id": "6613171819431602989",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -42,7 +42,8 @@
"display_apps": [
{}
],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -203,6 +204,8 @@
]
}
},
"timestamp": "2024-05-22T17:02:48Z",
"timestamp": "2024-05-31T22:25:24Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "3fb63a4e-bb0e-4380-9ed9-8b1581943b1f",
"id": "dac3e164-c9d2-43e2-89ee-54ce5955e551",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -38,7 +38,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "eb5720a7-91fd-4e37-8085-af3c8205702c",
"token": "99ccf297-47b1-4c7c-819e-0bac896b12bd",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -57,7 +57,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "2929624824161973000",
"id": "5268162908997861371",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -118,7 +119,7 @@
],
"prior_state": {
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -227,6 +228,8 @@
]
}
},
"timestamp": "2024-05-22T17:02:52Z",
"timestamp": "2024-05-31T22:25:28Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -54,7 +54,7 @@
}
],
"env": null,
"id": "923df4d0-cf96-4cf8-aaff-426e58927a81",
"id": "2fcac464-b22b-4567-8391-7cdf592dae14",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -66,7 +66,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "f5328221-90c7-4056-83b4-7b76d6f46580",
"token": "57bcc78a-ed9b-46f9-9901-ffbdfb325871",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -85,7 +85,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "4621387386750422041",
"id": "7076770981685522602",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -118,7 +119,7 @@
],
"prior_state": {
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -222,6 +223,8 @@
]
}
},
"timestamp": "2024-05-22T17:02:55Z",
"timestamp": "2024-05-31T22:25:30Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -52,7 +52,7 @@
}
],
"env": null,
"id": "48a24332-1a90-48d9-9e03-b4e9f09c6eab",
"id": "c924e5b7-e2cb-4eb5-993e-3cc489ed5213",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -64,7 +64,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "6a2ae93f-3f25-423d-aa97-b2f1c5d9c20b",
"token": "cc8ceb98-822f-4b8f-b645-2162fada1dfb",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -83,7 +83,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "8095584601893320918",
"id": "7049248910828562611",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -224,6 +225,8 @@
]
}
],
"timestamp": "2024-05-22T17:02:57Z",
"timestamp": "2024-05-31T22:25:32Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "3bc8e20f-2024-4014-ac11-806e7e1a1e24",
"id": "b691d6a2-76de-4441-ac90-3260282dc1fb",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -38,7 +38,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "6ef0492b-8dbe-4c61-8eb8-a37acb671278",
"token": "244bf23b-b483-46f9-b2ff-7a6e746c836f",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -57,8 +57,8 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "3bc8e20f-2024-4014-ac11-806e7e1a1e24",
"id": "7ba714fa-f2b8-4d33-8987-f67466505033",
"agent_id": "b691d6a2-76de-4441-ac90-3260282dc1fb",
"id": "66ce959f-b821-4657-9bdb-6290c3b3a0b9",
"instance_id": "example"
},
"sensitive_values": {},
@@ -74,7 +74,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "4065206823139127011",
"id": "3867175311980978156",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -326,6 +327,8 @@
]
}
],
"timestamp": "2024-05-22T17:02:59Z",
"timestamp": "2024-05-31T22:25:34Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "d8d2ed23-193d-4784-9ce5-7bc0d879bb14",
"id": "d3eece5c-3d36-4e77-a67c-284d6a665004",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -38,7 +38,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "0555adfc-e969-4fd2-8cfd-47560bd1b5a3",
"token": "793d9e17-fe59-4e70-83ee-76397b81a5bd",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -58,13 +58,13 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "d8d2ed23-193d-4784-9ce5-7bc0d879bb14",
"agent_id": "d3eece5c-3d36-4e77-a67c-284d6a665004",
"command": null,
"display_name": "app1",
"external": false,
"healthcheck": [],
"icon": null,
"id": "11fa3ff2-d6ba-41ca-b1df-6c98d395c0b8",
"id": "02a5c323-badd-4a9d-bb5e-6926b8c3f317",
"name": null,
"order": null,
"relative_path": null,
@@ -89,13 +89,13 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "d8d2ed23-193d-4784-9ce5-7bc0d879bb14",
"agent_id": "d3eece5c-3d36-4e77-a67c-284d6a665004",
"command": null,
"display_name": "app2",
"external": false,
"healthcheck": [],
"icon": null,
"id": "cd1a2e37-adbc-49f0-bd99-033c62a1533e",
"id": "3f9b0fb0-fc06-49ed-b869-27b570b86b47",
"name": null,
"order": null,
"relative_path": null,
@@ -119,7 +119,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "4490911212417021152",
"id": "6739553050203442390",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -61,7 +62,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -91,7 +93,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -121,7 +124,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -460,6 +464,8 @@
]
}
},
"timestamp": "2024-05-22T17:03:01Z",
"timestamp": "2024-05-31T22:25:36Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "0ffc6582-b017-404e-b83f-48e4a5ab38bc",
"id": "2cd8a28d-b73c-4801-8748-5681512b99ed",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -38,7 +38,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "b7f0a913-ecb1-4c80-8559-fbcb435d53d0",
"token": "68c874c4-2f0d-4dff-9fd7-67209e9a08c7",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -71,7 +71,7 @@
}
],
"env": null,
"id": "1780ae95-844c-4d5c-94fb-6ccfe4a7656d",
"id": "2e773a6e-0e57-428d-bdf8-414c2aaa55fc",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -83,7 +83,7 @@
"startup_script": null,
"startup_script_behavior": "non-blocking",
"startup_script_timeout": 30,
"token": "695f8765-3d3d-4da0-9a5a-bb7b1f568bde",
"token": "98944f07-1265-4329-8fd3-c92aac95855c",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -116,7 +116,7 @@
}
],
"env": null,
"id": "333b7856-24ac-46be-9ae3-e4981b25481d",
"id": "9568f00b-0bd8-4982-a502-7b37562b1fa3",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -128,7 +128,7 @@
"startup_script": null,
"startup_script_behavior": "blocking",
"startup_script_timeout": 300,
"token": "50ddfb93-264f-4f64-8c8d-db7d8d37c0a1",
"token": "8bf8789b-9efc-4517-aa30-89b99c46dd75",
"troubleshooting_url": "https://coder.com/troubleshoot"
},
"sensitive_values": {
@@ -161,7 +161,7 @@
}
],
"env": null,
"id": "90736626-71c9-4b76-bdfc-f6ce9b3dda05",
"id": "403e5299-2f3e-499c-b90a-2fa6fc9e44e6",
"init_script": "",
"login_before_ready": false,
"metadata": [],
@@ -173,7 +173,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "8c4ae7b9-12b7-4a9c-a55a-a98cfb049103",
"token": "a10e5bfb-9756-4210-a112-877f2cfbdc0a",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -192,7 +192,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "6980014108785645805",
"id": "2053669122262711043",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -445,6 +446,8 @@
]
}
],
"timestamp": "2024-05-22T17:03:03Z",
"timestamp": "2024-05-31T22:25:38Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "c950352c-7c4a-41cc-9049-ad07ded85c47",
"id": "26bc229a-d911-4d91-8b18-c59a2f2939f4",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -38,7 +38,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "143c3974-49f5-4898-815b-c4044283ebc8",
"token": "3be506a9-b085-4bd8-a6e9-ac1769aedac5",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -57,13 +57,13 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "c950352c-7c4a-41cc-9049-ad07ded85c47",
"agent_id": "26bc229a-d911-4d91-8b18-c59a2f2939f4",
"command": null,
"display_name": null,
"external": false,
"healthcheck": [],
"icon": null,
"id": "23135384-0e9f-4efc-b74c-d3e5e878ed67",
"id": "cbfb480c-49f0-41dc-a5e5-fa8ab21514e7",
"name": null,
"order": null,
"relative_path": null,
@@ -87,7 +87,7 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "c950352c-7c4a-41cc-9049-ad07ded85c47",
"agent_id": "26bc229a-d911-4d91-8b18-c59a2f2939f4",
"command": null,
"display_name": null,
"external": false,
@@ -99,7 +99,7 @@
}
],
"icon": null,
"id": "01e73639-0fd1-4bcb-bd88-d22eb8244627",
"id": "6cc74cc4-edd4-482a-be9c-46243008081d",
"name": null,
"order": null,
"relative_path": null,
@@ -125,13 +125,13 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "c950352c-7c4a-41cc-9049-ad07ded85c47",
"agent_id": "26bc229a-d911-4d91-8b18-c59a2f2939f4",
"command": null,
"display_name": null,
"external": false,
"healthcheck": [],
"icon": null,
"id": "058c9054-9714-4a5f-9fde-8a451ab58620",
"id": "7b2131ed-3850-439e-8942-6c83fe02ce0c",
"name": null,
"order": null,
"relative_path": null,
@@ -155,7 +155,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "9051436019409847411",
"id": "6270198559972381862",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -42,7 +42,8 @@
"display_apps": [],
"metadata": [
{}
]
],
"token": true
}
},
{
@@ -431,6 +432,8 @@
]
}
],
"timestamp": "2024-05-22T17:03:06Z",
"timestamp": "2024-05-31T22:25:42Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "8352a117-1250-44ef-bba2-0abdb2a77665",
"id": "15b21cea-46cb-4e70-b648-56dceff97236",
"init_script": "",
"login_before_ready": true,
"metadata": [
@@ -47,7 +47,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "b46fd197-3be4-42f8-9c47-5a9e71a76ef6",
"token": "3308a570-7944-4238-aca8-fbc3644d7548",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -71,7 +71,7 @@
"daily_cost": 29,
"hide": true,
"icon": "/icon/server.svg",
"id": "1f7911d4-5b64-4e20-af9b-b6ee2aff602b",
"id": "28db1106-e6f0-41ff-b707-3100a99cadff",
"item": [
{
"is_null": false,
@@ -86,7 +86,7 @@
"value": ""
}
],
"resource_id": "7229373774865666851"
"resource_id": "3221770356529482934"
},
"sensitive_values": {
"item": [
@@ -110,7 +110,7 @@
"daily_cost": 20,
"hide": true,
"icon": "/icon/server.svg",
"id": "34fe7a46-2a2f-4628-8946-ef80a7ffdb5e",
"id": "a30b56a6-c122-485a-a128-4210600ad17f",
"item": [
{
"is_null": false,
@@ -119,7 +119,7 @@
"value": "world"
}
],
"resource_id": "7229373774865666851"
"resource_id": "3221770356529482934"
},
"sensitive_values": {
"item": [
@@ -139,7 +139,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "7229373774865666851",
"id": "3221770356529482934",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -42,7 +42,8 @@
"display_apps": [],
"metadata": [
{}
]
],
"token": true
}
},
{
@@ -383,6 +384,8 @@
]
}
],
"timestamp": "2024-05-22T17:03:05Z",
"timestamp": "2024-05-31T22:25:40Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -26,7 +26,7 @@
}
],
"env": null,
"id": "847150eb-c3b6-497d-9dad-8e62d478cfff",
"id": "5d102462-7646-4aae-bdac-c8b9906fb5b3",
"init_script": "",
"login_before_ready": true,
"metadata": [
@@ -47,7 +47,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "a0c4f2f5-cc40-4731-9028-636033229c9c",
"token": "1d1ccced-ce84-4cbf-a80f-f17a59e948a0",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -71,7 +71,7 @@
"daily_cost": 29,
"hide": true,
"icon": "/icon/server.svg",
"id": "3feec3a3-6f9e-4cfb-b122-2273e345def0",
"id": "35194a0a-0012-4da3-9e3a-a4d7bdcc9638",
"item": [
{
"is_null": false,
@@ -98,7 +98,7 @@
"value": "squirrel"
}
],
"resource_id": "160324296641913729"
"resource_id": "2094194534443319186"
},
"sensitive_values": {
"item": [
@@ -121,7 +121,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "160324296641913729",
"id": "2094194534443319186",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -118,7 +119,7 @@
],
"prior_state": {
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -135,7 +136,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "7fb346d2-b8c2-4f2a-99d1-a8fd54cc479e",
"id": "5f79d935-c5bc-47e4-8152-eed302afc455",
"mutable": false,
"name": "Example",
"option": null,
@@ -162,7 +163,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "0581cc2a-9e6d-4f04-93a6-88fcbd0757f0",
"id": "e8af506e-91e7-457a-8e68-f33109f30e6a",
"mutable": false,
"name": "Sample",
"option": null,
@@ -268,6 +269,8 @@
]
}
},
"timestamp": "2024-05-22T17:03:11Z",
"timestamp": "2024-05-31T22:25:46Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -17,7 +17,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "5c9f037b-3cc1-4616-b4ba-9e7322856575",
"id": "487e2328-8fa1-472f-a35d-5c017f5a2621",
"mutable": false,
"name": "Example",
"option": null,
@@ -44,7 +44,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "71a4bcc8-bbcb-4619-9641-df3bc296f58e",
"id": "c85ec281-458c-4932-a10d-049be7e1b8f8",
"mutable": false,
"name": "Sample",
"option": null,
@@ -80,7 +80,7 @@
}
],
"env": null,
"id": "327e8ab1-90be-4c87-ac7d-09630ae46827",
"id": "3d98abaf-7a38-450f-9fc9-eaebbebb1f1f",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -92,7 +92,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "794a8a86-3bb9-4b3d-bbea-acff8b513964",
"token": "3000e759-60df-4470-8f51-50ea4bc6a1ad",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -111,7 +111,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "3735840255017039964",
"id": "4580074114866058503",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -118,7 +119,7 @@
],
"prior_state": {
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -135,7 +136,7 @@
"display_name": null,
"ephemeral": true,
"icon": null,
"id": "1e85f9f5-54c2-4a6b-ba7f-8627386b94b7",
"id": "c2d5292e-1dea-434b-b5cc-dc288c2a512b",
"mutable": true,
"name": "number_example",
"option": null,
@@ -162,7 +163,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "9908f4c5-87f5-496c-9479-d0f7d49f0fdf",
"id": "689418c1-935c-40ad-aa9f-37ab4f8d9501",
"mutable": false,
"name": "number_example_max",
"option": null,
@@ -201,7 +202,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "3f2d0054-0440-4a00-98f6-befa9475a5f4",
"id": "bc7db79f-d6ef-45a2-9bbf-50710eb1db8c",
"mutable": false,
"name": "number_example_max_zero",
"option": null,
@@ -240,7 +241,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "29abca17-5bd3-4ae3-9bd3-1e45301fc509",
"id": "5e88eade-4255-4693-86bf-2c0331ca2a06",
"mutable": false,
"name": "number_example_min",
"option": null,
@@ -279,7 +280,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "95630cc0-8040-4126-92bb-967dbf8eb2ed",
"id": "26c34bb9-535d-45d7-bebd-1dcb2300f242",
"mutable": false,
"name": "number_example_min_max",
"option": null,
@@ -318,7 +319,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "c256c60a-fdfe-42f1-bbaa-27880816a7bf",
"id": "3b55387f-0117-4d34-b585-14959f4a9267",
"mutable": false,
"name": "number_example_min_zero",
"option": null,
@@ -550,6 +551,8 @@
]
}
},
"timestamp": "2024-05-22T17:03:12Z",
"timestamp": "2024-05-31T22:25:48Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -17,7 +17,7 @@
"display_name": null,
"ephemeral": true,
"icon": null,
"id": "f7cabe8c-f091-4ced-bc9b-873f54edf61b",
"id": "1f836366-337f-47a9-bc49-f4810b2f1078",
"mutable": true,
"name": "number_example",
"option": null,
@@ -44,7 +44,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "13b33312-d49b-4df3-af89-5d6ec840a6e4",
"id": "d58e721b-0134-42b6-b4b9-bb012f43a439",
"mutable": false,
"name": "number_example_max",
"option": null,
@@ -83,7 +83,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "d5ff002b-d039-42e6-b638-6bc2e3d54c2b",
"id": "4c3ff771-15ab-4a33-8067-45d5d44a5f7e",
"mutable": false,
"name": "number_example_max_zero",
"option": null,
@@ -122,7 +122,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "f382fcba-2634-44e7-ab26-866228d0679a",
"id": "11f8f368-f829-403a-8ad9-3a10df1db0bf",
"mutable": false,
"name": "number_example_min",
"option": null,
@@ -161,7 +161,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "7f1c3032-1ed9-4602-80f8-cc84489bafc9",
"id": "9de03421-e747-4084-b808-90464beb8ab4",
"mutable": false,
"name": "number_example_min_max",
"option": null,
@@ -200,7 +200,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "c474219f-f1e7-4eca-921a-1ace9a8391ee",
"id": "eb75256a-66d6-45d6-a0f5-331a885742e4",
"mutable": false,
"name": "number_example_min_zero",
"option": null,
@@ -248,7 +248,7 @@
}
],
"env": null,
"id": "138f6db3-bd8d-4a9a-8e61-abc1fdf3c3af",
"id": "e6810890-032b-4a01-9562-b9a8428dcc97",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -260,7 +260,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "1ef5dec0-3339-4e24-b781-0166cc6a9820",
"token": "c162e35d-a066-472c-a469-91d6b116fa6f",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -279,7 +279,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "5975950266738511043",
"id": "8464994280406150541",
"triggers": null
},
"sensitive_values": {},

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.2",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"planned_values": {
"root_module": {
"resources": [
@@ -31,7 +31,8 @@
},
"sensitive_values": {
"display_apps": [],
"metadata": []
"metadata": [],
"token": true
}
},
{
@@ -118,7 +119,7 @@
],
"prior_state": {
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -135,7 +136,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "2be3cd75-c44b-482e-8f78-679067d8e0a4",
"id": "e5891365-ddf0-417c-a5d7-9ae7cdc76754",
"mutable": false,
"name": "Example",
"option": [
@@ -179,7 +180,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "5a2f0407-8f11-4ac8-980d-75f919959f08",
"id": "b95cd221-cdca-4d6e-98d0-e4fb6d90dc32",
"mutable": false,
"name": "number_example",
"option": null,
@@ -206,7 +207,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "cf4b28cf-ec3c-4f53-ae27-4733a9f7d71a",
"id": "e1e5bce0-ea22-401d-8253-1b9175077abc",
"mutable": false,
"name": "number_example_max_zero",
"option": null,
@@ -245,7 +246,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "70d63380-2020-4377-ae05-cecb12c0d709",
"id": "26a6eaca-c9ae-4130-a734-6c290637b250",
"mutable": false,
"name": "number_example_min_max",
"option": null,
@@ -284,7 +285,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "ec5827c2-2511-4f16-bd85-6249517c9e5b",
"id": "ad985f1d-21fe-4ce1-988d-903084016cb4",
"mutable": false,
"name": "number_example_min_zero",
"option": null,
@@ -323,7 +324,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "eec8845e-4316-450a-a5b7-eaa9567f469a",
"id": "9465cc3a-703a-4218-8fa4-d16a1631e648",
"mutable": false,
"name": "Sample",
"option": null,
@@ -354,7 +355,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "3b860d24-85ac-4540-b309-9321e732dfc4",
"id": "547f8420-0630-4c4d-9507-e2d63640d0d9",
"mutable": true,
"name": "First parameter from module",
"option": null,
@@ -381,7 +382,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "b36105e3-9bf1-43c7-a857-078ef1e8f95d",
"id": "5c32dcad-d54a-474f-97f0-fbcc8aaba9bd",
"mutable": true,
"name": "Second parameter from module",
"option": null,
@@ -413,7 +414,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "a2bee9f2-8a3c-404c-839b-01b6cd840707",
"id": "2362ba5e-0779-472c-bd3c-22446fd14075",
"mutable": true,
"name": "First parameter from child module",
"option": null,
@@ -440,7 +441,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "deb13c45-ed6d-45b6-b6eb-d319143fa8f2",
"id": "0a8f6df4-364f-4d5f-b935-7dee8c568e10",
"mutable": true,
"name": "Second parameter from child module",
"option": null,
@@ -793,6 +794,8 @@
}
}
},
"timestamp": "2024-05-22T17:03:08Z",
"timestamp": "2024-05-31T22:25:44Z",
"applyable": true,
"complete": true,
"errored": false
}

View File

@@ -1,6 +1,6 @@
{
"format_version": "1.0",
"terraform_version": "1.7.5",
"terraform_version": "1.8.4",
"values": {
"root_module": {
"resources": [
@@ -17,7 +17,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "7fa1e2f7-36a4-49cd-b92a-b3fc8732d359",
"id": "9f041124-ccf3-4b7b-9e0d-4d37335a6f98",
"mutable": false,
"name": "Example",
"option": [
@@ -61,7 +61,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "86a60580-7221-4bab-b229-9cb61bdb56a0",
"id": "ab5035e4-8dab-453d-92bc-9b866af26c78",
"mutable": false,
"name": "number_example",
"option": null,
@@ -88,7 +88,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "ed6bc6e5-b4ff-48b9-88b0-df5faa74ae66",
"id": "bdf84ab6-1029-4645-a2df-cd897f30c145",
"mutable": false,
"name": "number_example_max_zero",
"option": null,
@@ -127,7 +127,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "340b19e1-f651-4321-96b1-7908c2c66914",
"id": "b283766e-7e58-459d-a81f-aa71a95bbc0b",
"mutable": false,
"name": "number_example_min_max",
"option": null,
@@ -166,7 +166,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "f19c6763-2e55-40dd-9b49-82e9181e5b1b",
"id": "7a4f8f6d-d81a-4b15-9d5b-6f221f2a6b07",
"mutable": false,
"name": "number_example_min_zero",
"option": null,
@@ -205,7 +205,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "02169810-8080-4dc6-a656-5fbda745659e",
"id": "fd12f0d0-87dc-4d88-bcdc-352c11bd2144",
"mutable": false,
"name": "Sample",
"option": null,
@@ -241,7 +241,7 @@
}
],
"env": null,
"id": "42edc650-ddb6-4ed9-9624-7788d60d1507",
"id": "a20d4cf7-2d49-4ab8-8858-a9e1531e7033",
"init_script": "",
"login_before_ready": true,
"metadata": [],
@@ -253,7 +253,7 @@
"startup_script": null,
"startup_script_behavior": null,
"startup_script_timeout": 300,
"token": "c767a648-e670-4c6b-a28b-8559033e92a7",
"token": "0d8692b3-746f-4f2e-b0cc-7952ee240ba4",
"troubleshooting_url": null
},
"sensitive_values": {
@@ -272,7 +272,7 @@
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "7506678111935039701",
"id": "9033341587141190203",
"triggers": null
},
"sensitive_values": {},
@@ -297,7 +297,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "11b1ae03-cf81-4f60-9be1-bd4c0586516d",
"id": "6be6ebff-574c-4ab6-b314-a65f4f20446e",
"mutable": true,
"name": "First parameter from module",
"option": null,
@@ -324,7 +324,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "79d87261-bfda-46ee-958d-7d62252101ad",
"id": "d7e3d42e-dc51-47f2-ae5f-1b1bdaa85e25",
"mutable": true,
"name": "Second parameter from module",
"option": null,
@@ -356,7 +356,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "30c4c518-116a-4591-a571-886101cfcdfa",
"id": "69f71896-5cc4-44d0-ae7a-b7a5514a07ae",
"mutable": true,
"name": "First parameter from child module",
"option": null,
@@ -383,7 +383,7 @@
"display_name": null,
"ephemeral": false,
"icon": null,
"id": "4c7d9f15-da45-453e-85eb-1d22c9baa54c",
"id": "9a2b177e-8f3c-4d6b-b302-3ba2f0e6c76b",
"mutable": true,
"name": "Second parameter from child module",
"option": null,

View File

@@ -1 +1 @@
1.7.5
1.8.4

View File

@@ -26,7 +26,7 @@ RUN apk add --no-cache \
# Terraform was disabled in the edge repo due to a build issue.
# https://gitlab.alpinelinux.org/alpine/aports/-/commit/f3e263d94cfac02d594bef83790c280e045eba35
# Using wget for now. Note that busybox unzip doesn't support streaming.
RUN ARCH="$(arch)"; if [ "${ARCH}" == "x86_64" ]; then ARCH="amd64"; elif [ "${ARCH}" == "aarch64" ]; then ARCH="arm64"; fi; wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_linux_${ARCH}.zip" && \
RUN ARCH="$(arch)"; if [ "${ARCH}" == "x86_64" ]; then ARCH="amd64"; elif [ "${ARCH}" == "aarch64" ]; then ARCH="arm64"; fi; wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.8.4/terraform_1.8.4_linux_${ARCH}.zip" && \
busybox unzip /tmp/terraform.zip -d /usr/local/bin && \
rm -f /tmp/terraform.zip && \
chmod +x /usr/local/bin/terraform && \

View File

@@ -217,7 +217,7 @@ release_branch=${tag_version[0]}
new_version=${tag_version[1]}
new_version="${new_version%$'\n'}" # Remove the trailing newline.
release_notes="$(execrelative ./release/generate_release_notes.sh --old-version "$old_version" --new-version "$new_version" --ref "$ref")"
release_notes="$(execrelative ./release/generate_release_notes.sh --old-version "$old_version" --new-version "$new_version" --ref "$ref" --$channel)"
mkdir -p build
release_notes_file="build/RELEASE-${new_version}.md"
@@ -374,7 +374,7 @@ You can follow the release progress [here](https://github.com/coder/coder/action
create_pr_stash=1
fi
maybedryrun "${dry_run}" git checkout -b "${pr_branch}" "${remote}/${branch}"
execrelative go run ./release autoversion --channel "${channel}" "${new_version}" --dry-run
execrelative go run ./release autoversion --channel "${channel}" "${new_version}" --dry-run="${dry_run}"
maybedryrun "${dry_run}" git add docs
maybedryrun "${dry_run}" git commit -m "${title}"
# Return to previous branch.
@@ -384,7 +384,7 @@ You can follow the release progress [here](https://github.com/coder/coder/action
fi
# Push the branch so it's available for gh to create the PR.
maybedryrun "${dry_run}" git push -u "{remote}" "${pr_branch}"
maybedryrun "${dry_run}" git push -u "${remote}" "${pr_branch}"
log "Creating pull request..."
maybedryrun "${dry_run}" gh pr create \

View File

@@ -242,6 +242,7 @@ func (r *releaseCommand) promoteVersionToStable(ctx context.Context, inv *serpen
updatedBody := removeMainlineBlurb(newStable.GetBody())
updatedBody = addStableSince(time.Now().UTC(), updatedBody)
updatedNewStable.Body = github.String(updatedBody)
updatedNewStable.MakeLatest = github.String("true")
updatedNewStable.Prerelease = github.Bool(false)
updatedNewStable.Draft = github.Bool(false)
if !r.dryRun {

View File

@@ -86,8 +86,8 @@ fi
# shellcheck source=scripts/release/check_commit_metadata.sh
source "$SCRIPT_DIR/check_commit_metadata.sh" "$old_version" "$ref"
prev_increment=$increment
if ((COMMIT_METADATA_BREAKING == 1)); then
prev_increment=$increment
if [[ $increment == patch ]]; then
increment=minor
fi

View File

@@ -41,6 +41,7 @@ global.scrollTo = jest.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.open = jest.fn();
navigator.sendBeacon = jest.fn();
// Polyfill the getRandomValues that is used on utils/random.ts
Object.defineProperty(global.self, "crypto", {

View File

@@ -171,6 +171,7 @@ export interface BuildInfoResponse {
readonly external_url: string;
readonly version: string;
readonly dashboard_url: string;
readonly telemetry: boolean;
readonly workspace_proxy: boolean;
readonly agent_api_version: string;
readonly upgrade_message: string;
@@ -506,7 +507,6 @@ export interface ExternalAuthConfig {
readonly app_installations_url: string;
readonly no_refresh: boolean;
readonly scopes: readonly string[];
readonly extra_token_keys: readonly string[];
readonly device_flow: boolean;
readonly device_code_url: string;
readonly regex: string;

View File

@@ -19,7 +19,6 @@ const meta: Meta<typeof ExternalAuthSettingsPageView> = {
app_installations_url: "",
no_refresh: false,
scopes: [],
extra_token_keys: [],
device_flow: true,
device_code_url: "",
display_icon: "",

View File

@@ -1,4 +1,4 @@
import type { FC } from "react";
import { useEffect, type FC } from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
import { Navigate, useLocation, useNavigate } from "react-router-dom";
@@ -8,6 +8,7 @@ import { useAuthContext } from "contexts/auth/AuthProvider";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { getApplicationName } from "utils/appearance";
import { retrieveRedirect } from "utils/redirect";
import { sendDeploymentEvent } from "utils/telemetry";
import { LoginPageView } from "./LoginPageView";
export const LoginPage: FC = () => {
@@ -19,16 +20,37 @@ export const LoginPage: FC = () => {
signIn,
isSigningIn,
signInError,
user,
} = useAuthContext();
const authMethodsQuery = useQuery(authMethods());
const redirectTo = retrieveRedirect(location.search);
const applicationName = getApplicationName();
const navigate = useNavigate();
const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
useEffect(() => {
if (!buildInfoQuery.data || isSignedIn) {
// isSignedIn already tracks with window.href!
return;
}
// This uses `navigator.sendBeacon`, so navigating away will not prevent it!
sendDeploymentEvent(buildInfoQuery.data, {
type: "deployment_login",
user_id: user?.id,
});
}, [isSignedIn, buildInfoQuery.data, user?.id]);
if (isSignedIn) {
if (buildInfoQuery.data) {
// This uses `navigator.sendBeacon`, so window.href
// will not stop the request from being sent!
sendDeploymentEvent(buildInfoQuery.data, {
type: "deployment_login",
user_id: user?.id,
});
}
// If the redirect is going to a workspace application, and we
// are missing authentication, then we need to change the href location
// to trigger a HTTP request. This allows the BE to generate the auth
@@ -74,6 +96,15 @@ export const LoginPage: FC = () => {
isSigningIn={isSigningIn}
onSignIn={async ({ email, password }) => {
await signIn(email, password);
if (buildInfoQuery.data) {
// This uses `navigator.sendBeacon`, so navigating away
// will not prevent it!
sendDeploymentEvent(buildInfoQuery.data, {
type: "deployment_login",
user_id: user?.id,
});
}
navigate("/");
}}
/>

View File

@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
import { HttpResponse, http } from "msw";
import { createMemoryRouter } from "react-router-dom";
import type { Response, User } from "api/typesGenerated";
import { MockUser } from "testHelpers/entities";
import { MockBuildInfo, MockUser } from "testHelpers/entities";
import {
renderWithRouter,
waitForLoaderToBeRemoved,
@@ -99,4 +99,42 @@ describe("Setup Page", () => {
await fillForm();
await waitFor(() => screen.findByText("Templates"));
});
it("calls sendBeacon with telemetry", async () => {
const sendBeacon = jest.fn();
Object.defineProperty(window.navigator, "sendBeacon", {
value: sendBeacon,
});
renderWithRouter(
createMemoryRouter(
[
{
path: "/setup",
element: <SetupPage />,
},
{
path: "/templates",
element: <h1>Templates</h1>,
},
],
{ initialEntries: ["/setup"] },
),
);
await waitForLoaderToBeRemoved();
await waitFor(() => {
expect(navigator.sendBeacon).toBeCalledWith(
"https://coder.com/api/track-deployment",
new Blob(
[
JSON.stringify({
type: "deployment_setup",
deployment_id: MockBuildInfo.deployment_id,
}),
],
{
type: "application/json",
},
),
);
});
});
});

View File

@@ -1,11 +1,14 @@
import type { FC } from "react";
import { useEffect, type FC } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation } from "react-query";
import { useMutation, useQuery } from "react-query";
import { Navigate, useNavigate } from "react-router-dom";
import { buildInfo } from "api/queries/buildInfo";
import { createFirstUser } from "api/queries/users";
import { Loader } from "components/Loader/Loader";
import { useAuthContext } from "contexts/auth/AuthProvider";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { pageTitle } from "utils/page";
import { sendDeploymentEvent } from "utils/telemetry";
import { SetupPageView } from "./SetupPageView";
export const SetupPage: FC = () => {
@@ -18,7 +21,17 @@ export const SetupPage: FC = () => {
} = useAuthContext();
const createFirstUserMutation = useMutation(createFirstUser());
const setupIsComplete = !isConfiguringTheFirstUser;
const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
const navigate = useNavigate();
useEffect(() => {
if (!buildInfoQuery.data) {
return;
}
sendDeploymentEvent(buildInfoQuery.data, {
type: "deployment_setup",
});
}, [buildInfoQuery.data]);
if (isLoading) {
return <Loader fullscreen />;

View File

@@ -22,6 +22,15 @@ export const FormError: Story = {
},
};
export const TrialError: Story = {
args: {
error: mockApiError({
message: "Couldn't generate trial!",
detail: "It looks like your team is already trying Coder.",
}),
},
};
export const Loading: Story = {
args: {
isLoading: true,

View File

@@ -1,13 +1,16 @@
import LoadingButton from "@mui/lab/LoadingButton";
import AlertTitle from "@mui/material/AlertTitle";
import Autocomplete from "@mui/material/Autocomplete";
import Checkbox from "@mui/material/Checkbox";
import Link from "@mui/material/Link";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import { isAxiosError } from "axios";
import { type FormikContextType, useFormik } from "formik";
import type { FC } from "react";
import * as Yup from "yup";
import type * as TypesGen from "api/typesGenerated";
import { Alert, AlertDetail } from "components/Alert/Alert";
import { FormFields, VerticalForm } from "components/Form/Form";
import { CoderIcon } from "components/Icons/CoderIcon";
import { SignInLayout } from "components/SignInLayout/SignInLayout";
@@ -187,7 +190,7 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
<div css={{ fontSize: 14, paddingTop: 4 }}>
<span css={{ display: "block", fontWeight: 600 }}>
Start a 30-day free trial of Enterprise
Start a free trial of Enterprise
</span>
<span
css={(theme) => ({
@@ -316,6 +319,21 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
</>
)}
{isAxiosError(error) && error.response?.data?.message && (
<Alert severity="error">
<AlertTitle>{error.response.data.message}</AlertTitle>
{error.response.data.detail && (
<AlertDetail>
{error.response.data.detail}
<br />
<Link target="_blank" href="https://coder.com/contact/sales">
Contact Sales
</Link>
</AlertDetail>
)}
</Alert>
)}
<LoadingButton
fullWidth
loading={isLoading}

View File

@@ -97,6 +97,21 @@ export const StartButton: FC<ActionButtonPropsWithWorkspace> = ({
);
};
export const UpdateAndStartButton: FC<ActionButtonProps> = ({
handleAction,
}) => {
return (
<Tooltip title="This template requires automatic updates on workspace startup. Contact your administrator if you want to preserve the template version.">
<TopbarButton
startIcon={<PlayCircleOutlineIcon />}
onClick={() => handleAction()}
>
Update and start&hellip;
</TopbarButton>
</Tooltip>
);
};
export const StopButton: FC<ActionButtonProps> = ({
handleAction,
loading,
@@ -146,16 +161,13 @@ export const RestartButton: FC<ActionButtonPropsWithWorkspace> = ({
);
};
export const UpdateAndStartButton: FC<ActionButtonProps> = ({
export const UpdateAndRestartButton: FC<ActionButtonProps> = ({
handleAction,
}) => {
return (
<Tooltip title="This template requires automatic updates on workspace startup. Contact your administrator if you want to preserve the template version.">
<TopbarButton
startIcon={<PlayCircleOutlineIcon />}
onClick={() => handleAction()}
>
Update and start&hellip;
<TopbarButton startIcon={<ReplayIcon />} onClick={() => handleAction()}>
Update and restart&hellip;
</TopbarButton>
</Tooltip>
);

View File

@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within, expect } from "@storybook/test";
import * as Mocks from "testHelpers/entities";
import { WorkspaceActions } from "./WorkspaceActions";
@@ -25,6 +26,37 @@ export const Running: Story = {
},
};
export const RunningUpdateAvailable: Story = {
name: "Running (Update available)",
args: {
workspace: {
...Mocks.MockWorkspace,
outdated: true,
},
},
};
export const RunningRequireActiveVersion: Story = {
name: "Running (No required update)",
args: {
workspace: {
...Mocks.MockWorkspace,
template_require_active_version: true,
},
},
};
export const RunningUpdateRequired: Story = {
name: "Running (Update Required)",
args: {
workspace: {
...Mocks.MockWorkspace,
template_require_active_version: true,
outdated: true,
},
},
};
export const Stopping: Story = {
args: {
workspace: Mocks.MockStoppingWorkspace,
@@ -37,15 +69,54 @@ export const Stopped: Story = {
},
};
export const Canceling: Story = {
export const StoppedUpdateAvailable: Story = {
name: "Stopped (Update available)",
args: {
workspace: Mocks.MockCancelingWorkspace,
workspace: {
...Mocks.MockStoppedWorkspace,
outdated: true,
},
},
};
export const Canceled: Story = {
export const StoppedRequireActiveVersion: Story = {
name: "Stopped (No required update)",
args: {
workspace: Mocks.MockCanceledWorkspace,
workspace: {
...Mocks.MockStoppedWorkspace,
template_require_active_version: true,
},
},
};
export const StoppedUpdateRequired: Story = {
name: "Stopped (Update Required)",
args: {
workspace: {
...Mocks.MockStoppedWorkspace,
template_require_active_version: true,
outdated: true,
},
},
};
export const Updating: Story = {
args: {
workspace: Mocks.MockOutdatedWorkspace,
isUpdating: true,
},
};
export const Restarting: Story = {
args: {
workspace: Mocks.MockStoppingWorkspace,
isRestarting: true,
},
};
export const Canceling: Story = {
args: {
workspace: Mocks.MockCancelingWorkspace,
},
};
@@ -80,41 +151,6 @@ export const FailedWithDebug: Story = {
},
};
export const Updating: Story = {
args: {
isUpdating: true,
workspace: Mocks.MockOutdatedWorkspace,
},
};
export const RequireActiveVersionStarted: Story = {
args: {
workspace: Mocks.MockOutdatedRunningWorkspaceRequireActiveVersion,
canChangeVersions: false,
},
};
export const RequireActiveVersionStopped: Story = {
args: {
workspace: Mocks.MockOutdatedStoppedWorkspaceRequireActiveVersion,
canChangeVersions: false,
},
};
export const AlwaysUpdateStarted: Story = {
args: {
workspace: Mocks.MockOutdatedRunningWorkspaceAlwaysUpdate,
canChangeVersions: true,
},
};
export const AlwaysUpdateStopped: Story = {
args: {
workspace: Mocks.MockOutdatedStoppedWorkspaceAlwaysUpdate,
canChangeVersions: true,
},
};
export const CancelShownForOwner: Story = {
args: {
workspace: {
@@ -124,6 +160,7 @@ export const CancelShownForOwner: Story = {
isOwner: true,
},
};
export const CancelShownForUser: Story = {
args: {
workspace: Mocks.MockStartingWorkspace,
@@ -140,3 +177,15 @@ export const CancelHiddenForUser: Story = {
isOwner: false,
},
};
export const CanDeleteDormantWorkspace: Story = {
args: {
workspace: Mocks.MockDormantWorkspace,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: "More options" }));
const deleteButton = canvas.getByText("Delete…");
await expect(deleteButton).toBeEnabled();
},
};

View File

@@ -25,6 +25,7 @@ import {
ActivateButton,
FavoriteButton,
UpdateAndStartButton,
UpdateAndRestartButton,
} from "./Buttons";
import { type ActionType, abilitiesByWorkspaceStatus } from "./constants";
import { DebugButton } from "./DebugButton";
@@ -85,12 +86,12 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
const mustUpdate = mustUpdateWorkspace(workspace, canChangeVersions);
const tooltipText = getTooltipText(workspace, mustUpdate, canChangeVersions);
const canBeUpdated = workspace.outdated && canAcceptJobs;
// A mapping of button type to the corresponding React component
const buttonMapping: Record<ActionType, ReactNode> = {
update: <UpdateButton handleAction={handleUpdate} />,
updateAndStart: <UpdateAndStartButton handleAction={handleUpdate} />,
updateAndRestart: <UpdateAndRestartButton handleAction={handleUpdate} />,
updating: <UpdateButton loading handleAction={handleUpdate} />,
start: (
<StartButton
@@ -148,13 +149,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
enableBuildParameters={workspace.latest_build.transition === "start"}
/>
),
toggleFavorite: (
<FavoriteButton
workspaceID={workspace.id}
isFavorite={workspace.favorite}
onToggle={handleToggleFavorite}
/>
),
};
return (
@@ -162,25 +156,22 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
css={{ display: "flex", alignItems: "center", gap: 8 }}
data-testid="workspace-actions"
>
{canBeUpdated && (
<>
{isUpdating
? buttonMapping.updating
: workspace.template_require_active_version
? buttonMapping.updateAndStart
: buttonMapping.update}
</>
)}
{isRestarting
? buttonMapping.restarting
: actions.map((action) => (
<Fragment key={action}>{buttonMapping[action]}</Fragment>
))}
{/* Restarting must be handled separately, because it otherwise would appear as stopping */}
{isUpdating
? buttonMapping.updating
: isRestarting
? buttonMapping.restarting
: actions.map((action) => (
<Fragment key={action}>{buttonMapping[action]}</Fragment>
))}
{showCancel && <CancelButton handleAction={handleCancel} />}
{buttonMapping.toggleFavorite}
<FavoriteButton
workspaceID={workspace.id}
isFavorite={workspace.favorite}
onToggle={handleToggleFavorite}
/>
<MoreMenu>
<MoreMenuTrigger>
@@ -240,7 +231,11 @@ function getTooltipText(
return "";
}
if (!mustUpdate && canChangeVersions) {
if (
!mustUpdate &&
canChangeVersions &&
workspace.template_require_active_version
) {
return "This template requires automatic updates on workspace startup, but template administrators can ignore this policy.";
}

View File

@@ -6,16 +6,19 @@ import type { Workspace } from "api/typesGenerated";
export const actionTypes = [
"start",
"starting",
// Replaces start when an update is required.
"updateAndStart",
"stop",
"stopping",
"restart",
"restarting",
// Replaces restart when an update is required.
"updateAndRestart",
"deleting",
"update",
"updating",
"activate",
"activating",
"toggleFavorite",
// There's no need for a retrying state because retrying starts a transition
// into one of the starting, stopping, or deleting states (based on the
@@ -23,10 +26,6 @@ export const actionTypes = [
"retry",
"debug",
// When a template requires updates, we aim to display a distinct update
// button that clearly indicates a mandatory update.
"updateAndStart",
// These are buttons that should be used with disabled UI elements
"canceling",
"deleted",
@@ -49,18 +48,11 @@ export const abilitiesByWorkspaceStatus = (
return {
actions: ["activate"],
canCancel: false,
canAcceptJobs: false,
canAcceptJobs: true,
};
}
const status = workspace.latest_build.status;
if (status === "failed" && canDebug) {
return {
actions: ["retry", "debug"],
canCancel: false,
canAcceptJobs: true,
};
}
switch (status) {
case "starting": {
@@ -73,10 +65,12 @@ export const abilitiesByWorkspaceStatus = (
case "running": {
const actions: ActionType[] = ["stop"];
// If the template requires the latest version, we prevent the user from
// restarting the workspace without updating it first. In the Buttons
// component, we display an UpdateAndStart component to facilitate this.
if (!workspace.template_require_active_version) {
if (workspace.template_require_active_version && workspace.outdated) {
actions.push("updateAndRestart");
} else {
if (workspace.outdated) {
actions.unshift("update");
}
actions.push("restart");
}
@@ -96,10 +90,12 @@ export const abilitiesByWorkspaceStatus = (
case "stopped": {
const actions: ActionType[] = [];
// If the template requires the latest version, we prevent the user from
// starting the workspace without updating it first. In the Buttons
// component, we display an UpdateAndStart component to facilitate this.
if (!workspace.template_require_active_version) {
if (workspace.template_require_active_version && workspace.outdated) {
actions.push("updateAndStart");
} else {
if (workspace.outdated) {
actions.unshift("update");
}
actions.push("start");
}
@@ -117,14 +113,31 @@ export const abilitiesByWorkspaceStatus = (
};
}
case "failed": {
const actions: ActionType[] = ["retry"];
if (canDebug) {
actions.push("debug");
}
if (workspace.outdated) {
actions.unshift("update");
}
return {
actions: ["retry"],
actions,
canCancel: false,
canAcceptJobs: true,
};
}
// Disabled states
case "pending": {
return {
actions: ["pending"],
canCancel: false,
canAcceptJobs: false,
};
}
case "canceling": {
return {
actions: ["canceling"],
@@ -146,15 +159,8 @@ export const abilitiesByWorkspaceStatus = (
canAcceptJobs: false,
};
}
case "pending": {
return {
actions: ["pending"],
canCancel: false,
canAcceptJobs: false,
};
}
default: {
default:
throw new Error(`Unknown workspace status: ${status}`);
}
}
};

View File

@@ -18,7 +18,6 @@ import {
import type { Template, Workspace } from "api/typesGenerated";
import { TopbarData, TopbarIcon } from "components/FullPageLayout/Topbar";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Pill } from "components/Pill/Pill";
import { useTime } from "hooks/useTime";
import { getWorkspaceActivityStatus } from "modules/workspaces/activity";
import {
@@ -170,11 +169,9 @@ const AutostopDisplay: FC<AutostopDisplayProps> = ({
const [showControlsAnyway, setShowControlsAnyway] = useState(false);
let onClickScheduleIcon: (() => void) | undefined;
let activity: ReactNode = null;
if (activityStatus === "connected") {
onClickScheduleIcon = () => setShowControlsAnyway((it) => !it);
activity = <Pill type="active">Connected</Pill>;
const now = dayjs();
const noRequiredStopSoon =
@@ -183,12 +180,7 @@ const AutostopDisplay: FC<AutostopDisplayProps> = ({
// User has shown controls manually, or we should warn about a nearby required stop
if (!showControlsAnyway && noRequiredStopSoon) {
return (
<>
{activity}
<WorkspaceScheduleContainer onClickIcon={onClickScheduleIcon} />
</>
);
return <WorkspaceScheduleContainer onClickIcon={onClickScheduleIcon} />;
}
}
@@ -239,24 +231,18 @@ const AutostopDisplay: FC<AutostopDisplayProps> = ({
if (tooltip) {
return (
<>
{activity}
<WorkspaceScheduleContainer onClickIcon={onClickScheduleIcon}>
<Tooltip title={tooltip}>{display}</Tooltip>
{controls}
</WorkspaceScheduleContainer>
</>
<WorkspaceScheduleContainer onClickIcon={onClickScheduleIcon}>
<Tooltip title={tooltip}>{display}</Tooltip>
{controls}
</WorkspaceScheduleContainer>
);
}
return (
<>
{activity}
<WorkspaceScheduleContainer onClickIcon={onClickScheduleIcon}>
{display}
{controls}
</WorkspaceScheduleContainer>
</>
<WorkspaceScheduleContainer onClickIcon={onClickScheduleIcon}>
{display}
{controls}
</WorkspaceScheduleContainer>
);
};

View File

@@ -202,6 +202,7 @@ export const MockBuildInfo: TypesGen.BuildInfoResponse = {
workspace_proxy: false,
upgrade_message: "My custom upgrade message",
deployment_id: "510d407f-e521-4180-b559-eab4a6d802b8",
telemetry: true,
};
export const MockSupportLinks: TypesGen.LinkConfig[] = [
@@ -1172,10 +1173,6 @@ export const MockOutdatedRunningWorkspaceRequireActiveVersion: TypesGen.Workspac
id: "test-outdated-workspace-require-active-version",
outdated: true,
template_require_active_version: true,
latest_build: {
...MockWorkspaceBuild,
status: "running",
},
};
export const MockOutdatedRunningWorkspaceAlwaysUpdate: TypesGen.Workspace = {

View File

@@ -25,6 +25,7 @@
"datagrip.svg",
"dataspell.svg",
"debian.svg",
"desktop.svg",
"discord.svg",
"do.png",
"docker-white.svg",

View File

@@ -0,0 +1,33 @@
import type { BuildInfoResponse } from "api/typesGenerated";
// sendDeploymentEvent sends a CORs payload to coder.com
// to track a deployment event.
export const sendDeploymentEvent = (
buildInfo: BuildInfoResponse,
payload: {
type: "deployment_setup" | "deployment_login";
user_id?: string;
},
) => {
if (typeof navigator === "undefined" || !navigator.sendBeacon) {
// It's fine if we don't report this, it's not required!
return;
}
if (!buildInfo.telemetry) {
return;
}
navigator.sendBeacon(
"https://coder.com/api/track-deployment",
new Blob(
[
JSON.stringify({
...payload,
deployment_id: buildInfo.deployment_id,
}),
],
{
type: "application/json",
},
),
);
};

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31 6V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V6C1 4.35 2.35 3 4 3H28C29.65 3 31 4.35 31 6Z" fill="#2197F3"/>
<path d="M21 27H17V24C17 23.4478 16.5522 23 16 23C15.4478 23 15 23.4478 15 24V27H11C10.4478 27 10 27.4478 10 28C10 28.5522 10.4478 29 11 29H21C21.5522 29 22 28.5522 22 28C22 27.4478 21.5522 27 21 27Z" fill="#FFC10A"/>
<path d="M31 17V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V17H31Z" fill="#3F51B5"/>
</svg>

After

Width:  |  Height:  |  Size: 684 B