diff --git a/.github/actions/setup-tf/action.yaml b/.github/actions/setup-tf/action.yaml index c52f1138e03ca..f130bcdb7d028 100644 --- a/.github/actions/setup-tf/action.yaml +++ b/.github/actions/setup-tf/action.yaml @@ -7,5 +7,5 @@ runs: - name: Install Terraform uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: - terraform_version: 1.9.8 + terraform_version: 1.10.5 terraform_wrapper: false diff --git a/cli/cliui/agent.go b/cli/cliui/agent.go index f2c1378eecb7a..3bb6fee7be769 100644 --- a/cli/cliui/agent.go +++ b/cli/cliui/agent.go @@ -120,7 +120,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO if agent.Status == codersdk.WorkspaceAgentTimeout { now := time.Now() sw.Log(now, codersdk.LogLevelInfo, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.") - sw.Log(now, codersdk.LogLevelInfo, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#agent-connection-issues", opts.DocsURL))) + sw.Log(now, codersdk.LogLevelInfo, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#agent-connection-issues", opts.DocsURL))) for agent.Status == codersdk.WorkspaceAgentTimeout { if agent, err = fetch(); err != nil { return xerrors.Errorf("fetch: %w", err) @@ -225,13 +225,13 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO sw.Fail(stage, safeDuration(sw, agent.ReadyAt, agent.StartedAt)) // Use zero time (omitted) to separate these from the startup logs. sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script exited with an error and your workspace may be incomplete.") - sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#startup-script-exited-with-an-error", opts.DocsURL))) + sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#startup-script-exited-with-an-error", opts.DocsURL))) default: switch { case agent.LifecycleState.Starting(): // Use zero time (omitted) to separate these from the startup logs. sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup scripts are still running and your workspace may be incomplete.") - sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#your-workspace-may-be-incomplete", opts.DocsURL))) + sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#your-workspace-may-be-incomplete", opts.DocsURL))) // Note: We don't complete or fail the stage here, it's // intentionally left open to indicate this stage didn't // complete. @@ -253,7 +253,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO stage := "The workspace agent lost connection" sw.Start(stage) sw.Log(time.Now(), codersdk.LogLevelWarn, "Wait for it to reconnect or restart your workspace.") - sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/templates#agent-connection-issues", opts.DocsURL))) + sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#agent-connection-issues", opts.DocsURL))) disconnectedAt := agent.DisconnectedAt for agent.Status == codersdk.WorkspaceAgentDisconnected { diff --git a/cli/ping.go b/cli/ping.go index 0e219d5762f86..19191b92916bb 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -159,7 +159,7 @@ func (r *RootCmd) ping() *serpent.Command { LocalNetInfo: ni, Verbose: r.verbose, PingP2P: didP2p, - TroubleshootingURL: appearanceConfig.DocsURL + "/networking/troubleshooting", + TroubleshootingURL: appearanceConfig.DocsURL + "/admin/networking/troubleshooting", } awsRanges, err := cliutil.FetchAWSIPRanges(diagCtx, cliutil.AWSIPRangesURL) diff --git a/cli/server.go b/cli/server.go index 48f049c163b3b..cfb5ecaf542ce 100644 --- a/cli/server.go +++ b/cli/server.go @@ -781,40 +781,42 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // This should be output before the logs start streaming. cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):") - if vals.Telemetry.Enable { - 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(), - 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 { - return nil - } - - email, trial, err := options.ParseLicenseClaims(lic.JWT) - if err != nil { - return err - } - if email != "" { - lic.Email = &email - } - lic.Trial = &trial + deploymentConfigWithoutSecrets, err := vals.WithoutSecrets() + if err != nil { + return xerrors.Errorf("remove secrets from deployment values: %w", err) + } + telemetryReporter, err := telemetry.New(telemetry.Options{ + Disabled: !vals.Telemetry.Enable.Value(), + BuiltinPostgres: builtinPostgres, + DeploymentID: deploymentID, + Database: options.Database, + Logger: logger.Named("telemetry"), + URL: vals.Telemetry.URL.Value(), + Tunnel: tunnel != nil, + DeploymentConfig: deploymentConfigWithoutSecrets, + ParseLicenseJWT: func(lic *telemetry.License) error { + // This will be nil when running in AGPL-only mode. + if options.ParseLicenseClaims == nil { return nil - }, - }) - if err != nil { - return xerrors.Errorf("create telemetry reporter: %w", err) - } - defer options.Telemetry.Close() + } + + email, trial, err := options.ParseLicenseClaims(lic.JWT) + if err != nil { + return err + } + if email != "" { + lic.Email = &email + } + lic.Trial = &trial + return nil + }, + }) + if err != nil { + return xerrors.Errorf("create telemetry reporter: %w", err) + } + defer telemetryReporter.Close() + if vals.Telemetry.Enable.Value() { + options.Telemetry = telemetryReporter } else { logger.Warn(ctx, fmt.Sprintf(`telemetry disabled, unable to notify of security issues. Read more: %s/admin/setup/telemetry`, vals.DocsURL.String())) } diff --git a/cli/server_test.go b/cli/server_test.go index fa96e192f7eb3..988fde808dc5c 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -39,6 +39,7 @@ import ( "tailscale.com/types/key" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/cli" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/config" @@ -947,36 +948,40 @@ func TestServer(t *testing.T) { t.Run("Telemetry", func(t *testing.T) { t.Parallel() - deployment := make(chan struct{}, 64) - snapshot := make(chan *telemetry.Snapshot, 64) - r := chi.NewRouter() - r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusAccepted) - deployment <- struct{}{} - }) - r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusAccepted) - ss := &telemetry.Snapshot{} - err := json.NewDecoder(r.Body).Decode(ss) - require.NoError(t, err) - snapshot <- ss - }) - server := httptest.NewServer(r) - defer server.Close() + telemetryServerURL, deployment, snapshot := mockTelemetryServer(t) - inv, _ := clitest.New(t, + inv, cfg := clitest.New(t, "server", "--in-memory", "--http-address", ":0", "--access-url", "http://example.com", "--telemetry", - "--telemetry-url", server.URL, + "--telemetry-url", telemetryServerURL.String(), "--cache-dir", t.TempDir(), ) clitest.Start(t, inv) <-deployment <-snapshot + + accessURL := waitAccessURL(t, cfg) + + ctx := testutil.Context(t, testutil.WaitMedium) + client := codersdk.New(accessURL) + body, err := client.Request(ctx, http.MethodGet, "/", nil) + require.NoError(t, err) + require.NoError(t, body.Body.Close()) + + require.Eventually(t, func() bool { + snap := <-snapshot + htmlFirstServedFound := false + for _, item := range snap.TelemetryItems { + if item.Key == string(telemetry.TelemetryItemKeyHTMLFirstServedAt) { + htmlFirstServedFound = true + } + } + return htmlFirstServedFound + }, testutil.WaitMedium, testutil.IntervalFast, "no html_first_served telemetry item") }) t.Run("Prometheus", func(t *testing.T) { t.Parallel() @@ -1990,3 +1995,148 @@ func TestServer_DisabledDERP(t *testing.T) { err = c.Connect(ctx) require.Error(t, err) } + +type runServerOpts struct { + waitForSnapshot bool + telemetryDisabled bool + waitForTelemetryDisabledCheck bool +} + +func TestServer_TelemetryDisabled_FinalReport(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } + + telemetryServerURL, deployment, snapshot := mockTelemetryServer(t) + dbConnURL, err := dbtestutil.Open(t) + require.NoError(t, err) + + cacheDir := t.TempDir() + runServer := func(t *testing.T, opts runServerOpts) (chan error, context.CancelFunc) { + ctx, cancelFunc := context.WithCancel(context.Background()) + inv, _ := clitest.New(t, + "server", + "--postgres-url", dbConnURL, + "--http-address", ":0", + "--access-url", "http://example.com", + "--telemetry="+strconv.FormatBool(!opts.telemetryDisabled), + "--telemetry-url", telemetryServerURL.String(), + "--cache-dir", cacheDir, + "--log-filter", ".*", + ) + finished := make(chan bool, 2) + errChan := make(chan error, 1) + pty := ptytest.New(t).Attach(inv) + go func() { + errChan <- inv.WithContext(ctx).Run() + finished <- true + }() + go func() { + defer func() { + finished <- true + }() + if opts.waitForSnapshot { + pty.ExpectMatchContext(testutil.Context(t, testutil.WaitLong), "submitted snapshot") + } + if opts.waitForTelemetryDisabledCheck { + pty.ExpectMatchContext(testutil.Context(t, testutil.WaitLong), "finished telemetry status check") + } + }() + <-finished + return errChan, cancelFunc + } + waitForShutdown := func(t *testing.T, errChan chan error) error { + t.Helper() + select { + case err := <-errChan: + return err + case <-time.After(testutil.WaitMedium): + t.Fatalf("timed out waiting for server to shutdown") + } + return nil + } + + errChan, cancelFunc := runServer(t, runServerOpts{telemetryDisabled: true, waitForTelemetryDisabledCheck: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + + // Since telemetry was disabled, we expect no deployments or snapshots. + require.Empty(t, deployment) + require.Empty(t, snapshot) + + errChan, cancelFunc = runServer(t, runServerOpts{waitForSnapshot: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + // we expect to see a deployment and a snapshot twice: + // 1. the first pair is sent when the server starts + // 2. the second pair is sent when the server shuts down + for i := 0; i < 2; i++ { + select { + case <-snapshot: + case <-time.After(testutil.WaitShort / 2): + t.Fatalf("timed out waiting for snapshot") + } + select { + case <-deployment: + case <-time.After(testutil.WaitShort / 2): + t.Fatalf("timed out waiting for deployment") + } + } + + errChan, cancelFunc = runServer(t, runServerOpts{telemetryDisabled: true, waitForTelemetryDisabledCheck: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + + // Since telemetry is disabled, we expect no deployment. We expect a snapshot + // with the telemetry disabled item. + require.Empty(t, deployment) + select { + case ss := <-snapshot: + require.Len(t, ss.TelemetryItems, 1) + require.Equal(t, string(telemetry.TelemetryItemKeyTelemetryEnabled), ss.TelemetryItems[0].Key) + require.Equal(t, "false", ss.TelemetryItems[0].Value) + case <-time.After(testutil.WaitShort / 2): + t.Fatalf("timed out waiting for snapshot") + } + + errChan, cancelFunc = runServer(t, runServerOpts{telemetryDisabled: true, waitForTelemetryDisabledCheck: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + // Since telemetry is disabled and we've already sent a snapshot, we expect no + // new deployments or snapshots. + require.Empty(t, deployment) + require.Empty(t, snapshot) +} + +func mockTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, chan *telemetry.Snapshot) { + t.Helper() + deployment := make(chan *telemetry.Deployment, 64) + snapshot := make(chan *telemetry.Snapshot, 64) + r := chi.NewRouter() + r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) + dd := &telemetry.Deployment{} + err := json.NewDecoder(r.Body).Decode(dd) + require.NoError(t, err) + deployment <- dd + // Ensure the header is sent only after deployment is sent + w.WriteHeader(http.StatusAccepted) + }) + r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) + ss := &telemetry.Snapshot{} + err := json.NewDecoder(r.Body).Decode(ss) + require.NoError(t, err) + snapshot <- ss + // Ensure the header is sent only after snapshot is sent + w.WriteHeader(http.StatusAccepted) + }) + server := httptest.NewServer(r) + t.Cleanup(server.Close) + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + return serverURL, deployment, snapshot +} diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f16653c1c834b..329951003007b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3438,6 +3438,100 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/settings/idpsync/groups/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update group IdP Sync config", + "operationId": "update-group-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/groups/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update group IdP Sync mapping", + "operationId": "update-group-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, "/organizations/{organization}/settings/idpsync/roles": { "get": { "security": [ @@ -3518,6 +3612,100 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/settings/idpsync/roles/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update role IdP Sync config", + "operationId": "update-role-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/roles/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update role IdP Sync mapping", + "operationId": "update-role-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, "/organizations/{organization}/templates": { "get": { "security": [ @@ -4248,6 +4436,84 @@ const docTemplate = `{ } } }, + "/settings/idpsync/organization/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update organization IdP Sync config", + "operationId": "update-organization-idp-sync-config", + "parameters": [ + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, + "/settings/idpsync/organization/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Update organization IdP Sync mapping", + "operationId": "update-organization-idp-sync-mapping", + "parameters": [ + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, "/tailnet": { "get": { "security": [ @@ -12391,6 +12657,57 @@ const docTemplate = `{ } } }, + "codersdk.PatchGroupIDPSyncConfigRequest": { + "type": "object", + "properties": { + "auto_create_missing_groups": { + "type": "boolean" + }, + "field": { + "type": "string" + }, + "regex_filter": { + "$ref": "#/definitions/regexp.Regexp" + } + } + }, + "codersdk.PatchGroupIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchGroupRequest": { "type": "object", "properties": { @@ -12420,6 +12737,99 @@ const docTemplate = `{ } } }, + "codersdk.PatchOrganizationIDPSyncConfigRequest": { + "type": "object", + "properties": { + "assign_default": { + "type": "boolean" + }, + "field": { + "type": "string" + } + } + }, + "codersdk.PatchOrganizationIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, + "codersdk.PatchRoleIDPSyncConfigRequest": { + "type": "object", + "properties": { + "field": { + "type": "string" + } + } + }, + "codersdk.PatchRoleIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7859d7ffdc5e5..63b7146365d9f 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3030,6 +3030,88 @@ } } }, + "/organizations/{organization}/settings/idpsync/groups/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update group IdP Sync config", + "operationId": "update-group-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/groups/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update group IdP Sync mapping", + "operationId": "update-group-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchGroupIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.GroupSyncSettings" + } + } + } + } + }, "/organizations/{organization}/settings/idpsync/roles": { "get": { "security": [ @@ -3100,6 +3182,88 @@ } } }, + "/organizations/{organization}/settings/idpsync/roles/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update role IdP Sync config", + "operationId": "update-role-idp-sync-config", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, + "/organizations/{organization}/settings/idpsync/roles/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update role IdP Sync mapping", + "operationId": "update-role-idp-sync-mapping", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchRoleIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.RoleSyncSettings" + } + } + } + } + }, "/organizations/{organization}/templates": { "get": { "security": [ @@ -3744,6 +3908,72 @@ } } }, + "/settings/idpsync/organization/config": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update organization IdP Sync config", + "operationId": "update-organization-idp-sync-config", + "parameters": [ + { + "description": "New config values", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, + "/settings/idpsync/organization/mapping": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Update organization IdP Sync mapping", + "operationId": "update-organization-idp-sync-mapping", + "parameters": [ + { + "description": "Description of the mappings to add and remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationIDPSyncMappingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OrganizationSyncSettings" + } + } + } + } + }, "/tailnet": { "get": { "security": [ @@ -11172,6 +11402,57 @@ } } }, + "codersdk.PatchGroupIDPSyncConfigRequest": { + "type": "object", + "properties": { + "auto_create_missing_groups": { + "type": "boolean" + }, + "field": { + "type": "string" + }, + "regex_filter": { + "$ref": "#/definitions/regexp.Regexp" + } + } + }, + "codersdk.PatchGroupIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchGroupRequest": { "type": "object", "properties": { @@ -11201,6 +11482,99 @@ } } }, + "codersdk.PatchOrganizationIDPSyncConfigRequest": { + "type": "object", + "properties": { + "assign_default": { + "type": "boolean" + }, + "field": { + "type": "string" + } + } + }, + "codersdk.PatchOrganizationIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, + "codersdk.PatchRoleIDPSyncConfigRequest": { + "type": "object", + "properties": { + "field": { + "type": "string" + } + } + }, + "codersdk.PatchRoleIDPSyncMappingRequest": { + "type": "object", + "properties": { + "add": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + }, + "remove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "gets": { + "description": "The ID of the Coder resource the user should be added to", + "type": "string" + }, + "given": { + "description": "The IdP claim the user has", + "type": "string" + } + } + } + } + } + }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index e273b7afdb80f..be558797389b9 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -585,6 +585,8 @@ func New(options *Options) *API { AppearanceFetcher: &api.AppearanceFetcher, BuildInfo: buildInfo, Entitlements: options.Entitlements, + Telemetry: options.Telemetry, + Logger: options.Logger.Named("site"), }) api.SiteHandler.Experiments.Store(&experiments) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 2e12cab9d33e0..0ba9e20216b41 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2096,6 +2096,20 @@ func (q *querier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) return q.db.GetTailnetTunnelPeerIDs(ctx, srcID) } +func (q *querier) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return database.TelemetryItem{}, err + } + return q.db.GetTelemetryItem(ctx, key) +} + +func (q *querier) GetTelemetryItems(ctx context.Context) ([]database.TelemetryItem, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.GetTelemetryItems(ctx) +} + func (q *querier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { return nil, err @@ -3085,6 +3099,13 @@ func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaP return q.db.InsertReplica(ctx, arg) } +func (q *querier) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.InsertTelemetryItemIfNotExists(ctx, arg) +} + func (q *querier) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) error { obj := rbac.ResourceTemplate.InOrg(arg.OrganizationID) if err := q.authorizeContext(ctx, policy.ActionCreate, obj); err != nil { @@ -4345,6 +4366,13 @@ func (q *querier) UpsertTailnetTunnel(ctx context.Context, arg database.UpsertTa return q.db.UpsertTailnetTunnel(ctx, arg) } +func (q *querier) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.UpsertTelemetryItem(ctx, arg) +} + func (q *querier) UpsertTemplateUsageStats(ctx context.Context) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index fdbbcc8b34ca6..9e784fff0bf12 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4224,6 +4224,24 @@ func (s *MethodTestSuite) TestSystemFunctions() { s.Run("GetWorkspaceModulesCreatedAfter", s.Subtest(func(db database.Store, check *expects) { check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead) })) + s.Run("GetTelemetryItem", s.Subtest(func(db database.Store, check *expects) { + check.Args("test").Asserts(rbac.ResourceSystem, policy.ActionRead).Errors(sql.ErrNoRows) + })) + s.Run("GetTelemetryItems", s.Subtest(func(db database.Store, check *expects) { + check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("InsertTelemetryItemIfNotExists", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.InsertTelemetryItemIfNotExistsParams{ + Key: "test", + Value: "value", + }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + })) + s.Run("UpsertTelemetryItem", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.UpsertTelemetryItemParams{ + Key: "test", + Value: "value", + }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + })) } func (s *MethodTestSuite) TestNotifications() { diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 566540dcb2906..54e4f99959b44 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1093,6 +1093,23 @@ func ProvisionerJobTimings(t testing.TB, db database.Store, build database.Works return timings } +func TelemetryItem(t testing.TB, db database.Store, seed database.TelemetryItem) database.TelemetryItem { + if seed.Key == "" { + seed.Key = testutil.GetRandomName(t) + } + if seed.Value == "" { + seed.Value = time.Now().Format(time.RFC3339) + } + err := db.UpsertTelemetryItem(genCtx, database.UpsertTelemetryItemParams{ + Key: seed.Key, + Value: seed.Value, + }) + require.NoError(t, err, "upsert telemetry item") + item, err := db.GetTelemetryItem(genCtx, seed.Key) + require.NoError(t, err, "get telemetry item") + return item +} + func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming { timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{ JobID: takeFirst(seed.JobID, uuid.New()), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 6b518c7696369..103ee1e717149 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -89,6 +89,7 @@ func New() database.Store { locks: map[int64]struct{}{}, runtimeConfig: map[string]string{}, userStatusChanges: make([]database.UserStatusChange, 0), + telemetryItems: make([]database.TelemetryItem, 0), }, } // Always start with a default org. Matching migration 198. @@ -258,6 +259,7 @@ type data struct { defaultProxyDisplayName string defaultProxyIconURL string userStatusChanges []database.UserStatusChange + telemetryItems []database.TelemetryItem } func tryPercentile(fs []float64, p float64) float64 { @@ -4330,6 +4332,23 @@ func (*FakeQuerier) GetTailnetTunnelPeerIDs(context.Context, uuid.UUID) ([]datab return nil, ErrUnimplemented } +func (q *FakeQuerier) GetTelemetryItem(_ context.Context, key string) (database.TelemetryItem, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, item := range q.telemetryItems { + if item.Key == key { + return item, nil + } + } + + return database.TelemetryItem{}, sql.ErrNoRows +} + +func (q *FakeQuerier) GetTelemetryItems(_ context.Context) ([]database.TelemetryItem, error) { + return q.telemetryItems, nil +} + func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { err := validateDatabaseType(arg) if err != nil { @@ -8120,6 +8139,30 @@ func (q *FakeQuerier) InsertReplica(_ context.Context, arg database.InsertReplic return replica, nil } +func (q *FakeQuerier) InsertTelemetryItemIfNotExists(_ context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, item := range q.telemetryItems { + if item.Key == arg.Key { + return nil + } + } + + q.telemetryItems = append(q.telemetryItems, database.TelemetryItem{ + Key: arg.Key, + Value: arg.Value, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + return nil +} + func (q *FakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTemplateParams) error { if err := validateDatabaseType(arg); err != nil { return err @@ -10874,6 +10917,33 @@ func (*FakeQuerier) UpsertTailnetTunnel(_ context.Context, arg database.UpsertTa return database.TailnetTunnel{}, ErrUnimplemented } +func (q *FakeQuerier) UpsertTelemetryItem(_ context.Context, arg database.UpsertTelemetryItemParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, item := range q.telemetryItems { + if item.Key == arg.Key { + q.telemetryItems[i].Value = arg.Value + q.telemetryItems[i].UpdatedAt = time.Now() + return nil + } + } + + q.telemetryItems = append(q.telemetryItems, database.TelemetryItem{ + Key: arg.Key, + Value: arg.Value, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + + return nil +} + func (q *FakeQuerier) UpsertTemplateUsageStats(ctx context.Context) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index ba8a1f9cdc8a6..c0d3ed4994f9c 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1134,6 +1134,20 @@ func (m queryMetricsStore) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uu return r0, r1 } +func (m queryMetricsStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { + start := time.Now() + r0, r1 := m.s.GetTelemetryItem(ctx, key) + m.queryLatencies.WithLabelValues("GetTelemetryItem").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) GetTelemetryItems(ctx context.Context) ([]database.TelemetryItem, error) { + start := time.Now() + r0, r1 := m.s.GetTelemetryItems(ctx) + m.queryLatencies.WithLabelValues("GetTelemetryItems").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { start := time.Now() r0, r1 := m.s.GetTemplateAppInsights(ctx, arg) @@ -1911,6 +1925,13 @@ func (m queryMetricsStore) InsertReplica(ctx context.Context, arg database.Inser return replica, err } +func (m queryMetricsStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { + start := time.Now() + r0 := m.s.InsertTelemetryItemIfNotExists(ctx, arg) + m.queryLatencies.WithLabelValues("InsertTelemetryItemIfNotExists").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) error { start := time.Now() err := m.s.InsertTemplate(ctx, arg) @@ -2772,6 +2793,13 @@ func (m queryMetricsStore) UpsertTailnetTunnel(ctx context.Context, arg database return r0, r1 } +func (m queryMetricsStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { + start := time.Now() + r0 := m.s.UpsertTelemetryItem(ctx, arg) + m.queryLatencies.WithLabelValues("UpsertTelemetryItem").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpsertTemplateUsageStats(ctx context.Context) error { start := time.Now() r0 := m.s.UpsertTemplateUsageStats(ctx) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index d2aa8aa6fa62e..e32834a441e6d 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2346,6 +2346,36 @@ func (mr *MockStoreMockRecorder) GetTailnetTunnelPeerIDs(ctx, srcID any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetTunnelPeerIDs", reflect.TypeOf((*MockStore)(nil).GetTailnetTunnelPeerIDs), ctx, srcID) } +// GetTelemetryItem mocks base method. +func (m *MockStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTelemetryItem", ctx, key) + ret0, _ := ret[0].(database.TelemetryItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTelemetryItem indicates an expected call of GetTelemetryItem. +func (mr *MockStoreMockRecorder) GetTelemetryItem(ctx, key any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTelemetryItem", reflect.TypeOf((*MockStore)(nil).GetTelemetryItem), ctx, key) +} + +// GetTelemetryItems mocks base method. +func (m *MockStore) GetTelemetryItems(ctx context.Context) ([]database.TelemetryItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTelemetryItems", ctx) + ret0, _ := ret[0].([]database.TelemetryItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTelemetryItems indicates an expected call of GetTelemetryItems. +func (mr *MockStoreMockRecorder) GetTelemetryItems(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTelemetryItems", reflect.TypeOf((*MockStore)(nil).GetTelemetryItems), ctx) +} + // GetTemplateAppInsights mocks base method. func (m *MockStore) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { m.ctrl.T.Helper() @@ -4051,6 +4081,20 @@ func (mr *MockStoreMockRecorder) InsertReplica(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertReplica", reflect.TypeOf((*MockStore)(nil).InsertReplica), ctx, arg) } +// InsertTelemetryItemIfNotExists mocks base method. +func (m *MockStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertTelemetryItemIfNotExists", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertTelemetryItemIfNotExists indicates an expected call of InsertTelemetryItemIfNotExists. +func (mr *MockStoreMockRecorder) InsertTelemetryItemIfNotExists(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTelemetryItemIfNotExists", reflect.TypeOf((*MockStore)(nil).InsertTelemetryItemIfNotExists), ctx, arg) +} + // InsertTemplate mocks base method. func (m *MockStore) InsertTemplate(ctx context.Context, arg database.InsertTemplateParams) error { m.ctrl.T.Helper() @@ -5861,6 +5905,20 @@ func (mr *MockStoreMockRecorder) UpsertTailnetTunnel(ctx, arg any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetTunnel", reflect.TypeOf((*MockStore)(nil).UpsertTailnetTunnel), ctx, arg) } +// UpsertTelemetryItem mocks base method. +func (m *MockStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertTelemetryItem", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertTelemetryItem indicates an expected call of UpsertTelemetryItem. +func (mr *MockStoreMockRecorder) UpsertTelemetryItem(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTelemetryItem", reflect.TypeOf((*MockStore)(nil).UpsertTelemetryItem), ctx, arg) +} + // UpsertTemplateUsageStats mocks base method. func (m *MockStore) UpsertTemplateUsageStats(ctx context.Context) error { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index c241548e166c2..9cc38adf23b6b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1164,6 +1164,13 @@ CREATE TABLE tailnet_tunnels ( updated_at timestamp with time zone NOT NULL ); +CREATE TABLE telemetry_items ( + key text NOT NULL, + value text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + CREATE TABLE template_usage_stats ( start_time timestamp with time zone NOT NULL, end_time timestamp with time zone NOT NULL, @@ -2026,6 +2033,9 @@ ALTER TABLE ONLY tailnet_peers ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); +ALTER TABLE ONLY telemetry_items + ADD CONSTRAINT telemetry_items_pkey PRIMARY KEY (key); + ALTER TABLE ONLY template_usage_stats ADD CONSTRAINT template_usage_stats_pkey PRIMARY KEY (start_time, template_id, user_id); diff --git a/coderd/database/migrations/000288_telemetry_items.down.sql b/coderd/database/migrations/000288_telemetry_items.down.sql new file mode 100644 index 0000000000000..118188f519e76 --- /dev/null +++ b/coderd/database/migrations/000288_telemetry_items.down.sql @@ -0,0 +1 @@ +DROP TABLE telemetry_items; diff --git a/coderd/database/migrations/000288_telemetry_items.up.sql b/coderd/database/migrations/000288_telemetry_items.up.sql new file mode 100644 index 0000000000000..40279827788d6 --- /dev/null +++ b/coderd/database/migrations/000288_telemetry_items.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE telemetry_items ( + key TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); diff --git a/coderd/database/migrations/testdata/fixtures/000288_telemetry_items.up.sql b/coderd/database/migrations/testdata/fixtures/000288_telemetry_items.up.sql new file mode 100644 index 0000000000000..0189558292915 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000288_telemetry_items.up.sql @@ -0,0 +1,4 @@ +INSERT INTO + telemetry_items (key, value) +VALUES + ('example_key', 'example_value'); diff --git a/coderd/database/models.go b/coderd/database/models.go index b0a487c192793..9769bde33052b 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2787,6 +2787,13 @@ type TailnetTunnel struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } +type TelemetryItem struct { + Key string `db:"key" json:"key"` + Value string `db:"value" json:"value"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + // Joins in the display name information such as username, avatar, and organization name. type Template struct { ID uuid.UUID `db:"id" json:"id"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 132a7aea75bdd..1fa83208a2218 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -224,6 +224,8 @@ type sqlcQuerier interface { GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerBindingsRow, error) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerIDsRow, error) + GetTelemetryItem(ctx context.Context, key string) (TelemetryItem, error) + GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error) // GetTemplateAppInsights returns the aggregate usage of each app in a given // timeframe. The result can be filtered on template_ids, meaning only user data // from workspaces based on those templates will be included. @@ -404,6 +406,7 @@ type sqlcQuerier interface { InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error) InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) + InsertTelemetryItemIfNotExists(ctx context.Context, arg InsertTelemetryItemIfNotExistsParams) error InsertTemplate(ctx context.Context, arg InsertTemplateParams) error InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) @@ -546,6 +549,7 @@ type sqlcQuerier interface { UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) + UpsertTelemetryItem(ctx context.Context, arg UpsertTelemetryItemParams) error // This query aggregates the workspace_agent_stats and workspace_app_stats data // into a single table for efficient storage and querying. Half-hour buckets are // used to store the data, and the minutes are summed for each user and template diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 38dbf1fbfd0bb..86db8fb66956a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8702,6 +8702,86 @@ func (q *sqlQuerier) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetT return i, err } +const getTelemetryItem = `-- name: GetTelemetryItem :one +SELECT key, value, created_at, updated_at FROM telemetry_items WHERE key = $1 +` + +func (q *sqlQuerier) GetTelemetryItem(ctx context.Context, key string) (TelemetryItem, error) { + row := q.db.QueryRowContext(ctx, getTelemetryItem, key) + var i TelemetryItem + err := row.Scan( + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getTelemetryItems = `-- name: GetTelemetryItems :many +SELECT key, value, created_at, updated_at FROM telemetry_items +` + +func (q *sqlQuerier) GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error) { + rows, err := q.db.QueryContext(ctx, getTelemetryItems) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TelemetryItem + for rows.Next() { + var i TelemetryItem + if err := rows.Scan( + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertTelemetryItemIfNotExists = `-- name: InsertTelemetryItemIfNotExists :exec +INSERT INTO telemetry_items (key, value) +VALUES ($1, $2) +ON CONFLICT (key) DO NOTHING +` + +type InsertTelemetryItemIfNotExistsParams struct { + Key string `db:"key" json:"key"` + Value string `db:"value" json:"value"` +} + +func (q *sqlQuerier) InsertTelemetryItemIfNotExists(ctx context.Context, arg InsertTelemetryItemIfNotExistsParams) error { + _, err := q.db.ExecContext(ctx, insertTelemetryItemIfNotExists, arg.Key, arg.Value) + return err +} + +const upsertTelemetryItem = `-- name: UpsertTelemetryItem :exec +INSERT INTO telemetry_items (key, value) +VALUES ($1, $2) +ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW() WHERE telemetry_items.key = $1 +` + +type UpsertTelemetryItemParams struct { + Key string `db:"key" json:"key"` + Value string `db:"value" json:"value"` +} + +func (q *sqlQuerier) UpsertTelemetryItem(ctx context.Context, arg UpsertTelemetryItemParams) error { + _, err := q.db.ExecContext(ctx, upsertTelemetryItem, arg.Key, arg.Value) + return err +} + const getTemplateAverageBuildTime = `-- name: GetTemplateAverageBuildTime :one WITH build_times AS ( SELECT diff --git a/coderd/database/queries/telemetryitems.sql b/coderd/database/queries/telemetryitems.sql new file mode 100644 index 0000000000000..7b7349db59943 --- /dev/null +++ b/coderd/database/queries/telemetryitems.sql @@ -0,0 +1,15 @@ +-- name: InsertTelemetryItemIfNotExists :exec +INSERT INTO telemetry_items (key, value) +VALUES ($1, $2) +ON CONFLICT (key) DO NOTHING; + +-- name: GetTelemetryItem :one +SELECT * FROM telemetry_items WHERE key = $1; + +-- name: UpsertTelemetryItem :exec +INSERT INTO telemetry_items (key, value) +VALUES ($1, $2) +ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW() WHERE telemetry_items.key = $1; + +-- name: GetTelemetryItems :many +SELECT * FROM telemetry_items; diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index f253aa98ec266..2e4b813e438b8 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -55,6 +55,7 @@ const ( UniqueTailnetCoordinatorsPkey UniqueConstraint = "tailnet_coordinators_pkey" // ALTER TABLE ONLY tailnet_coordinators ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id); UniqueTailnetPeersPkey UniqueConstraint = "tailnet_peers_pkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetTunnelsPkey UniqueConstraint = "tailnet_tunnels_pkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); + UniqueTelemetryItemsPkey UniqueConstraint = "telemetry_items_pkey" // ALTER TABLE ONLY telemetry_items ADD CONSTRAINT telemetry_items_pkey PRIMARY KEY (key); UniqueTemplateUsageStatsPkey UniqueConstraint = "template_usage_stats_pkey" // ALTER TABLE ONLY template_usage_stats ADD CONSTRAINT template_usage_stats_pkey PRIMARY KEY (start_time, template_id, user_id); UniqueTemplateVersionParametersTemplateVersionIDNameKey UniqueConstraint = "template_version_parameters_template_version_id_name_key" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name); UniqueTemplateVersionVariablesTemplateVersionIDNameKey UniqueConstraint = "template_version_variables_template_version_id_name_key" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_name_key UNIQUE (template_version_id, name); diff --git a/coderd/idpsync/group.go b/coderd/idpsync/group.go index c14b7655e7e20..4524284260359 100644 --- a/coderd/idpsync/group.go +++ b/coderd/idpsync/group.go @@ -30,7 +30,7 @@ func (AGPLIDPSync) GroupSyncEntitled() bool { return false } -func (s AGPLIDPSync) UpdateGroupSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error { +func (s AGPLIDPSync) UpdateGroupSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error { orgResolver := s.Manager.OrganizationResolver(db, orgID) err := s.SyncSettings.Group.SetRuntimeValue(ctx, orgResolver, &settings) if err != nil { diff --git a/coderd/idpsync/idpsync.go b/coderd/idpsync/idpsync.go index e936bada73752..4da101635bd23 100644 --- a/coderd/idpsync/idpsync.go +++ b/coderd/idpsync/idpsync.go @@ -26,7 +26,7 @@ import ( type IDPSync interface { OrganizationSyncEntitled() bool OrganizationSyncSettings(ctx context.Context, db database.Store) (*OrganizationSyncSettings, error) - UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error + UpdateOrganizationSyncSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error // OrganizationSyncEnabled returns true if all OIDC users are assigned // to organizations via org sync settings. // This is used to know when to disable manual org membership assignment. @@ -48,7 +48,7 @@ type IDPSync interface { // on the settings used by IDPSync. This entry is thread safe and can be // accessed concurrently. The settings are stored in the database. GroupSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*GroupSyncSettings, error) - UpdateGroupSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error + UpdateGroupSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings GroupSyncSettings) error // RoleSyncEntitled returns true if the deployment is entitled to role syncing. RoleSyncEntitled() bool @@ -61,7 +61,7 @@ type IDPSync interface { // RoleSyncSettings is similar to GroupSyncSettings. See GroupSyncSettings for // rational. RoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store) (*RoleSyncSettings, error) - UpdateRoleSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error + UpdateRoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error // ParseRoleClaims takes claims from an OIDC provider, and returns the params // for role syncing. Most of the logic happens in SyncRoles. ParseRoleClaims(ctx context.Context, mergedClaims jwt.MapClaims) (RoleParams, *HTTPError) @@ -70,6 +70,9 @@ type IDPSync interface { SyncRoles(ctx context.Context, db database.Store, user database.User, params RoleParams) error } +// AGPLIDPSync implements the IDPSync interface +var _ IDPSync = AGPLIDPSync{} + // AGPLIDPSync is the configuration for syncing user information from an external // IDP. All related code to syncing user information should be in this package. type AGPLIDPSync struct { diff --git a/coderd/idpsync/organization.go b/coderd/idpsync/organization.go index 12d79bc047776..6f755529cdde7 100644 --- a/coderd/idpsync/organization.go +++ b/coderd/idpsync/organization.go @@ -34,7 +34,7 @@ func (AGPLIDPSync) OrganizationSyncEnabled(_ context.Context, _ database.Store) return false } -func (s AGPLIDPSync) UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error { +func (s AGPLIDPSync) UpdateOrganizationSyncSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error { rlv := s.Manager.Resolver(db) err := s.SyncSettings.Organization.SetRuntimeValue(ctx, rlv, &settings) if err != nil { @@ -45,6 +45,8 @@ func (s AGPLIDPSync) UpdateOrganizationSettings(ctx context.Context, db database } func (s AGPLIDPSync) OrganizationSyncSettings(ctx context.Context, db database.Store) (*OrganizationSyncSettings, error) { + // If this logic is ever updated, make sure to update the corresponding + // checkIDPOrgSync in coderd/telemetry/telemetry.go. rlv := s.Manager.Resolver(db) orgSettings, err := s.SyncSettings.Organization.Resolve(ctx, rlv) if err != nil { diff --git a/coderd/idpsync/role.go b/coderd/idpsync/role.go index cf768ee0eb05d..5cb0ac172581c 100644 --- a/coderd/idpsync/role.go +++ b/coderd/idpsync/role.go @@ -42,7 +42,7 @@ func (AGPLIDPSync) SiteRoleSyncEnabled() bool { return false } -func (s AGPLIDPSync) UpdateRoleSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error { +func (s AGPLIDPSync) UpdateRoleSyncSettings(ctx context.Context, orgID uuid.UUID, db database.Store, settings RoleSyncSettings) error { orgResolver := s.Manager.OrganizationResolver(db, orgID) err := s.SyncSettings.Role.SetRuntimeValue(ctx, orgResolver, &settings) if err != nil { diff --git a/coderd/runtimeconfig/resolver.go b/coderd/runtimeconfig/resolver.go index d899680f034a4..5d06a156bfb41 100644 --- a/coderd/runtimeconfig/resolver.go +++ b/coderd/runtimeconfig/resolver.go @@ -12,6 +12,9 @@ import ( "github.com/coder/coder/v2/coderd/database" ) +// NoopResolver implements the Resolver interface +var _ Resolver = &NoopResolver{} + // NoopResolver is a useful test device. type NoopResolver struct{} @@ -31,6 +34,9 @@ func (NoopResolver) DeleteRuntimeConfig(context.Context, string) error { return ErrEntryNotFound } +// StoreResolver implements the Resolver interface +var _ Resolver = &StoreResolver{} + // StoreResolver uses the database as the underlying store for runtime settings. type StoreResolver struct { db Store diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 233450c43d943..78819b0c65462 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/sha256" + "database/sql" "encoding/json" "errors" "fmt" @@ -14,6 +15,7 @@ import ( "regexp" "runtime" "slices" + "strconv" "strings" "sync" "time" @@ -41,6 +43,7 @@ const ( ) type Options struct { + Disabled bool Database database.Store Logger slog.Logger // URL is an endpoint to direct telemetry towards! @@ -115,8 +118,8 @@ type remoteReporter struct { shutdownAt *time.Time } -func (*remoteReporter) Enabled() bool { - return true +func (r *remoteReporter) Enabled() bool { + return !r.options.Disabled } func (r *remoteReporter) Report(snapshot *Snapshot) { @@ -160,10 +163,12 @@ func (r *remoteReporter) Close() { close(r.closed) now := dbtime.Now() r.shutdownAt = &now - // Report a final collection of telemetry prior to close! - // This could indicate final actions a user has taken, and - // the time the deployment was shutdown. - r.reportWithDeployment() + if r.Enabled() { + // Report a final collection of telemetry prior to close! + // This could indicate final actions a user has taken, and + // the time the deployment was shutdown. + r.reportWithDeployment() + } r.closeFunc() } @@ -176,7 +181,74 @@ func (r *remoteReporter) isClosed() bool { } } +// See the corresponding test in telemetry_test.go for a truth table. +func ShouldReportTelemetryDisabled(recordedTelemetryEnabled *bool, telemetryEnabled bool) bool { + return recordedTelemetryEnabled != nil && *recordedTelemetryEnabled && !telemetryEnabled +} + +// RecordTelemetryStatus records the telemetry status in the database. +// If the status changed from enabled to disabled, returns a snapshot to +// be sent to the telemetry server. +func RecordTelemetryStatus( //nolint:revive + ctx context.Context, + logger slog.Logger, + db database.Store, + telemetryEnabled bool, +) (*Snapshot, error) { + item, err := db.GetTelemetryItem(ctx, string(TelemetryItemKeyTelemetryEnabled)) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get telemetry enabled: %w", err) + } + var recordedTelemetryEnabled *bool + if !errors.Is(err, sql.ErrNoRows) { + value, err := strconv.ParseBool(item.Value) + if err != nil { + logger.Debug(ctx, "parse telemetry enabled", slog.Error(err)) + } + // If ParseBool fails, value will default to false. + // This may happen if an admin manually edits the telemetry item + // in the database. + recordedTelemetryEnabled = &value + } + + if err := db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ + Key: string(TelemetryItemKeyTelemetryEnabled), + Value: strconv.FormatBool(telemetryEnabled), + }); err != nil { + return nil, xerrors.Errorf("upsert telemetry enabled: %w", err) + } + + shouldReport := ShouldReportTelemetryDisabled(recordedTelemetryEnabled, telemetryEnabled) + if !shouldReport { + return nil, nil //nolint:nilnil + } + // If any of the following calls fail, we will never report that telemetry changed + // from enabled to disabled. This is okay. We only want to ping the telemetry server + // once, and never again. If that attempt fails, so be it. + item, err = db.GetTelemetryItem(ctx, string(TelemetryItemKeyTelemetryEnabled)) + if err != nil { + return nil, xerrors.Errorf("get telemetry enabled after upsert: %w", err) + } + return &Snapshot{ + TelemetryItems: []TelemetryItem{ + ConvertTelemetryItem(item), + }, + }, nil +} + func (r *remoteReporter) runSnapshotter() { + telemetryDisabledSnapshot, err := RecordTelemetryStatus(r.ctx, r.options.Logger, r.options.Database, r.Enabled()) + if err != nil { + r.options.Logger.Debug(r.ctx, "record and maybe report telemetry status", slog.Error(err)) + } + if telemetryDisabledSnapshot != nil { + r.reportSync(telemetryDisabledSnapshot) + } + r.options.Logger.Debug(r.ctx, "finished telemetry status check") + if !r.Enabled() { + return + } + first := true ticker := time.NewTicker(r.options.SnapshotFrequency) defer ticker.Stop() @@ -244,6 +316,11 @@ func (r *remoteReporter) deployment() error { return xerrors.Errorf("install source must be <=64 chars: %s", installSource) } + idpOrgSync, err := checkIDPOrgSync(r.ctx, r.options.Database, r.options.DeploymentConfig) + if err != nil { + r.options.Logger.Debug(r.ctx, "check IDP org sync", slog.Error(err)) + } + data, err := json.Marshal(&Deployment{ ID: r.options.DeploymentID, Architecture: sysInfo.Architecture, @@ -263,6 +340,7 @@ func (r *remoteReporter) deployment() error { MachineID: sysInfo.UniqueID, StartedAt: r.startedAt, ShutdownAt: r.shutdownAt, + IDPOrgSync: &idpOrgSync, }) if err != nil { return xerrors.Errorf("marshal deployment: %w", err) @@ -284,6 +362,45 @@ func (r *remoteReporter) deployment() error { return nil } +// idpOrgSyncConfig is a subset of +// https://github.com/coder/coder/blob/5c6578d84e2940b9cfd04798c45e7c8042c3fe0e/coderd/idpsync/organization.go#L148 +type idpOrgSyncConfig struct { + Field string `json:"field"` +} + +// checkIDPOrgSync inspects the server flags and the runtime config. It's based on +// the OrganizationSyncEnabled function from enterprise/coderd/enidpsync/organizations.go. +// It has one distinct difference: it doesn't check if the license entitles to the +// feature, it only checks if the feature is configured. +// +// The above function is not used because it's very hard to make it available in +// the telemetry package due to coder/coder package structure and initialization +// order of the coder server. +// +// We don't check license entitlements because it's also hard to do from the +// telemetry package, and the config check should be sufficient for telemetry purposes. +// +// While this approach duplicates code, it's simpler than the alternative. +// +// See https://github.com/coder/coder/pull/16323 for more details. +func checkIDPOrgSync(ctx context.Context, db database.Store, values *codersdk.DeploymentValues) (bool, error) { + // key based on https://github.com/coder/coder/blob/5c6578d84e2940b9cfd04798c45e7c8042c3fe0e/coderd/idpsync/idpsync.go#L168 + syncConfigRaw, err := db.GetRuntimeConfig(ctx, "organization-sync-settings") + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // If the runtime config is not set, we check if the deployment config + // has the organization field set. + return values != nil && values.OIDC.OrganizationField != "", nil + } + return false, xerrors.Errorf("get runtime config: %w", err) + } + syncConfig := idpOrgSyncConfig{} + if err := json.Unmarshal([]byte(syncConfigRaw), &syncConfig); err != nil { + return false, xerrors.Errorf("unmarshal runtime config: %w", err) + } + return syncConfig.Field != "", nil +} + // createSnapshot collects a full snapshot from the database. func (r *remoteReporter) createSnapshot() (*Snapshot, error) { var ( @@ -518,6 +635,32 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { } return nil }) + eg.Go(func() error { + // Warning: When an organization is deleted, it's completely removed from + // the database. It will no longer be reported, and there will be no other + // indicator that it was deleted. This requires special handling when + // interpreting the telemetry data later. + orgs, err := r.options.Database.GetOrganizations(r.ctx, database.GetOrganizationsParams{}) + if err != nil { + return xerrors.Errorf("get organizations: %w", err) + } + snapshot.Organizations = make([]Organization, 0, len(orgs)) + for _, org := range orgs { + snapshot.Organizations = append(snapshot.Organizations, ConvertOrganization(org)) + } + return nil + }) + eg.Go(func() error { + items, err := r.options.Database.GetTelemetryItems(ctx) + if err != nil { + return xerrors.Errorf("get telemetry items: %w", err) + } + snapshot.TelemetryItems = make([]TelemetryItem, 0, len(items)) + for _, item := range items { + snapshot.TelemetryItems = append(snapshot.TelemetryItems, ConvertTelemetryItem(item)) + } + return nil + }) err := eg.Wait() if err != nil { @@ -916,6 +1059,23 @@ func ConvertExternalProvisioner(id uuid.UUID, tags map[string]string, provisione } } +func ConvertOrganization(org database.Organization) Organization { + return Organization{ + ID: org.ID, + CreatedAt: org.CreatedAt, + IsDefault: org.IsDefault, + } +} + +func ConvertTelemetryItem(item database.TelemetryItem) TelemetryItem { + return TelemetryItem{ + Key: item.Key, + Value: item.Value, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } +} + // 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. @@ -942,6 +1102,8 @@ type Snapshot struct { WorkspaceModules []WorkspaceModule `json:"workspace_modules"` Workspaces []Workspace `json:"workspaces"` NetworkEvents []NetworkEvent `json:"network_events"` + Organizations []Organization `json:"organizations"` + TelemetryItems []TelemetryItem `json:"telemetry_items"` } // Deployment contains information about the host running Coder. @@ -964,6 +1126,9 @@ type Deployment struct { MachineID string `json:"machine_id"` StartedAt time.Time `json:"started_at"` ShutdownAt *time.Time `json:"shutdown_at"` + // While IDPOrgSync will always be set, it's nullable to make + // the struct backwards compatible with older coder versions. + IDPOrgSync *bool `json:"idp_org_sync"` } type APIKey struct { @@ -1457,8 +1622,36 @@ func NetworkEventFromProto(proto *tailnetproto.TelemetryEvent) (NetworkEvent, er }, nil } +type Organization struct { + ID uuid.UUID `json:"id"` + IsDefault bool `json:"is_default"` + CreatedAt time.Time `json:"created_at"` +} + +type telemetryItemKey string + +// The comment below gets rid of the warning that the name "TelemetryItemKey" has +// the "Telemetry" prefix, and that stutters when you use it outside the package +// (telemetry.TelemetryItemKey...). "TelemetryItem" is the name of a database table, +// so it makes sense to use the "Telemetry" prefix. +// +//revive:disable:exported +const ( + TelemetryItemKeyHTMLFirstServedAt telemetryItemKey = "html_first_served_at" + TelemetryItemKeyTelemetryEnabled telemetryItemKey = "telemetry_enabled" +) + +type TelemetryItem struct { + Key string `json:"key"` + Value string `json:"value"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type noopReporter struct{} -func (*noopReporter) Report(_ *Snapshot) {} -func (*noopReporter) Enabled() bool { return false } -func (*noopReporter) Close() {} +func (*noopReporter) Report(_ *Snapshot) {} +func (*noopReporter) Enabled() bool { return false } +func (*noopReporter) Close() {} +func (*noopReporter) RunSnapshotter() {} +func (*noopReporter) ReportDisabledIfNeeded() error { return nil } diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index e0cbfd1cfa193..29fcb644fc88f 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -22,7 +22,10 @@ import ( "github.com/coder/coder/v2/coderd/database/dbmem" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/idpsync" + "github.com/coder/coder/v2/coderd/runtimeconfig" "github.com/coder/coder/v2/coderd/telemetry" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -40,27 +43,42 @@ func TestTelemetry(t *testing.T) { db := dbmem.New() ctx := testutil.Context(t, testutil.WaitMedium) + + org, err := db.GetDefaultOrganization(ctx) + require.NoError(t, err) + _, _ = dbgen.APIKey(t, db, database.APIKey{}) _ = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ - Provisioner: database.ProvisionerTypeTerraform, - StorageMethod: database.ProvisionerStorageMethodFile, - Type: database.ProvisionerJobTypeTemplateVersionDryRun, + Provisioner: database.ProvisionerTypeTerraform, + StorageMethod: database.ProvisionerStorageMethodFile, + Type: database.ProvisionerJobTypeTemplateVersionDryRun, + OrganizationID: org.ID, }) _ = dbgen.Template(t, db, database.Template{ - Provisioner: database.ProvisionerTypeTerraform, + Provisioner: database.ProvisionerTypeTerraform, + OrganizationID: org.ID, }) sourceExampleID := uuid.NewString() _ = dbgen.TemplateVersion(t, db, database.TemplateVersion{ SourceExampleID: sql.NullString{String: sourceExampleID, Valid: true}, + OrganizationID: org.ID, + }) + _ = dbgen.TemplateVersion(t, db, database.TemplateVersion{ + OrganizationID: org.ID, }) - _ = dbgen.TemplateVersion(t, db, database.TemplateVersion{}) user := dbgen.User(t, db, database.User{}) - _ = dbgen.Workspace(t, db, database.WorkspaceTable{}) + _ = dbgen.Workspace(t, db, database.WorkspaceTable{ + OrganizationID: org.ID, + }) _ = dbgen.WorkspaceApp(t, db, database.WorkspaceApp{ SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }) + _ = dbgen.TelemetryItem(t, db, database.TelemetryItem{ + Key: string(telemetry.TelemetryItemKeyHTMLFirstServedAt), + Value: time.Now().Format(time.RFC3339), + }) group := dbgen.Group(t, db, database.Group{}) _ = dbgen.GroupMember(t, db, database.GroupMemberTable{UserID: user.ID, GroupID: group.ID}) wsagent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{}) @@ -112,7 +130,9 @@ func TestTelemetry(t *testing.T) { require.Len(t, snapshot.WorkspaceAgentStats, 1) require.Len(t, snapshot.WorkspaceProxies, 1) require.Len(t, snapshot.WorkspaceModules, 1) - + require.Len(t, snapshot.Organizations, 1) + // We create one item manually above. The other is TelemetryEnabled, created by the snapshotter. + require.Len(t, snapshot.TelemetryItems, 2) wsa := snapshot.WorkspaceAgents[0] require.Len(t, wsa.Subsystems, 2) require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystems[0]) @@ -128,6 +148,19 @@ func TestTelemetry(t *testing.T) { }) require.Equal(t, tvs[0].SourceExampleID, &sourceExampleID) require.Nil(t, tvs[1].SourceExampleID) + + for _, entity := range snapshot.Workspaces { + require.Equal(t, entity.OrganizationID, org.ID) + } + for _, entity := range snapshot.ProvisionerJobs { + require.Equal(t, entity.OrganizationID, org.ID) + } + for _, entity := range snapshot.TemplateVersions { + require.Equal(t, entity.OrganizationID, org.ID) + } + for _, entity := range snapshot.Templates { + require.Equal(t, entity.OrganizationID, org.ID) + } }) t.Run("HashedEmail", func(t *testing.T) { t.Parallel() @@ -243,6 +276,41 @@ func TestTelemetry(t *testing.T) { require.Equal(t, c.want, telemetry.GetModuleSourceType(c.source)) } }) + t.Run("IDPOrgSync", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + db, _ := dbtestutil.NewDB(t) + + // 1. No org sync settings + deployment, _ := collectSnapshot(t, db, nil) + require.False(t, *deployment.IDPOrgSync) + + // 2. Org sync settings set in server flags + deployment, _ = collectSnapshot(t, db, func(opts telemetry.Options) telemetry.Options { + opts.DeploymentConfig = &codersdk.DeploymentValues{ + OIDC: codersdk.OIDCConfig{ + OrganizationField: "organizations", + }, + } + return opts + }) + require.True(t, *deployment.IDPOrgSync) + + // 3. Org sync settings set in runtime config + org, err := db.GetDefaultOrganization(ctx) + require.NoError(t, err) + sync := idpsync.NewAGPLSync(testutil.Logger(t), runtimeconfig.NewManager(), idpsync.DeploymentSyncSettings{}) + err = sync.UpdateOrganizationSyncSettings(ctx, db, idpsync.OrganizationSyncSettings{ + Field: "organizations", + Mapping: map[string][]uuid.UUID{ + "first": {org.ID}, + }, + AssignDefault: true, + }) + require.NoError(t, err) + deployment, _ = collectSnapshot(t, db, nil) + require.True(t, *deployment.IDPOrgSync) + }) } // nolint:paralleltest @@ -253,31 +321,153 @@ func TestTelemetryInstallSource(t *testing.T) { require.Equal(t, "aws_marketplace", deployment.InstallSource) } -func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts telemetry.Options) telemetry.Options) (*telemetry.Deployment, *telemetry.Snapshot) { +func TestTelemetryItem(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + db, _ := dbtestutil.NewDB(t) + key := testutil.GetRandomName(t) + value := time.Now().Format(time.RFC3339) + + err := db.InsertTelemetryItemIfNotExists(ctx, database.InsertTelemetryItemIfNotExistsParams{ + Key: key, + Value: value, + }) + require.NoError(t, err) + + item, err := db.GetTelemetryItem(ctx, key) + require.NoError(t, err) + require.Equal(t, item.Key, key) + require.Equal(t, item.Value, value) + + // Inserting a new value should not update the existing value + err = db.InsertTelemetryItemIfNotExists(ctx, database.InsertTelemetryItemIfNotExistsParams{ + Key: key, + Value: "new_value", + }) + require.NoError(t, err) + + item, err = db.GetTelemetryItem(ctx, key) + require.NoError(t, err) + require.Equal(t, item.Value, value) + + // Upserting a new value should update the existing value + err = db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ + Key: key, + Value: "new_value", + }) + require.NoError(t, err) + + item, err = db.GetTelemetryItem(ctx, key) + require.NoError(t, err) + require.Equal(t, item.Value, "new_value") +} + +func TestShouldReportTelemetryDisabled(t *testing.T) { + t.Parallel() + // Description | telemetryEnabled (db) | telemetryEnabled (is) | Report Telemetry Disabled | + //----------------------------------------|-----------------------|-----------------------|---------------------------| + // New deployment | | true | No | + // New deployment with telemetry disabled | | false | No | + // Telemetry was enabled, and still is | true | true | No | + // Telemetry was enabled but now disabled | true | false | Yes | + // Telemetry was disabled, now is enabled | false | true | No | + // Telemetry was disabled, still disabled | false | false | No | + boolTrue := true + boolFalse := false + require.False(t, telemetry.ShouldReportTelemetryDisabled(nil, true)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(nil, false)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolTrue, true)) + require.True(t, telemetry.ShouldReportTelemetryDisabled(&boolTrue, false)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolFalse, true)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolFalse, false)) +} + +func TestRecordTelemetryStatus(t *testing.T) { + t.Parallel() + for _, testCase := range []struct { + name string + recordedTelemetryEnabled string + telemetryEnabled bool + shouldReport bool + }{ + {name: "New deployment", recordedTelemetryEnabled: "nil", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry disabled", recordedTelemetryEnabled: "nil", telemetryEnabled: false, shouldReport: false}, + {name: "Telemetry was enabled and still is", recordedTelemetryEnabled: "true", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry was enabled but now disabled", recordedTelemetryEnabled: "true", telemetryEnabled: false, shouldReport: true}, + {name: "Telemetry was disabled now is enabled", recordedTelemetryEnabled: "false", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry was disabled still disabled", recordedTelemetryEnabled: "false", telemetryEnabled: false, shouldReport: false}, + {name: "Telemetry was disabled still disabled, invalid value", recordedTelemetryEnabled: "invalid", telemetryEnabled: false, shouldReport: false}, + } { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitMedium) + logger := testutil.Logger(t) + if testCase.recordedTelemetryEnabled != "nil" { + db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ + Key: string(telemetry.TelemetryItemKeyTelemetryEnabled), + Value: testCase.recordedTelemetryEnabled, + }) + } + snapshot1, err := telemetry.RecordTelemetryStatus(ctx, logger, db, testCase.telemetryEnabled) + require.NoError(t, err) + + if testCase.shouldReport { + require.NotNil(t, snapshot1) + require.Equal(t, snapshot1.TelemetryItems[0].Key, string(telemetry.TelemetryItemKeyTelemetryEnabled)) + require.Equal(t, snapshot1.TelemetryItems[0].Value, "false") + } else { + require.Nil(t, snapshot1) + } + + for i := 0; i < 3; i++ { + // Whatever happens, subsequent calls should not report if telemetryEnabled didn't change + snapshot2, err := telemetry.RecordTelemetryStatus(ctx, logger, db, testCase.telemetryEnabled) + require.NoError(t, err) + require.Nil(t, snapshot2) + } + }) + } +} + +func mockTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, chan *telemetry.Snapshot) { t.Helper() deployment := make(chan *telemetry.Deployment, 64) snapshot := make(chan *telemetry.Snapshot, 64) r := chi.NewRouter() r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) - w.WriteHeader(http.StatusAccepted) dd := &telemetry.Deployment{} err := json.NewDecoder(r.Body).Decode(dd) require.NoError(t, err) deployment <- dd + // Ensure the header is sent only after deployment is sent + w.WriteHeader(http.StatusAccepted) }) r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) - w.WriteHeader(http.StatusAccepted) ss := &telemetry.Snapshot{} err := json.NewDecoder(r.Body).Decode(ss) require.NoError(t, err) snapshot <- ss + // Ensure the header is sent only after snapshot is sent + w.WriteHeader(http.StatusAccepted) }) server := httptest.NewServer(r) t.Cleanup(server.Close) serverURL, err := url.Parse(server.URL) require.NoError(t, err) + + return serverURL, deployment, snapshot +} + +func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts telemetry.Options) telemetry.Options) (*telemetry.Deployment, *telemetry.Snapshot) { + t.Helper() + + serverURL, deployment, snapshot := mockTelemetryServer(t) + options := telemetry.Options{ Database: db, Logger: testutil.Logger(t), diff --git a/coderd/users.go b/coderd/users.go index 56f295986859c..964f18724449a 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -918,6 +918,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName string, targetUser database.User, status database.UserStatus) error { var labels map[string]string + var data map[string]any var adminTemplateID, personalTemplateID uuid.UUID switch status { case database.UserStatusSuspended: @@ -926,6 +927,9 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName stri "suspended_account_user_name": targetUser.Name, "initiator": actingUserName, } + data = map[string]any{ + "user": map[string]any{"id": targetUser.ID, "name": targetUser.Name, "email": targetUser.Email}, + } adminTemplateID = notifications.TemplateUserAccountSuspended personalTemplateID = notifications.TemplateYourAccountSuspended case database.UserStatusActive: @@ -934,6 +938,9 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName stri "activated_account_user_name": targetUser.Name, "initiator": actingUserName, } + data = map[string]any{ + "user": map[string]any{"id": targetUser.ID, "name": targetUser.Name, "email": targetUser.Email}, + } adminTemplateID = notifications.TemplateUserAccountActivated personalTemplateID = notifications.TemplateYourAccountActivated default: @@ -949,16 +956,16 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName stri // Send notifications to user admins and affected user for _, u := range userAdmins { // nolint:gocritic // Need notifier actor to enqueue notifications - if _, err := api.NotificationsEnqueuer.Enqueue(dbauthz.AsNotifier(ctx), u.ID, adminTemplateID, - labels, "api-put-user-status", + if _, err := api.NotificationsEnqueuer.EnqueueWithData(dbauthz.AsNotifier(ctx), u.ID, adminTemplateID, + labels, data, "api-put-user-status", targetUser.ID, ); err != nil { api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", targetUser.Username), slog.Error(err)) } } // nolint:gocritic // Need notifier actor to enqueue notifications - if _, err := api.NotificationsEnqueuer.Enqueue(dbauthz.AsNotifier(ctx), targetUser.ID, personalTemplateID, - labels, "api-put-user-status", + if _, err := api.NotificationsEnqueuer.EnqueueWithData(dbauthz.AsNotifier(ctx), targetUser.ID, personalTemplateID, + labels, data, "api-put-user-status", targetUser.ID, ); err != nil { api.Logger.Warn(ctx, "unable to notify user about status change of their account", slog.F("affected_user", targetUser.Username), slog.Error(err)) @@ -1424,13 +1431,20 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create } for _, u := range userAdmins { - // nolint:gocritic // Need notifier actor to enqueue notifications - if _, err := api.NotificationsEnqueuer.Enqueue(dbauthz.AsNotifier(ctx), u.ID, notifications.TemplateUserAccountCreated, + if _, err := api.NotificationsEnqueuer.EnqueueWithData( + // nolint:gocritic // Need notifier actor to enqueue notifications + dbauthz.AsNotifier(ctx), + u.ID, + notifications.TemplateUserAccountCreated, map[string]string{ "created_account_name": user.Username, "created_account_user_name": user.Name, "initiator": req.accountCreatorName, - }, "api-users-create", + }, + map[string]any{ + "user": map[string]any{"id": user.ID, "name": user.Name, "email": user.Email}, + }, + "api-users-create", user.ID, ); err != nil { api.Logger.Warn(ctx, "unable to notify about created user", slog.F("created_user", user.Username), slog.Error(err)) diff --git a/coderd/users_test.go b/coderd/users_test.go index 1386d76f3e0bf..53ec98b30d911 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -392,12 +392,19 @@ func TestNotifyUserStatusChanged(t *testing.T) { // Validate that each expected notification is present in notifyEnq.Sent() for _, expected := range expectedNotifications { found := false - for _, sent := range notifyEnq.Sent() { + for _, sent := range notifyEnq.Sent(notificationstest.WithTemplateID(expected.TemplateID)) { if sent.TemplateID == expected.TemplateID && sent.UserID == expected.UserID && slices.Contains(sent.Targets, member.ID) && sent.Labels[label] == member.Username { found = true + + require.IsType(t, map[string]any{}, sent.Data["user"]) + userData := sent.Data["user"].(map[string]any) + require.Equal(t, member.ID, userData["id"]) + require.Equal(t, member.Name, userData["name"]) + require.Equal(t, member.Email, userData["email"]) + break } } @@ -858,11 +865,18 @@ func TestNotifyCreatedUser(t *testing.T) { require.NoError(t, err) // then - require.Len(t, notifyEnq.Sent(), 1) - require.Equal(t, notifications.TemplateUserAccountCreated, notifyEnq.Sent()[0].TemplateID) - require.Equal(t, firstUser.UserID, notifyEnq.Sent()[0].UserID) - require.Contains(t, notifyEnq.Sent()[0].Targets, user.ID) - require.Equal(t, user.Username, notifyEnq.Sent()[0].Labels["created_account_name"]) + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateUserAccountCreated)) + require.Len(t, sent, 1) + require.Equal(t, notifications.TemplateUserAccountCreated, sent[0].TemplateID) + require.Equal(t, firstUser.UserID, sent[0].UserID) + require.Contains(t, sent[0].Targets, user.ID) + require.Equal(t, user.Username, sent[0].Labels["created_account_name"]) + + require.IsType(t, map[string]any{}, sent[0].Data["user"]) + userData := sent[0].Data["user"].(map[string]any) + require.Equal(t, user.ID, userData["id"]) + require.Equal(t, user.Name, userData["name"]) + require.Equal(t, user.Email, userData["email"]) }) t.Run("UserAdminNotified", func(t *testing.T) { diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 375eaab5cd33b..76166bfcb6164 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -527,7 +527,7 @@ func (api *API) notifyWorkspaceUpdated( "workspace": map[string]any{"id": workspace.ID, "name": workspace.Name}, "template": map[string]any{"id": template.ID, "name": template.Name}, "template_version": map[string]any{"id": version.ID, "name": version.Name}, - "owner": map[string]any{"id": owner.ID, "name": owner.Name}, + "owner": map[string]any{"id": owner.ID, "name": owner.Name, "email": owner.Email}, "parameters": buildParameters, }, "api-workspaces-updated", diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index da4c09329cc39..fc8961a8c74ac 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -648,7 +648,7 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: notify}) first := coderdtest.CreateFirstUser(t, client) templateAdminClient, templateAdmin := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleTemplateAdmin()) - userClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) // Create a template with an initial version version := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil) @@ -684,6 +684,12 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T) require.Contains(t, sent[0].Targets, workspace.ID) require.Contains(t, sent[0].Targets, workspace.OrganizationID) require.Contains(t, sent[0].Targets, workspace.OwnerID) + + owner, ok := sent[0].Data["owner"].(map[string]any) + require.True(t, ok, "notification data should have owner") + require.Equal(t, user.ID, owner["id"]) + require.Equal(t, user.Name, owner["name"]) + require.Equal(t, user.Email, owner["email"]) }) } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 158f27132b427..7a64648033c79 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -809,7 +809,7 @@ func (api *API) notifyWorkspaceCreated( "workspace": map[string]any{"id": workspace.ID, "name": workspace.Name}, "template": map[string]any{"id": template.ID, "name": template.Name}, "template_version": map[string]any{"id": version.ID, "name": version.Name}, - "owner": map[string]any{"id": owner.ID, "name": owner.Name}, + "owner": map[string]any{"id": owner.ID, "name": owner.Name, "email": owner.Email}, "parameters": buildParameters, }, "api-workspaces-create", diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index e74d5174123a1..b8bf71c3eb3ac 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -639,6 +639,12 @@ func TestPostWorkspacesByOrganization(t *testing.T) { require.Contains(t, sent[0].Targets, workspace.ID) require.Contains(t, sent[0].Targets, workspace.OrganizationID) require.Contains(t, sent[0].Targets, workspace.OwnerID) + + owner, ok := sent[0].Data["owner"].(map[string]any) + require.True(t, ok, "notification data should have owner") + require.Equal(t, memberUser.ID, owner["id"]) + require.Equal(t, memberUser.Name, owner["name"]) + require.Equal(t, memberUser.Email, owner["email"]) }) t.Run("CreateWithAuditLogs", func(t *testing.T) { diff --git a/codersdk/idpsync.go b/codersdk/idpsync.go index 2cc1f51ee3011..8f92cea680e25 100644 --- a/codersdk/idpsync.go +++ b/codersdk/idpsync.go @@ -12,6 +12,13 @@ import ( "golang.org/x/xerrors" ) +type IDPSyncMapping[ResourceIdType uuid.UUID | string] struct { + // The IdP claim the user has + Given string + // The ID of the Coder resource the user should be added to + Gets ResourceIdType +} + type GroupSyncSettings struct { // Field is the name of the claim field that specifies what groups a user // should be in. If empty, no groups will be synced. @@ -61,6 +68,46 @@ func (c *Client) PatchGroupIDPSyncSettings(ctx context.Context, orgID string, re return resp, json.NewDecoder(res.Body).Decode(&resp) } +type PatchGroupIDPSyncConfigRequest struct { + Field string `json:"field"` + RegexFilter *regexp.Regexp `json:"regex_filter"` + AutoCreateMissing bool `json:"auto_create_missing_groups"` +} + +func (c *Client) PatchGroupIDPSyncConfig(ctx context.Context, orgID string, req PatchGroupIDPSyncConfigRequest) (GroupSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/groups/config", orgID), req) + if err != nil { + return GroupSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GroupSyncSettings{}, ReadBodyAsError(res) + } + var resp GroupSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// If the same mapping is present in both Add and Remove, Remove will take presidence. +type PatchGroupIDPSyncMappingRequest struct { + Add []IDPSyncMapping[uuid.UUID] + Remove []IDPSyncMapping[uuid.UUID] +} + +func (c *Client) PatchGroupIDPSyncMapping(ctx context.Context, orgID string, req PatchGroupIDPSyncMappingRequest) (GroupSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/groups/mapping", orgID), req) + if err != nil { + return GroupSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return GroupSyncSettings{}, ReadBodyAsError(res) + } + var resp GroupSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + type RoleSyncSettings struct { // Field is the name of the claim field that specifies what organization roles // a user should be given. If empty, no roles will be synced. @@ -97,6 +144,44 @@ func (c *Client) PatchRoleIDPSyncSettings(ctx context.Context, orgID string, req return resp, json.NewDecoder(res.Body).Decode(&resp) } +type PatchRoleIDPSyncConfigRequest struct { + Field string `json:"field"` +} + +func (c *Client) PatchRoleIDPSyncConfig(ctx context.Context, orgID string, req PatchRoleIDPSyncConfigRequest) (RoleSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles/config", orgID), req) + if err != nil { + return RoleSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return RoleSyncSettings{}, ReadBodyAsError(res) + } + var resp RoleSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// If the same mapping is present in both Add and Remove, Remove will take presidence. +type PatchRoleIDPSyncMappingRequest struct { + Add []IDPSyncMapping[string] + Remove []IDPSyncMapping[string] +} + +func (c *Client) PatchRoleIDPSyncMapping(ctx context.Context, orgID string, req PatchRoleIDPSyncMappingRequest) (RoleSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/idpsync/roles/mapping", orgID), req) + if err != nil { + return RoleSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return RoleSyncSettings{}, ReadBodyAsError(res) + } + var resp RoleSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + type OrganizationSyncSettings struct { // Field selects the claim field to be used as the created user's // organizations. If the field is the empty string, then no organization @@ -137,6 +222,45 @@ func (c *Client) PatchOrganizationIDPSyncSettings(ctx context.Context, req Organ return resp, json.NewDecoder(res.Body).Decode(&resp) } +type PatchOrganizationIDPSyncConfigRequest struct { + Field string `json:"field"` + AssignDefault bool `json:"assign_default"` +} + +func (c *Client) PatchOrganizationIDPSyncConfig(ctx context.Context, req PatchOrganizationIDPSyncConfigRequest) (OrganizationSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, "/api/v2/settings/idpsync/organization/config", req) + if err != nil { + return OrganizationSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return OrganizationSyncSettings{}, ReadBodyAsError(res) + } + var resp OrganizationSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// If the same mapping is present in both Add and Remove, Remove will take presidence. +type PatchOrganizationIDPSyncMappingRequest struct { + Add []IDPSyncMapping[uuid.UUID] + Remove []IDPSyncMapping[uuid.UUID] +} + +func (c *Client) PatchOrganizationIDPSyncMapping(ctx context.Context, req PatchOrganizationIDPSyncMappingRequest) (OrganizationSyncSettings, error) { + res, err := c.Request(ctx, http.MethodPatch, "/api/v2/settings/idpsync/organization/mapping", req) + if err != nil { + return OrganizationSyncSettings{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return OrganizationSyncSettings{}, ReadBodyAsError(res) + } + var resp OrganizationSyncSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + func (c *Client) GetAvailableIDPSyncFields(ctx context.Context) ([]string, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/settings/idpsync/available-fields", nil) if err != nil { diff --git a/docs/admin/users/idp-sync.md b/docs/admin/users/idp-sync.md index 8e9ea79a9a80b..ee2dc83be387c 100644 --- a/docs/admin/users/idp-sync.md +++ b/docs/admin/users/idp-sync.md @@ -1,13 +1,39 @@ -# IDP Sync +# IdP Sync
-IDP sync is an Enterprise and Premium feature. +IdP sync is an Enterprise and Premium feature. [Learn more](https://coder.com/pricing#compare-plans).
+IdP (Identity provider) sync allows you to use OpenID Connect (OIDC) to +synchronize Coder groups, roles, and organizations based on claims from your IdP. + +## Prerequisites + +### Confirm that OIDC provider sends claims + +To confirm that your OIDC provider is sending claims, log in with OIDC and visit +the following URL with an `Owner` account: + +```text +https://[coder.example.com]/api/v2/debug/[your-username]/debug-link +``` + +You should see a field in either `id_token_claims`, `user_info_claims` or +both followed by a list of the user's OIDC groups in the response. + +This is the [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) +sent by the OIDC provider. + +Depending on the OIDC provider, this claim might be called something else. +Common names include `groups`, `memberOf`, and `roles`. + +See the [troubleshooting section](#troubleshooting-grouproleorganization-sync) +for help troubleshooting common issues. + ## Group Sync If your OpenID Connect provider supports group claims, you can configure Coder @@ -21,115 +47,36 @@ If group sync is enabled, the user's groups will be controlled by the OIDC provider. This means manual group additions/removals will be overwritten on the next user login. -There are two ways you can configure group sync: +For deployments with multiple [organizations](./organizations.md), configure +group sync for each organization.
-## Server Flags +### Dashboard -1. Confirm that your OIDC provider is sending claims. - - Log in with OIDC and visit the following URL with an `Owner` account: +1. Fetch the corresponding group IDs using the following endpoint: ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link + https://[coder.example.com]/api/v2/groups ``` - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC groups in the response. This is - the [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) - sent by the OIDC provider. - - See [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug - this. +1. As an Owner or Organization Admin, go to **Admin settings**, select + **Organizations**, then **IdP Sync**: - Depending on the OIDC provider, this claim may be called something else. - Common names include `groups`, `memberOf`, and `roles`. + ![IdP Sync - Group sync settings](../../images/admin/users/organizations/group-sync-empty.png) -1. Configure the Coder server to read groups from the claim name with the - [OIDC group field](../../reference/cli/server.md#--oidc-group-field) server - flag: +1. Enter the **Group sync field** and an optional **Regex filter**, then select + **Save**. - - Environment variable: +1. Select **Auto create missing groups** to automatically create groups + returned by the OIDC provider if they do not exist in Coder. - ```sh - CODER_OIDC_GROUP_FIELD=groups - ``` +1. Enter the **IdP group name** and **Coder group**, then **Add IdP group**. - - As a flag: - - ```sh - --oidc-group-field groups - ``` - -On login, users will automatically be assigned to groups that have matching -names in Coder and removed from groups that the user no longer belongs to. - -For cases when an OIDC provider only returns group IDs or you want to have -different group names in Coder than in your OIDC provider, you can configure -mapping between the two with the -[OIDC group mapping](../../reference/cli/server.md#--oidc-group-mapping) server -flag: - -- Environment variable: - - ```sh - CODER_OIDC_GROUP_MAPPING='{"myOIDCGroupID": "myCoderGroupName"}' - ``` - -- As a flag: - - ```sh - --oidc-group-mapping '{"myOIDCGroupID": "myCoderGroupName"}' - ``` - -Below is an example mapping in the Coder Helm chart: - -```yaml -coder: - env: - - name: CODER_OIDC_GROUP_MAPPING - value: > - {"myOIDCGroupID": "myCoderGroupName"} -``` - -From the example above, users that belong to the `myOIDCGroupID` group in your -OIDC provider will be added to the `myCoderGroupName` group in Coder. - -## Runtime (Organizations) - -
- -You must have a Premium license with Organizations enabled to use this. -[Contact your account team](https://coder.com/contact) for more details. - -
- -For deployments with multiple [organizations](./organizations.md), you must -configure group sync at the organization level. In future Coder versions, you -will be able to configure this in the UI. For now, you must use CLI commands. +### CLI 1. Confirm you have the [Coder CLI](../../install/index.md) installed and are - logged in with a user who is an Owner or Organization Admin role. - -1. Confirm that your OIDC provider is sending a groups claim. - - Log in with OIDC and visit the following URL: - - ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link - ``` - - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC groups in the response. This is - the [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) - sent by the OIDC provider. - - See [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug - this. - - Depending on the OIDC provider, this claim may be called something else. - Common names include `groups`, `memberOf`, and `roles`. + logged in with a user who is an Owner or has an Organization Admin role. 1. To fetch the current group sync settings for an organization, run the following: @@ -165,7 +112,7 @@ Below is an example that uses the `groups` claim and maps all groups prefixed by
-You much specify Coder group IDs instead of group names. The fastest way to find +You must specify Coder group IDs instead of group names. The fastest way to find the ID for a corresponding group is by visiting `https://coder.example.com/api/v2/groups`. @@ -200,102 +147,110 @@ coder organizations settings set group-sync \ Visit the Coder UI to confirm these changes: -![IDP Sync](../../images/admin/users/organizations/group-sync.png) +![IdP Sync](../../images/admin/users/organizations/group-sync.png) -
+### Server Flags -### Group allowlist +
-You can limit which groups from your identity provider can log in to Coder with -[CODER_OIDC_ALLOWED_GROUPS](https://coder.com/docs/cli/server#--oidc-allowed-groups). -Users who are not in a matching group will see the following error: +Use server flags only with Coder deployments with a single organization. -Unauthorized group error +You can use the dashboard to configure group sync instead. -## Role Sync +
-
+1. Configure the Coder server to read groups from the claim name with the + [OIDC group field](../../reference/cli/server.md#--oidc-group-field) server + flag: -Role sync is an Enterprise and Premium feature. -[Learn more](https://coder.com/pricing#compare-plans). + - Environment variable: -
+ ```sh + CODER_OIDC_GROUP_FIELD=groups + ``` -If your OpenID Connect provider supports roles claims, you can configure Coder -to synchronize roles in your auth provider to roles within Coder. + - As a flag: -There are 2 ways to do role sync. Server Flags assign site wide roles, and -runtime org role sync assigns organization roles + ```sh + --oidc-group-field groups + ``` -
+1. On login, users will automatically be assigned to groups that have matching + names in Coder and removed from groups that the user no longer belongs to. -You must have a Premium license with Organizations enabled to use this. -[Contact your account team](https://coder.com/contact) for more details. +1. For cases when an OIDC provider only returns group IDs or you want to have + different group names in Coder than in your OIDC provider, you can configure + mapping between the two with the + [OIDC group mapping](../../reference/cli/server.md#--oidc-group-mapping) server + flag: -
+ - Environment variable: -
+ ```sh + CODER_OIDC_GROUP_MAPPING='{"myOIDCGroupID": "myCoderGroupName"}' + ``` + + - As a flag: -## Server Flags + ```sh + --oidc-group-mapping '{"myOIDCGroupID": "myCoderGroupName"}' + ``` -1. Confirm that your OIDC provider is sending a roles claim by logging in with - OIDC and visiting the following URL with an `Owner` account: + Below is an example mapping in the Coder Helm chart: - ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link + ```yaml + coder: + env: + - name: CODER_OIDC_GROUP_MAPPING + value: > + {"myOIDCGroupID": "myCoderGroupName"} ``` - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC roles in the response. This is the - [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) sent by - the OIDC provider. + From this example, users that belong to the `myOIDCGroupID` group in your + OIDC provider will be added to the `myCoderGroupName` group in Coder. - See [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug - this. +
- Depending on the OIDC provider, this claim may be called something else. +### Group allowlist -1. Configure the Coder server to read groups from the claim name with the - [OIDC role field](../../reference/cli/server.md#--oidc-user-role-field) - server flag: +You can limit which groups from your identity provider can log in to Coder with +[CODER_OIDC_ALLOWED_GROUPS](https://coder.com/docs/cli/server#--oidc-allowed-groups). +Users who are not in a matching group will see the following error: -1. Set the following in your Coder server [configuration](../setup/index.md). +Unauthorized group error - ```env - # Depending on your identity provider configuration, you may need to explicitly request a "roles" scope - CODER_OIDC_SCOPES=openid,profile,email,roles +## Role Sync - # The following fields are required for role sync: - CODER_OIDC_USER_ROLE_FIELD=roles - CODER_OIDC_USER_ROLE_MAPPING='{"TemplateAuthor":["template-admin","user-admin"]}' - ``` +If your OpenID Connect provider supports roles claims, you can configure Coder +to synchronize roles in your auth provider to roles within Coder. -One role from your identity provider can be mapped to many roles in Coder. The -example above maps to two roles in Coder. +For deployments with multiple [organizations](./organizations.md), configure +role sync at the organization level. -## Runtime (Organizations) +
-For deployments with multiple [organizations](./organizations.md), you can -configure role sync at the organization level. In future Coder versions, you -will be able to configure this in the UI. For now, you must use CLI commands. +### Dashboard -1. Confirm that your OIDC provider is sending a roles claim. +1. As an Owner or Organization Admin, go to **Admin settings**, select + **Organizations**, then **IdP Sync**. - Log in with OIDC and visit the following URL with an `Owner` account: +1. Select the **Role sync settings** tab: - ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link - ``` + ![IdP Sync - Role sync settings](../../images/admin/users/organizations/role-sync-empty.png) + +1. Enter the **Role sync field**, then select **Save**. - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC roles in the response. This is the - [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) sent by - the OIDC provider. +1. Enter the **IdP role name** and **Coder role**, then **Add IdP role**. - See [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug - this. + To add a new custom role, select **Roles** from the sidebar, then + **Create custom role**. - Depending on the OIDC provider, this claim may be called something else. + Visit the [groups and roles documentation](./groups-roles.md) for more information. + +### CLI + +1. Confirm you have the [Coder CLI](../../install/index.md) installed and are + logged in with a user who is an Owner or has an Organization Admin role. 1. To fetch the current group sync settings for an organization, run the following: @@ -316,7 +271,7 @@ will be able to configure this in the UI. For now, you must use CLI commands. ``` Below is an example that uses the `roles` claim and maps `coder-admins` from the -IDP as an `Organization Admin` and also maps to a custom `provisioner-admin` +IdP as an `Organization Admin` and also maps to a custom `provisioner-admin` role: ```json @@ -332,7 +287,7 @@ role:
Be sure to use the `name` field for each role, not the display name. Use -`coder organization roles show --org=` to see roles for your +`coder organization roles show --org=` to see roles for your organization.
@@ -347,19 +302,40 @@ coder organizations settings set role-sync \ Visit the Coder UI to confirm these changes: -![IDP Sync](../../images/admin/users/organizations/role-sync.png) +![IdP Sync](../../images/admin/users/organizations/role-sync.png) -
+### Server Flags -## Organization Sync +
-
+Use server flags only with Coder deployments with a single organization. -Organization sync is an Enterprise and Premium feature. -[Learn more](https://coder.com/pricing#compare-plans). +You can use the dashboard to configure role sync instead.
+1. Configure the Coder server to read groups from the claim name with the + [OIDC role field](../../reference/cli/server.md#--oidc-user-role-field) + server flag: + +1. Set the following in your Coder server [configuration](../setup/index.md). + + ```env + # Depending on your identity provider configuration, you may need to explicitly request a "roles" scope + CODER_OIDC_SCOPES=openid,profile,email,roles + + # The following fields are required for role sync: + CODER_OIDC_USER_ROLE_FIELD=roles + CODER_OIDC_USER_ROLE_MAPPING='{"TemplateAuthor":["template-admin","user-admin"]}' + ``` + +One role from your identity provider can be mapped to many roles in Coder. The +example above maps to two roles in Coder. + + + +## Organization Sync + If your OpenID Connect provider supports groups/role claims, you can configure Coder to synchronize claims in your auth provider to organizations within Coder. @@ -370,28 +346,11 @@ Organization sync works across all organizations. On user login, the sync will add and remove the user from organizations based on their IdP claims. After the sync, the user's state should match that of the IdP. -You can initiate an organization sync through the CLI or through the Coder -dashboard: +You can initiate an organization sync through the Coder dashboard or CLI:
-## Dashboard - -1. Confirm that your OIDC provider is sending claims. Log in with OIDC and visit - the following URL with an `Owner` account: - - ```text - https://[coder.example.com]/api/v2/debug/[your-username]/debug-link - ``` - - You should see a field in either `id_token_claims`, `user_info_claims` or - both followed by a list of the user's OIDC groups in the response. This is - the [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) - sent by the OIDC provider. See - [Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug this. - - Depending on the OIDC provider, this claim may be called something else. - Common names include `groups`, `memberOf`, and `roles`. +### Dashboard 1. Fetch the corresponding organization IDs using the following endpoint: @@ -400,7 +359,7 @@ dashboard: ``` 1. As a Coder organization user admin or site-wide user admin, go to - **Settings** > **IdP organization sync**. + **Admin settings** > **Deployment** and select **IdP organization sync**. 1. In the **Organization sync field** text box, enter the organization claim, then select **Save**. @@ -415,7 +374,7 @@ dashboard: ![IdP organization sync](../../images/admin/users/organizations/idp-org-sync.png) -## CLI +### CLI Use the Coder CLI to show and adjust the settings. @@ -467,11 +426,11 @@ settings, a user's memberships will update when they log out and log back in. ## Troubleshooting group/role/organization sync -Some common issues when enabling group/role sync. +Some common issues when enabling group, role, or organization sync. ### General guidelines -If you are running into issues with group/role sync: +If you are running into issues with a sync: 1. View your Coder server logs and enable [verbose mode](../../reference/cli/index.md#-v---verbose). @@ -487,7 +446,7 @@ If you are running into issues with group/role sync: 1. Attempt to log in, preferably with a user who has the `Owner` role. -The logs for a successful group sync look like this (human-readable): +The logs for a successful sync look like this (human-readable): ```sh [debu] coderd.userauth: got oidc claims request_id=49e86507-6842-4b0b-94d4-f245e62e49f3 source=id_token claim_fields="[aio aud email exp groups iat idp iss name nbf oid preferred_username rh sub tid uti ver]" blank=[] @@ -552,7 +511,7 @@ The application '' asked for scope 'groups' that doesn't exist This can happen because the identity provider has a different name for the scope. For example, Azure AD uses `GroupMember.Read.All` instead of `groups`. -You can find the correct scope name in the IDP's documentation. Some IDP's allow +You can find the correct scope name in the IdP's documentation. Some IdPs allow configuring the name of this scope. The solution is to update the value of `CODER_OIDC_SCOPES` to the correct value @@ -562,15 +521,15 @@ for the identity provider. Steps to troubleshoot. -1. Ensure the user is a part of a group in the IDP. If the user has 0 groups, no +1. Ensure the user is a part of a group in the IdP. If the user has 0 groups, no `groups` claim will be sent. 2. Check if another claim appears to be the correct claim with a different name. A common name is `memberOf` instead of `groups`. If this is present, update `CODER_OIDC_GROUP_FIELD=memberOf`. -3. Make sure the number of groups being sent is under the limit of the IDP. Some - IDPs will return an error, while others will just omit the `groups` claim. A +3. Make sure the number of groups being sent is under the limit of the IdP. Some + IdPs will return an error, while others will just omit the `groups` claim. A common solution is to create a filter on the identity provider that returns - less than the limit for your IDP. + less than the limit for your IdP. - [Azure AD limit is 200, and omits groups if exceeded.](https://learn.microsoft.com/en-us/azure/active-directory/hybrid/connect/how-to-connect-fed-group-claims#options-for-applications-to-consume-group-information) - [Okta limit is 100, and returns an error if exceeded.](https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned) @@ -582,32 +541,37 @@ Below are some details specific to individual OIDC providers. > **Note:** Tested on ADFS 4.0, Windows Server 2019 -1. In your Federation Server, create a new application group for Coder. Follow - the steps as described - [here.](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/development/msal/adfs-msal-web-app-web-api#app-registration-in-ad-fs) +1. In your Federation Server, create a new application group for Coder. + Follow the steps as described in the [Windows Server documentation] + (https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/development/msal/adfs-msal-web-app-web-api#app-registration-in-ad-fs). + - **Server Application**: Note the Client ID. - **Configure Application Credentials**: Note the Client Secret. - **Configure Web API**: Set the Client ID as the relying party identifier. - **Application Permissions**: Allow access to the claims `openid`, `email`, `profile`, and `allatclaims`. + 1. Visit your ADFS server's `/.well-known/openid-configuration` URL and note the value for `issuer`. - > **Note:** This is usually of the form - > `https://adfs.corp/adfs/.well-known/openid-configuration` + + This will look something like + `https://adfs.corp/adfs/.well-known/openid-configuration`. + 1. In Coder's configuration file (or Helm values as appropriate), set the following environment variables or their corresponding CLI arguments: - - `CODER_OIDC_ISSUER_URL`: the `issuer` value from the previous step. - - `CODER_OIDC_CLIENT_ID`: the Client ID from step 1. - - `CODER_OIDC_CLIENT_SECRET`: the Client Secret from step 1. + - `CODER_OIDC_ISSUER_URL`: `issuer` value from the previous step. + - `CODER_OIDC_CLIENT_ID`: Client ID from step 1. + - `CODER_OIDC_CLIENT_SECRET`: Client Secret from step 1. - `CODER_OIDC_AUTH_URL_PARAMS`: set to - ```console + ```json {"resource":"$CLIENT_ID"} ``` - where `$CLIENT_ID` is the Client ID from step 1 - ([see here](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#:~:text=scope%E2%80%AFopenid.-,resource,-optional)). + Where `$CLIENT_ID` is the Client ID from step 1. + Consult the Microsoft [AD FS OpenID Connect/OAuth flows and Application Scenarios documentation](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#:~:text=scope%E2%80%AFopenid.-,resource,-optional) for more information. + This is required for the upstream OIDC provider to return the requested claims. @@ -615,34 +579,35 @@ Below are some details specific to individual OIDC providers. 1. Configure [Issuance Transform Rules](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-rule-to-send-ldap-attributes-as-claims) - on your federation server to send the following claims: + on your Federation Server to send the following claims: - `preferred_username`: You can use e.g. "Display Name" as required. - `email`: You can use e.g. the LDAP attribute "E-Mail-Addresses" as required. - `email_verified`: Create a custom claim rule: - ```console + ```json => issue(Type = "email_verified", Value = "true") ``` - (Optional) If using Group Sync, send the required groups in the configured - groups claim field. See [here](https://stackoverflow.com/a/55570286) for an - example. + groups claim field. + Use [this answer from Stack Overflow](https://stackoverflow.com/a/55570286) for an example. ### Keycloak -The access_type parameter has two possible values: "online" and "offline." By -default, the value is set to "offline". This means that when a user -authenticates using OIDC, the application requests offline access to the user's -resources, including the ability to refresh access tokens without requiring the -user to reauthenticate. +The `access_type` parameter has two possible values: `online` and `offline`. +By default, the value is set to `offline`. + +This means that when a user authenticates using OIDC, the application requests +offline access to the user's resources, including the ability to refresh access +tokens without requiring the user to reauthenticate. -To enable the `offline_access` scope, which allows for the refresh token +To enable the `offline_access` scope which allows for the refresh token functionality, you need to add it to the list of requested scopes during the -authentication flow. Including the `offline_access` scope in the requested -scopes ensures that the user is granted the necessary permissions to obtain -refresh tokens. +authentication flow. +Including the `offline_access` scope in the requested scopes ensures that the +user is granted the necessary permissions to obtain refresh tokens. By combining the `{"access_type":"offline"}` parameter in the OIDC Auth URL with the `offline_access` scope, you can achieve the desired behavior of obtaining diff --git a/docs/images/admin/users/organizations/group-sync-empty.png b/docs/images/admin/users/organizations/group-sync-empty.png new file mode 100644 index 0000000000000..4114ec7cacd8f Binary files /dev/null and b/docs/images/admin/users/organizations/group-sync-empty.png differ diff --git a/docs/images/admin/users/organizations/group-sync.png b/docs/images/admin/users/organizations/group-sync.png index a4013f2f15559..f617dd02eeef0 100644 Binary files a/docs/images/admin/users/organizations/group-sync.png and b/docs/images/admin/users/organizations/group-sync.png differ diff --git a/docs/images/admin/users/organizations/role-sync-empty.png b/docs/images/admin/users/organizations/role-sync-empty.png new file mode 100644 index 0000000000000..91e36fff5bf02 Binary files /dev/null and b/docs/images/admin/users/organizations/role-sync-empty.png differ diff --git a/docs/images/admin/users/organizations/role-sync.png b/docs/images/admin/users/organizations/role-sync.png index 1b0fafb39fae1..9360c9e1337aa 100644 Binary files a/docs/images/admin/users/organizations/role-sync.png and b/docs/images/admin/users/organizations/role-sync.png differ diff --git a/docs/images/admin/users/organizations/template-org-picker.png b/docs/images/admin/users/organizations/template-org-picker.png index 73c37ed517aec..cf5d80761902c 100644 Binary files a/docs/images/admin/users/organizations/template-org-picker.png and b/docs/images/admin/users/organizations/template-org-picker.png differ diff --git a/docs/images/admin/users/organizations/workspace-list.png b/docs/images/admin/users/organizations/workspace-list.png index bbe6cca9eb909..e007cdaf8734a 100644 Binary files a/docs/images/admin/users/organizations/workspace-list.png and b/docs/images/admin/users/organizations/workspace-list.png differ diff --git a/docs/install/offline.md b/docs/install/offline.md index 6a41bd9437894..0f83ae4077ee4 100644 --- a/docs/install/offline.md +++ b/docs/install/offline.md @@ -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.9.8 +ARG TERRAFORM_VERSION=1.10.5 RUN apk update && \ apk del terraform && \ curl -LOs https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip \ diff --git a/docs/manifest.json b/docs/manifest.json index 7cf556984ecbf..119099ab0b061 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -284,7 +284,7 @@ "state": ["enterprise", "premium"] }, { - "title": "IDP Sync", + "title": "IdP Sync", "path": "./admin/users/idp-sync.md", "state": ["enterprise", "premium"] }, diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 6f8b061ed9025..a1a61f4a5b54a 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -1953,6 +1953,141 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Update group IdP Sync config + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/groups/config \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/groups/config` + +> Body parameter + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "regex_filter": {} +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|----------------------------------------------------------------------------------------------|----------|-------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchGroupIDPSyncConfigRequest](schemas.md#codersdkpatchgroupidpsyncconfigrequest) | true | New config values | + +### Example responses + +> 200 Response + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "legacy_group_name_mapping": { + "property1": "string", + "property2": "string" + }, + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "regex_filter": {} +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.GroupSyncSettings](schemas.md#codersdkgroupsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update group IdP Sync mapping + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/groups/mapping \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/groups/mapping` + +> Body parameter + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|------------------------------------------------------------------------------------------------|----------|-----------------------------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchGroupIDPSyncMappingRequest](schemas.md#codersdkpatchgroupidpsyncmappingrequest) | true | Description of the mappings to add and remove | + +### Example responses + +> 200 Response + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "legacy_group_name_mapping": { + "property1": "string", + "property2": "string" + }, + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "regex_filter": {} +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.GroupSyncSettings](schemas.md#codersdkgroupsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get role IdP Sync settings by organization ### Code samples @@ -2061,6 +2196,127 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Update role IdP Sync config + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/roles/config \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/roles/config` + +> Body parameter + +```json +{ + "field": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|--------------------------------------------------------------------------------------------|----------|-------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchRoleIDPSyncConfigRequest](schemas.md#codersdkpatchroleidpsyncconfigrequest) | true | New config values | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + } +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.RoleSyncSettings](schemas.md#codersdkrolesyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update role IdP Sync mapping + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/settings/idpsync/roles/mapping \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}/settings/idpsync/roles/mapping` + +> Body parameter + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|----------------------------------------------------------------------------------------------|----------|-----------------------------------------------| +| `organization` | path | string(uuid) | true | Organization ID or name | +| `body` | body | [codersdk.PatchRoleIDPSyncMappingRequest](schemas.md#codersdkpatchroleidpsyncmappingrequest) | true | Description of the mappings to add and remove | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + } +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.RoleSyncSettings](schemas.md#codersdkrolesyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Fetch provisioner key details ### Code samples @@ -2677,6 +2933,128 @@ curl -X PATCH http://coder-server:8080/api/v2/settings/idpsync/organization \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Update organization IdP Sync config + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/settings/idpsync/organization/config \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /settings/idpsync/organization/config` + +> Body parameter + +```json +{ + "assign_default": true, + "field": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|--------|------|------------------------------------------------------------------------------------------------------------|----------|-------------------| +| `body` | body | [codersdk.PatchOrganizationIDPSyncConfigRequest](schemas.md#codersdkpatchorganizationidpsyncconfigrequest) | true | New config values | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "organization_assign_default": true +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OrganizationSyncSettings](schemas.md#codersdkorganizationsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update organization IdP Sync mapping + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/settings/idpsync/organization/mapping \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /settings/idpsync/organization/mapping` + +> Body parameter + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|--------|------|--------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------| +| `body` | body | [codersdk.PatchOrganizationIDPSyncMappingRequest](schemas.md#codersdkpatchorganizationidpsyncmappingrequest) | true | Description of the mappings to add and remove | + +### Example responses + +> 200 Response + +```json +{ + "field": "string", + "mapping": { + "property1": [ + "string" + ], + "property2": [ + "string" + ] + }, + "organization_assign_default": true +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OrganizationSyncSettings](schemas.md#codersdkorganizationsyncsettings) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get template ACLs ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index db6fc2a51f58e..20ed37f81f7f7 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4152,6 +4152,54 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | » `[any property]` | array of string | false | | | | `organization_assign_default` | boolean | false | | Organization assign default will ensure the default org is always included for every user, regardless of their claims. This preserves legacy behavior. | +## codersdk.PatchGroupIDPSyncConfigRequest + +```json +{ + "auto_create_missing_groups": true, + "field": "string", + "regex_filter": {} +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------------------|--------------------------------|----------|--------------|-------------| +| `auto_create_missing_groups` | boolean | false | | | +| `field` | string | false | | | +| `regex_filter` | [regexp.Regexp](#regexpregexp) | false | | | + +## codersdk.PatchGroupIDPSyncMappingRequest + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|-----------------|----------|--------------|----------------------------------------------------------| +| `add` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | +| `remove` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | + ## codersdk.PatchGroupRequest ```json @@ -4180,6 +4228,96 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `quota_allowance` | integer | false | | | | `remove_users` | array of string | false | | | +## codersdk.PatchOrganizationIDPSyncConfigRequest + +```json +{ + "assign_default": true, + "field": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------|---------|----------|--------------|-------------| +| `assign_default` | boolean | false | | | +| `field` | string | false | | | + +## codersdk.PatchOrganizationIDPSyncMappingRequest + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|-----------------|----------|--------------|----------------------------------------------------------| +| `add` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | +| `remove` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | + +## codersdk.PatchRoleIDPSyncConfigRequest + +```json +{ + "field": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------|--------|----------|--------------|-------------| +| `field` | string | false | | | + +## codersdk.PatchRoleIDPSyncMappingRequest + +```json +{ + "add": [ + { + "gets": "string", + "given": "string" + } + ], + "remove": [ + { + "gets": "string", + "given": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|-----------------|----------|--------------|----------------------------------------------------------| +| `add` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | +| `remove` | array of object | false | | | +| `» gets` | string | false | | The ID of the Coder resource the user should be added to | +| `» given` | string | false | | The IdP claim the user has | + ## codersdk.PatchTemplateVersionRequest ```json diff --git a/docs/tutorials/best-practices/organizations.md b/docs/tutorials/best-practices/organizations.md index 473bf832e11d8..5771df3e0bb8d 100644 --- a/docs/tutorials/best-practices/organizations.md +++ b/docs/tutorials/best-practices/organizations.md @@ -94,17 +94,6 @@ provider such as Okta. A single claim from the identity provider (like `memberOf`) can be used to sync site-wide roles, organizations, groups, and organization roles. -### Planned enhancements - -Site-wide role sync is managed via server flags. We plan on changing this to -runtime configuration so Coder does not need a re-deploy: - -- Issue [coder/internal#86](https://github.com/coder/internal/issues/86) - -Make all sync configurable via the dashboard UI: - -- [coder/coder#15290](https://github.com/coder/coder/issues/15290) - Regex filters and mapping can be configured to ensure the proper resources are allocated in Coder. Learn more about [IDP sync](../../admin/users/idp-sync.md). diff --git a/dogfood/contents/Dockerfile b/dogfood/contents/Dockerfile index 2de358c5c91e6..8c3613f59d468 100644 --- a/dogfood/contents/Dockerfile +++ b/dogfood/contents/Dockerfile @@ -195,9 +195,9 @@ RUN apt-get update --quiet && apt-get install --yes \ # Configure FIPS-compliant policies update-crypto-policies --set FIPS -# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.9.8. +# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.10.5. # Installing the same version here to match. -RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.9.8/terraform_1.9.8_linux_amd64.zip" && \ +RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.10.5/terraform_1.10.5_linux_amd64.zip" && \ unzip /tmp/terraform.zip -d /usr/local/bin && \ rm -f /tmp/terraform.zip && \ chmod +x /usr/local/bin/terraform && \ diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index b32f763720b9d..2a91fbbfd6f93 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -295,7 +295,10 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Route("/organization", func(r chi.Router) { r.Get("/", api.organizationIDPSyncSettings) r.Patch("/", api.patchOrganizationIDPSyncSettings) + r.Patch("/config", api.patchOrganizationIDPSyncConfig) + r.Patch("/mapping", api.patchOrganizationIDPSyncMapping) }) + r.Get("/available-fields", api.deploymentIDPSyncClaimFields) r.Get("/field-values", api.deploymentIDPSyncClaimFieldValues) }) @@ -307,11 +310,17 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { httpmw.ExtractOrganizationParam(api.Database), ) r.Route("/organizations/{organization}/settings", func(r chi.Router) { - r.Get("/idpsync/available-fields", api.organizationIDPSyncClaimFields) r.Get("/idpsync/groups", api.groupIDPSyncSettings) r.Patch("/idpsync/groups", api.patchGroupIDPSyncSettings) + r.Patch("/idpsync/groups/config", api.patchGroupIDPSyncConfig) + r.Patch("/idpsync/groups/mapping", api.patchGroupIDPSyncMapping) + r.Get("/idpsync/roles", api.roleIDPSyncSettings) r.Patch("/idpsync/roles", api.patchRoleIDPSyncSettings) + r.Patch("/idpsync/roles/config", api.patchRoleIDPSyncConfig) + r.Patch("/idpsync/roles/mapping", api.patchRoleIDPSyncMapping) + + r.Get("/idpsync/available-fields", api.organizationIDPSyncClaimFields) r.Get("/idpsync/field-values", api.organizationIDPSyncClaimFieldValues) }) }) diff --git a/enterprise/coderd/enidpsync/enidpsync.go b/enterprise/coderd/enidpsync/enidpsync.go index c7ba8dd3ecdc6..2020a4300ebc6 100644 --- a/enterprise/coderd/enidpsync/enidpsync.go +++ b/enterprise/coderd/enidpsync/enidpsync.go @@ -7,6 +7,8 @@ import ( "github.com/coder/coder/v2/coderd/runtimeconfig" ) +var _ idpsync.IDPSync = &EnterpriseIDPSync{} + // EnterpriseIDPSync enabled syncing user information from an external IDP. // The sync is an enterprise feature, so this struct wraps the AGPL implementation // and extends it with enterprise capabilities. These capabilities can entirely diff --git a/enterprise/coderd/enidpsync/organizations.go b/enterprise/coderd/enidpsync/organizations.go index 313d90fac8a9f..826144afc1492 100644 --- a/enterprise/coderd/enidpsync/organizations.go +++ b/enterprise/coderd/enidpsync/organizations.go @@ -19,6 +19,8 @@ func (e EnterpriseIDPSync) OrganizationSyncEnabled(ctx context.Context, db datab return false } + // If this logic is ever updated, make sure to update the corresponding + // checkIDPOrgSync in coderd/telemetry/telemetry.go. settings, err := e.OrganizationSyncSettings(ctx, db) if err == nil && settings.Field != "" { return true diff --git a/enterprise/coderd/enidpsync/organizations_test.go b/enterprise/coderd/enidpsync/organizations_test.go index 36dbedf3a466d..391535c9478d7 100644 --- a/enterprise/coderd/enidpsync/organizations_test.go +++ b/enterprise/coderd/enidpsync/organizations_test.go @@ -300,7 +300,7 @@ func TestOrganizationSync(t *testing.T) { // Create a new sync object sync := enidpsync.NewSync(logger, runtimeconfig.NewManager(), caseData.Entitlements, caseData.Settings) if caseData.RuntimeSettings != nil { - err := sync.UpdateOrganizationSettings(ctx, rdb, *caseData.RuntimeSettings) + err := sync.UpdateOrganizationSyncSettings(ctx, rdb, *caseData.RuntimeSettings) require.NoError(t, err) } diff --git a/enterprise/coderd/idpsync.go b/enterprise/coderd/idpsync.go index 192d61ea996c6..2dcee572eb692 100644 --- a/enterprise/coderd/idpsync.go +++ b/enterprise/coderd/idpsync.go @@ -3,6 +3,7 @@ package coderd import ( "fmt" "net/http" + "slices" "github.com/google/uuid" @@ -14,6 +15,7 @@ import ( "github.com/coder/coder/v2/coderd/idpsync" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" ) @@ -59,7 +61,6 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques ctx := r.Context() org := httpmw.OrganizationParam(r) auditor := *api.AGPL.Auditor.Load() - aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ Audit: auditor, Log: api.Logger, @@ -102,7 +103,7 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques } aReq.Old = *existing - err = api.IDPSync.UpdateGroupSettings(sysCtx, org.ID, api.Database, idpsync.GroupSyncSettings{ + err = api.IDPSync.UpdateGroupSyncSettings(sysCtx, org.ID, api.Database, idpsync.GroupSyncSettings{ Field: req.Field, Mapping: req.Mapping, RegexFilter: req.RegexFilter, @@ -130,6 +131,153 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques }) } +// @Summary Update group IdP Sync config +// @ID update-group-idp-sync-config +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.GroupSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchGroupIDPSyncConfigRequest true "New config values" +// @Router /organizations/{organization}/settings/idpsync/groups/config [patch] +func (api *API) patchGroupIDPSyncConfig(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchGroupIDPSyncConfigRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.GroupSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.GroupSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + settings = idpsync.GroupSyncSettings{ + Field: req.Field, + RegexFilter: req.RegexFilter, + AutoCreateMissing: req.AutoCreateMissing, + LegacyNameMapping: existing.LegacyNameMapping, + Mapping: existing.Mapping, + } + + err = api.IDPSync.UpdateGroupSyncSettings(sysCtx, org.ID, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.GroupSyncSettings{ + Field: settings.Field, + RegexFilter: settings.RegexFilter, + AutoCreateMissing: settings.AutoCreateMissing, + LegacyNameMapping: settings.LegacyNameMapping, + Mapping: settings.Mapping, + }) +} + +// @Summary Update group IdP Sync mapping +// @ID update-group-idp-sync-mapping +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.GroupSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchGroupIDPSyncMappingRequest true "Description of the mappings to add and remove" +// @Router /organizations/{organization}/settings/idpsync/groups/mapping [patch] +func (api *API) patchGroupIDPSyncMapping(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchGroupIDPSyncMappingRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.GroupSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.GroupSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + newMapping := applyIDPSyncMappingDiff(existing.Mapping, req.Add, req.Remove) + settings = idpsync.GroupSyncSettings{ + Field: existing.Field, + RegexFilter: existing.RegexFilter, + AutoCreateMissing: existing.AutoCreateMissing, + LegacyNameMapping: existing.LegacyNameMapping, + Mapping: newMapping, + } + + err = api.IDPSync.UpdateGroupSyncSettings(sysCtx, org.ID, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.GroupSyncSettings{ + Field: settings.Field, + RegexFilter: settings.RegexFilter, + AutoCreateMissing: settings.AutoCreateMissing, + LegacyNameMapping: settings.LegacyNameMapping, + Mapping: settings.Mapping, + }) +} + // @Summary Get role IdP Sync settings by organization // @ID get-role-idp-sync-settings-by-organization // @Security CoderSessionToken @@ -201,7 +349,7 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request } aReq.Old = *existing - err = api.IDPSync.UpdateRoleSettings(sysCtx, org.ID, api.Database, idpsync.RoleSyncSettings{ + err = api.IDPSync.UpdateRoleSyncSettings(sysCtx, org.ID, api.Database, idpsync.RoleSyncSettings{ Field: req.Field, Mapping: req.Mapping, }) @@ -223,6 +371,141 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request }) } +// @Summary Update role IdP Sync config +// @ID update-role-idp-sync-config +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.RoleSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchRoleIDPSyncConfigRequest true "New config values" +// @Router /organizations/{organization}/settings/idpsync/roles/config [patch] +func (api *API) patchRoleIDPSyncConfig(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.RoleSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchRoleIDPSyncConfigRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.RoleSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.RoleSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + settings = idpsync.RoleSyncSettings{ + Field: req.Field, + Mapping: existing.Mapping, + } + + err = api.IDPSync.UpdateRoleSyncSettings(sysCtx, org.ID, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.RoleSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + }) +} + +// @Summary Update role IdP Sync mapping +// @ID update-role-idp-sync-mapping +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.RoleSyncSettings +// @Param organization path string true "Organization ID or name" format(uuid) +// @Param request body codersdk.PatchRoleIDPSyncMappingRequest true "Description of the mappings to add and remove" +// @Router /organizations/{organization}/settings/idpsync/roles/mapping [patch] +func (api *API) patchRoleIDPSyncMapping(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.RoleSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchRoleIDPSyncMappingRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.RoleSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.RoleSyncSettings(sysCtx, org.ID, tx) + if err != nil { + return err + } + aReq.Old = *existing + + newMapping := applyIDPSyncMappingDiff(existing.Mapping, req.Add, req.Remove) + settings = idpsync.RoleSyncSettings{ + Field: existing.Field, + Mapping: newMapping, + } + + err = api.IDPSync.UpdateRoleSyncSettings(sysCtx, org.ID, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.RoleSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + }) +} + // @Summary Get organization IdP Sync settings // @ID get-organization-idp-sync-settings // @Security CoderSessionToken @@ -292,7 +575,7 @@ func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http } aReq.Old = *existing - err = api.IDPSync.UpdateOrganizationSettings(sysCtx, api.Database, idpsync.OrganizationSyncSettings{ + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, api.Database, idpsync.OrganizationSyncSettings{ Field: req.Field, // We do not check if the mappings point to actual organizations. Mapping: req.Mapping, @@ -317,6 +600,139 @@ func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http }) } +// @Summary Update organization IdP Sync config +// @ID update-organization-idp-sync-config +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.OrganizationSyncSettings +// @Param request body codersdk.PatchOrganizationIDPSyncConfigRequest true "New config values" +// @Router /settings/idpsync/organization/config [patch] +func (api *API) patchOrganizationIDPSyncConfig(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.OrganizationSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchOrganizationIDPSyncConfigRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.OrganizationSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.OrganizationSyncSettings(sysCtx, tx) + if err != nil { + return err + } + aReq.Old = *existing + + settings = idpsync.OrganizationSyncSettings{ + Field: req.Field, + AssignDefault: req.AssignDefault, + Mapping: existing.Mapping, + } + + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.OrganizationSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + AssignDefault: settings.AssignDefault, + }) +} + +// @Summary Update organization IdP Sync mapping +// @ID update-organization-idp-sync-mapping +// @Security CoderSessionToken +// @Produce json +// @Accept json +// @Tags Enterprise +// @Success 200 {object} codersdk.OrganizationSyncSettings +// @Param request body codersdk.PatchOrganizationIDPSyncMappingRequest true "Description of the mappings to add and remove" +// @Router /settings/idpsync/organization/mapping [patch] +func (api *API) patchOrganizationIDPSyncMapping(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.OrganizationSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + defer commitAudit() + + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings) { + httpapi.Forbidden(rw) + return + } + + var req codersdk.PatchOrganizationIDPSyncMappingRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var settings idpsync.OrganizationSyncSettings + //nolint:gocritic // Requires system context to update runtime config + sysCtx := dbauthz.AsSystemRestricted(ctx) + err := database.ReadModifyUpdate(api.Database, func(tx database.Store) error { + existing, err := api.IDPSync.OrganizationSyncSettings(sysCtx, tx) + if err != nil { + return err + } + aReq.Old = *existing + + newMapping := applyIDPSyncMappingDiff(existing.Mapping, req.Add, req.Remove) + settings = idpsync.OrganizationSyncSettings{ + Field: existing.Field, + Mapping: newMapping, + AssignDefault: existing.AssignDefault, + } + + err = api.IDPSync.UpdateOrganizationSyncSettings(sysCtx, tx, settings) + if err != nil { + return err + } + + return nil + }) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + aReq.New = settings + httpapi.Write(ctx, rw, http.StatusOK, codersdk.OrganizationSyncSettings{ + Field: settings.Field, + Mapping: settings.Mapping, + AssignDefault: settings.AssignDefault, + }) +} + // @Summary Get the available organization idp sync claim fields // @ID get-the-available-organization-idp-sync-claim-fields // @Security CoderSessionToken @@ -423,3 +839,31 @@ func (api *API) idpSyncClaimFieldValues(orgID uuid.UUID, rw http.ResponseWriter, httpapi.Write(ctx, rw, http.StatusOK, fieldValues) } + +func applyIDPSyncMappingDiff[IDType uuid.UUID | string]( + previous map[string][]IDType, + add, remove []codersdk.IDPSyncMapping[IDType], +) map[string][]IDType { + next := make(map[string][]IDType) + + // Copy existing mapping + for key, ids := range previous { + next[key] = append(next[key], ids...) + } + + // Add unique entries + for _, mapping := range add { + if !slice.Contains(next[mapping.Given], mapping.Gets) { + next[mapping.Given] = append(next[mapping.Given], mapping.Gets) + } + } + + // Remove entries + for _, mapping := range remove { + next[mapping.Given] = slices.DeleteFunc(next[mapping.Given], func(u IDType) bool { + return u == mapping.Gets + }) + } + + return next +} diff --git a/enterprise/coderd/idpsync_internal_test.go b/enterprise/coderd/idpsync_internal_test.go new file mode 100644 index 0000000000000..51db04e74b913 --- /dev/null +++ b/enterprise/coderd/idpsync_internal_test.go @@ -0,0 +1,117 @@ +package coderd + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" +) + +func TestApplyIDPSyncMappingDiff(t *testing.T) { + t.Parallel() + + t.Run("with UUIDs", func(t *testing.T) { + t.Parallel() + + id := []uuid.UUID{ + uuid.MustParse("00000000-b8bd-46bb-bb6c-6c2b2c0dd2ea"), + uuid.MustParse("01000000-fbe8-464c-9429-fe01a03f3644"), + uuid.MustParse("02000000-0926-407b-9998-39af62e3d0c5"), + uuid.MustParse("03000000-92f6-4bfd-bba6-0f54667b131c"), + } + + mapping := applyIDPSyncMappingDiff(map[string][]uuid.UUID{}, + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: id[0]}, + {Given: "wibble", Gets: id[1]}, + {Given: "wobble", Gets: id[0]}, + {Given: "wobble", Gets: id[1]}, + {Given: "wobble", Gets: id[2]}, + {Given: "wobble", Gets: id[3]}, + {Given: "wooble", Gets: id[0]}, + }, + // Remove takes priority over Add, so `3` should not actually be added. + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wobble", Gets: id[3]}, + }, + ) + + expected := map[string][]uuid.UUID{ + "wibble": {id[0], id[1]}, + "wobble": {id[0], id[1], id[2]}, + "wooble": {id[0]}, + } + + require.Equal(t, expected, mapping) + + mapping = applyIDPSyncMappingDiff(mapping, + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: id[2]}, + {Given: "wobble", Gets: id[3]}, + {Given: "wooble", Gets: id[0]}, + }, + []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: id[0]}, + {Given: "wobble", Gets: id[1]}, + }, + ) + + expected = map[string][]uuid.UUID{ + "wibble": {id[1], id[2]}, + "wobble": {id[0], id[2], id[3]}, + "wooble": {id[0]}, + } + + require.Equal(t, expected, mapping) + }) + + t.Run("with strings", func(t *testing.T) { + t.Parallel() + + mapping := applyIDPSyncMappingDiff(map[string][]string{}, + []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-00"}, + {Given: "wibble", Gets: "group-01"}, + {Given: "wobble", Gets: "group-00"}, + {Given: "wobble", Gets: "group-01"}, + {Given: "wobble", Gets: "group-02"}, + {Given: "wobble", Gets: "group-03"}, + {Given: "wooble", Gets: "group-00"}, + }, + // Remove takes priority over Add, so `3` should not actually be added. + []codersdk.IDPSyncMapping[string]{ + {Given: "wobble", Gets: "group-03"}, + }, + ) + + expected := map[string][]string{ + "wibble": {"group-00", "group-01"}, + "wobble": {"group-00", "group-01", "group-02"}, + "wooble": {"group-00"}, + } + + require.Equal(t, expected, mapping) + + mapping = applyIDPSyncMappingDiff(mapping, + []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-02"}, + {Given: "wobble", Gets: "group-03"}, + {Given: "wooble", Gets: "group-00"}, + }, + []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-00"}, + {Given: "wobble", Gets: "group-01"}, + }, + ) + + expected = map[string][]string{ + "wibble": {"group-01", "group-02"}, + "wobble": {"group-00", "group-02", "group-03"}, + "wooble": {"group-00"}, + } + + require.Equal(t, expected, mapping) + }) +} diff --git a/enterprise/coderd/idpsync_test.go b/enterprise/coderd/idpsync_test.go index 41a8db2dd0792..d34701c3f6936 100644 --- a/enterprise/coderd/idpsync_test.go +++ b/enterprise/coderd/idpsync_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" @@ -19,7 +20,7 @@ import ( "github.com/coder/serpent" ) -func TestGetGroupSyncConfig(t *testing.T) { +func TestGetGroupSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -82,7 +83,7 @@ func TestGetGroupSyncConfig(t *testing.T) { }) } -func TestPostGroupSyncConfig(t *testing.T) { +func TestPatchGroupSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -140,7 +141,172 @@ func TestPostGroupSyncConfig(t *testing.T) { }) } -func TestGetRoleSyncConfig(t *testing.T) { +func TestPatchGroupSyncConfig(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + mapping := map[string][]uuid.UUID{"wibble": {uuid.New()}} + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchGroupIDPSyncSettings(ctx, orgID.String(), codersdk.GroupSyncSettings{ + Field: "wibble", + RegexFilter: regexp.MustCompile("wib{2,}le"), + AutoCreateMissing: false, + Mapping: mapping, + }) + + require.NoError(t, err) + + fetchedSettings, err := orgAdmin.GroupIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, "wib{2,}le", fetchedSettings.RegexFilter.String()) + require.Equal(t, false, fetchedSettings.AutoCreateMissing) + require.Equal(t, mapping, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchGroupIDPSyncConfig(ctx, orgID.String(), codersdk.PatchGroupIDPSyncConfigRequest{ + Field: "wobble", + RegexFilter: regexp.MustCompile("wob{2,}le"), + AutoCreateMissing: true, + }) + + require.NoError(t, err) + require.Equal(t, "wobble", settings.Field) + require.Equal(t, "wob{2,}le", settings.RegexFilter.String()) + require.Equal(t, true, settings.AutoCreateMissing) + require.Equal(t, mapping, settings.Mapping) + + fetchedSettings, err = orgAdmin.GroupIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wobble", fetchedSettings.Field) + require.Equal(t, "wob{2,}le", fetchedSettings.RegexFilter.String()) + require.Equal(t, true, fetchedSettings.AutoCreateMissing) + require.Equal(t, mapping, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchGroupIDPSyncConfig(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncConfigRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchGroupSyncMapping(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + // These IDs are easier to visually diff if the test fails than truly random + // ones. + orgs := []uuid.UUID{ + uuid.MustParse("00000000-b8bd-46bb-bb6c-6c2b2c0dd2ea"), + uuid.MustParse("01000000-fbe8-464c-9429-fe01a03f3644"), + uuid.MustParse("02000000-0926-407b-9998-39af62e3d0c5"), + } + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchGroupIDPSyncSettings(ctx, orgID.String(), codersdk.GroupSyncSettings{ + Field: "wibble", + RegexFilter: regexp.MustCompile("wib{2,}le"), + AutoCreateMissing: true, + Mapping: map[string][]uuid.UUID{"wobble": {orgs[0]}}, + }) + require.NoError(t, err) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchGroupIDPSyncMapping(ctx, orgID.String(), codersdk.PatchGroupIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: orgs[0]}, + {Given: "wobble", Gets: orgs[1]}, + {Given: "wobble", Gets: orgs[2]}, + }, + // Remove takes priority over Add, so "3" should not actually be added to wooble. + Remove: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wobble", Gets: orgs[1]}, + }, + }) + + expected := map[string][]uuid.UUID{ + "wibble": {orgs[0]}, + "wobble": {orgs[0], orgs[2]}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err := orgAdmin.GroupIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, "wib{2,}le", fetchedSettings.RegexFilter.String()) + require.Equal(t, true, fetchedSettings.AutoCreateMissing) + require.Equal(t, expected, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchGroupIDPSyncMapping(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncMappingRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestGetRoleSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -174,7 +340,7 @@ func TestGetRoleSyncConfig(t *testing.T) { }) } -func TestPostRoleSyncConfig(t *testing.T) { +func TestPatchRoleSyncSettings(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { @@ -231,3 +397,381 @@ func TestPostRoleSyncConfig(t *testing.T) { require.Equal(t, http.StatusForbidden, apiError.StatusCode()) }) } + +func TestPatchRoleSyncConfig(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + mapping := map[string][]string{"wibble": {"group-01"}} + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, orgID.String(), codersdk.RoleSyncSettings{ + Field: "wibble", + Mapping: mapping, + }) + + require.NoError(t, err) + + fetchedSettings, err := orgAdmin.RoleIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, mapping, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchRoleIDPSyncConfig(ctx, orgID.String(), codersdk.PatchRoleIDPSyncConfigRequest{ + Field: "wobble", + }) + + require.NoError(t, err) + require.Equal(t, "wobble", settings.Field) + require.Equal(t, mapping, settings.Mapping) + + fetchedSettings, err = orgAdmin.RoleIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wobble", fetchedSettings.Field) + require.Equal(t, mapping, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchGroupIDPSyncConfig(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncConfigRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchRoleSyncMapping(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + orgID := user.OrganizationID + orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAdmin(user.OrganizationID)) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := orgAdmin.PatchRoleIDPSyncSettings(ctx, orgID.String(), codersdk.RoleSyncSettings{ + Field: "wibble", + Mapping: map[string][]string{"wobble": {"group-00"}}, + }) + require.NoError(t, err) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := orgAdmin.PatchRoleIDPSyncMapping(ctx, orgID.String(), codersdk.PatchRoleIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[string]{ + {Given: "wibble", Gets: "group-00"}, + {Given: "wobble", Gets: "group-01"}, + {Given: "wobble", Gets: "group-02"}, + }, + // Remove takes priority over Add, so "3" should not actually be added to wooble. + Remove: []codersdk.IDPSyncMapping[string]{ + {Given: "wobble", Gets: "group-01"}, + }, + }) + + expected := map[string][]string{ + "wibble": {"group-00"}, + "wobble": {"group-00", "group-02"}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err := orgAdmin.RoleIDPSyncSettings(ctx, orgID.String()) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, expected, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchGroupIDPSyncMapping(ctx, user.OrganizationID.String(), codersdk.PatchGroupIDPSyncMappingRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestGetOrganizationSyncSettings(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, _, _, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + expected := map[string][]uuid.UUID{"foo": {user.OrganizationID}} + + ctx := testutil.Context(t, testutil.WaitShort) + settings, err := owner.PatchOrganizationIDPSyncSettings(ctx, codersdk.OrganizationSyncSettings{ + Field: "august", + Mapping: expected, + }) + + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + require.Equal(t, expected, settings.Mapping) + + settings, err = owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + require.Equal(t, expected, settings.Mapping) + }) +} + +func TestPatchOrganizationSyncSettings(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, _ := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // Only owners can change Organization IdP sync settings + settings, err := owner.PatchOrganizationIDPSyncSettings(ctx, codersdk.OrganizationSyncSettings{ + Field: "august", + }) + require.NoError(t, err) + require.Equal(t, "august", settings.Field) + + fetchedSettings, err := owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "august", fetchedSettings.Field) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchRoleIDPSyncSettings(ctx, user.OrganizationID.String(), codersdk.RoleSyncSettings{ + Field: "august", + }) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + + _, err = member.RoleIDPSyncSettings(ctx, user.OrganizationID.String()) + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchOrganizationSyncConfig(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + mapping := map[string][]uuid.UUID{"wibble": {user.OrganizationID}} + + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // Only owners can change Organization IdP sync settings + _, err := owner.PatchOrganizationIDPSyncSettings(ctx, codersdk.OrganizationSyncSettings{ + Field: "wibble", + AssignDefault: true, + Mapping: mapping, + }) + + require.NoError(t, err) + + fetchedSettings, err := owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "wibble", fetchedSettings.Field) + require.Equal(t, true, fetchedSettings.AssignDefault) + require.Equal(t, mapping, fetchedSettings.Mapping) + + ctx = testutil.Context(t, testutil.WaitShort) + settings, err := owner.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{ + Field: "wobble", + }) + + require.NoError(t, err) + require.Equal(t, "wobble", settings.Field) + require.Equal(t, false, settings.AssignDefault) + require.Equal(t, mapping, settings.Mapping) + + fetchedSettings, err = owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, "wobble", fetchedSettings.Field) + require.Equal(t, false, fetchedSettings.AssignDefault) + require.Equal(t, mapping, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} + +func TestPatchOrganizationSyncMapping(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + owner, _ := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + // These IDs are easier to visually diff if the test fails than truly random + // ones. + orgs := []uuid.UUID{ + uuid.MustParse("00000000-b8bd-46bb-bb6c-6c2b2c0dd2ea"), + uuid.MustParse("01000000-fbe8-464c-9429-fe01a03f3644"), + uuid.MustParse("02000000-0926-407b-9998-39af62e3d0c5"), + } + + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // Only owners can change Organization IdP sync settings + settings, err := owner.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{ + Add: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wibble", Gets: orgs[0]}, + {Given: "wobble", Gets: orgs[0]}, + {Given: "wobble", Gets: orgs[1]}, + {Given: "wobble", Gets: orgs[2]}, + }, + Remove: []codersdk.IDPSyncMapping[uuid.UUID]{ + {Given: "wobble", Gets: orgs[1]}, + }, + }) + + expected := map[string][]uuid.UUID{ + "wibble": {orgs[0]}, + "wobble": {orgs[0], orgs[2]}, + } + + require.NoError(t, err) + require.Equal(t, expected, settings.Mapping) + + fetchedSettings, err := owner.OrganizationIDPSyncSettings(ctx) + require.NoError(t, err) + require.Equal(t, expected, fetchedSettings.Mapping) + }) + + t.Run("NotAuthorized", func(t *testing.T) { + t.Parallel() + + owner, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + + member, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID) + + ctx := testutil.Context(t, testutil.WaitShort) + _, err := member.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{}) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusForbidden, apiError.StatusCode()) + }) +} diff --git a/go.mod b/go.mod index a1f8b6b7a7ac8..a21bd05fada74 100644 --- a/go.mod +++ b/go.mod @@ -88,7 +88,7 @@ require ( github.com/chromedp/chromedp v0.11.0 github.com/cli/safeexec v1.0.1 github.com/coder/flog v1.1.0 - github.com/coder/guts v1.0.0 + github.com/coder/guts v1.0.1 github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 github.com/coder/quartz v0.1.2 github.com/coder/retry v1.5.1 diff --git a/go.sum b/go.sum index 1737a0ea1e0b1..2f14b991fee23 100644 --- a/go.sum +++ b/go.sum @@ -226,8 +226,8 @@ github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322 h1:m0lPZjlQ7vdVp github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322/go.mod h1:rOLFDDVKVFiDqZFXoteXc97YXx7kFi9kYqR+2ETPkLQ= github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136 h1:0RgB61LcNs24WOxc3PBvygSNTQurm0PYPujJjLLOzs0= github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136/go.mod h1:VkD1P761nykiq75dz+4iFqIQIZka189tx1BQLOp0Skc= -github.com/coder/guts v1.0.0 h1:Ba6TBOeED+96Dv8IdISjbGhCzHKicqSc4SEYVV+4zeE= -github.com/coder/guts v1.0.0/go.mod h1:SfmxjDaSfPjzKJ9mGU4sA/1OHU+u66uRfhFF+y4BARQ= +github.com/coder/guts v1.0.1 h1:tU9pW+1jftCSX1eBxnNHiouQBSBJIej3I+kqfjIyeJU= +github.com/coder/guts v1.0.1/go.mod h1:z8LHbF6vwDOXQOReDvay7Rpwp/jHwCZiZwjd6wfLcJg= github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048 h1:3jzYUlGH7ZELIH4XggXhnTnP05FCYiAFeQpoN+gNR5I= github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= diff --git a/install.sh b/install.sh index 734fd3c44f320..931426c54c5db 100755 --- a/install.sh +++ b/install.sh @@ -273,7 +273,7 @@ EOF main() { MAINLINE=1 STABLE=0 - TERRAFORM_VERSION="1.9.8" + TERRAFORM_VERSION="1.10.5" if [ "${TRACE-}" ]; then set -x diff --git a/provisioner/terraform/install.go b/provisioner/terraform/install.go index 7f6474d022ba1..74229c8539bc0 100644 --- a/provisioner/terraform/install.go +++ b/provisioner/terraform/install.go @@ -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.9.8")) + TerraformVersion = version.Must(version.NewVersion("1.10.5")) minTerraformVersion = version.Must(version.NewVersion("1.1.0")) - maxTerraformVersion = version.Must(version.NewVersion("1.9.9")) // use .9 to automatically allow patch releases + maxTerraformVersion = version.Must(version.NewVersion("1.10.9")) // use .9 to automatically allow patch releases terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.") ) diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tf b/provisioner/terraform/testdata/calling-module/calling-module.tf index 14777169d9994..33fcbb3f1984f 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tf +++ b/provisioner/terraform/testdata/calling-module/calling-module.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json index 30bc360bb1940..31faf235810fa 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -90,16 +87,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -177,7 +171,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "module.module:null": { "name": "null", @@ -201,7 +195,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 } ], "module_calls": { @@ -260,7 +254,7 @@ ] } ], - "timestamp": "2024-10-28T20:07:49Z", + "timestamp": "2025-03-04T19:25:00Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json index 5ead2c6ace0d5..5a6b65b4d08ce 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "04d66dc4-e25a-4f65-af6f-a9af6b907430", + "id": "8632f695-0881-4df5-999c-ff105e2a62a4", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "10fbd765-b0cc-4d6f-b5de-e5a036b2cb4b", + "startup_script_behavior": "non-blocking", + "token": "18782be6-3080-42a8-bec4-b2e0cb4caf93", "troubleshooting_url": null }, "sensitive_values": { @@ -69,7 +66,7 @@ "outputs": { "script": "" }, - "random": "7917595776755902204" + "random": "735568859568633344" }, "sensitive_values": { "inputs": {}, @@ -84,7 +81,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2669991968036854745", + "id": "280446487996139212", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf index 3f210452dfee0..6ad44a62de986 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json index 38af6827019e7..b143b45e90fce 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -80,16 +77,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -155,7 +149,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -178,7 +172,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.a", @@ -205,7 +199,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:50Z", + "timestamp": "2025-03-04T19:25:02Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json index 0cee8567db250..7681bf7d6b50c 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "bcf4bae1-0870-48e9-8bb4-af2f652c4d54", + "id": "c58b518a-428d-44b2-bfd7-1ac17188c528", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "afe98f25-25a2-4892-b921-be04bcd71efc", + "startup_script_behavior": "non-blocking", + "token": "085fd3ad-9462-4f9c-8f0f-05941d6cbc90", "troubleshooting_url": null }, "sensitive_values": { @@ -57,7 +54,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "6598177855275264799", + "id": "2593580341963886034", "triggers": null }, "sensitive_values": {}, @@ -74,7 +71,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4663187895457986148", + "id": "8775084967398626100", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf index 8c7b200fca7b0..86585b6a85357 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json index 3fe9f6c41fa9b..ba67f290022c0 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -80,16 +77,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -155,7 +149,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -178,7 +172,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.first", @@ -205,7 +199,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:52Z", + "timestamp": "2025-03-04T19:25:04Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json index ffd0690db2263..fdc0f8b1d062a 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "d047c7b6-b69e-4029-ab82-67468a0364f7", + "id": "bdf2fe69-0a3d-4aac-80c9-0896b0362d81", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "ceff37e3-52b9-4c80-af1b-1f9f99184590", + "startup_script_behavior": "non-blocking", + "token": "1428ac88-6dd9-4520-9c5c-0946fec8466b", "troubleshooting_url": null }, "sensitive_values": { @@ -57,7 +54,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3120105803817695206", + "id": "3464200430566318947", "triggers": null }, "sensitive_values": {}, @@ -73,7 +70,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2942451035046396496", + "id": "4854441548409483963", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tf b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tf index 494e0acafb48f..155b81889540e 100644 --- a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tf +++ b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfplan.json b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfplan.json index 598d6f1735a84..00e14d332bcf7 100644 --- a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfplan.json +++ b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,16 +26,13 @@ } ], "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -88,16 +85,13 @@ } ], "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -146,7 +140,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -188,7 +182,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -204,7 +198,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:55Z", + "timestamp": "2025-03-04T19:25:06Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfstate.json b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfstate.json index 7e9bdad7a02bb..b85deea275dae 100644 --- a/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfstate.json +++ b/provisioner/terraform/testdata/display-apps-disabled/display-apps-disabled.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "6ba13739-4a9c-456f-90cf-feba8f194853", + "id": "a443e5e2-d59e-456d-95ff-b15685a37ebd", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "6e348a4c-ef00-40ab-9732-817fb828045c", + "startup_script_behavior": "non-blocking", + "token": "5407b786-d16b-4e64-abfa-75bc641fa6c3", "troubleshooting_url": null }, "sensitive_values": { @@ -57,7 +54,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3123606937441446452", + "id": "1848001870879012103", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/display-apps/display-apps.tf b/provisioner/terraform/testdata/display-apps/display-apps.tf index a36b68cd3b1cc..3544ab535ad2f 100644 --- a/provisioner/terraform/testdata/display-apps/display-apps.tf +++ b/provisioner/terraform/testdata/display-apps/display-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/display-apps/display-apps.tfplan.json b/provisioner/terraform/testdata/display-apps/display-apps.tfplan.json index 3331a8f282c2b..e79b567562e7c 100644 --- a/provisioner/terraform/testdata/display-apps/display-apps.tfplan.json +++ b/provisioner/terraform/testdata/display-apps/display-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,16 +26,13 @@ } ], "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -88,16 +85,13 @@ } ], "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -146,7 +140,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -188,7 +182,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -204,7 +198,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:54Z", + "timestamp": "2025-03-04T19:25:08Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/display-apps/display-apps.tfstate.json b/provisioner/terraform/testdata/display-apps/display-apps.tfstate.json index 2b04222e751f2..6cbccd0b8ea8a 100644 --- a/provisioner/terraform/testdata/display-apps/display-apps.tfstate.json +++ b/provisioner/terraform/testdata/display-apps/display-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "b7e8dd7a-34aa-41e2-977e-e38577ab2476", + "id": "7399a566-8666-4a7b-a916-5043ea8b5a39", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "c6aeeb35-2766-4524-9818-687f7687831d", + "startup_script_behavior": "non-blocking", + "token": "47650163-1c1b-431b-81d6-42991663f53b", "troubleshooting_url": null }, "sensitive_values": { @@ -57,7 +54,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2407243137316459395", + "id": "1536600762010500828", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tf b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tf index 0b68bbe5710fe..5f45a88aacb6a 100644 --- a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tf +++ b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfplan.json b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfplan.json index 5ba9e7b6af80f..47f3624b64d70 100644 --- a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfplan.json +++ b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -68,16 +65,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -119,7 +113,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -160,7 +154,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -183,7 +177,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -228,7 +222,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:57Z", + "timestamp": "2025-03-04T19:25:10Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfstate.json b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfstate.json index 875d8c9aaf439..6d23cab247b9d 100644 --- a/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfstate.json +++ b/provisioner/terraform/testdata/external-auth-providers/external-auth-providers.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -38,7 +38,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -54,19 +54,16 @@ } ], "env": null, - "id": "ec5d36c9-8690-4246-8ab3-2d85a3eacee6", + "id": "3af7f86b-3674-4e9d-b92d-d2e8890e5c38", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "78c55fa2-8e3c-4564-950d-e022c76cf05a", + "startup_script_behavior": "non-blocking", + "token": "cf973ff9-17e3-4e08-abc5-7a37d3f74d0f", "troubleshooting_url": null }, "sensitive_values": { @@ -85,7 +82,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "455343782636271645", + "id": "7283290279914631370", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.json b/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.json index fba34f1cb5f4d..adf7f32ffa4d6 100644 --- a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.json +++ b/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -119,7 +119,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -223,7 +223,7 @@ ] } }, - "timestamp": "2024-10-28T20:07:58Z", + "timestamp": "2025-03-04T19:25:12Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json b/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json index 3cf905c0a2948..5b7393c568a5a 100644 --- a/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json +++ b/provisioner/terraform/testdata/git-auth-providers/git-auth-providers.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -52,7 +52,7 @@ } ], "env": null, - "id": "ffa1f524-0350-4891-868d-93cad369318a", + "id": "1023a3a5-f8c1-45f6-a0cc-bf3a1e1f3c63", "init_script": "", "login_before_ready": true, "metadata": [], @@ -64,7 +64,7 @@ "startup_script": null, "startup_script_behavior": null, "startup_script_timeout": 300, - "token": "8ba649af-b498-4f20-8055-b6a0b995837e", + "token": "a59f2270-ac62-47bc-9c68-7bbfe2981c2e", "troubleshooting_url": null }, "sensitive_values": { @@ -83,7 +83,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "7420557451345159984", + "id": "3284204005710492153", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tf b/provisioner/terraform/testdata/instance-id/instance-id.tf index 1cd4ab828b4f0..84e010a79d6e9 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tf +++ b/provisioner/terraform/testdata/instance-id/instance-id.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json index 527a2fa05769d..e287a5b6ab582 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "google-instance-identity", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -80,16 +77,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -156,7 +150,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -182,7 +176,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent_instance.main", @@ -225,7 +219,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:00Z", + "timestamp": "2025-03-04T19:25:13Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json index 929d72365502c..843ad66bc0a57 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "google-instance-identity", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "0389c8a5-cc5c-485d-959c-8738bada65ff", + "id": "9d3d75ba-4c12-42e5-bb49-62c258096d79", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "097b6128-8d60-4849-969b-03f0b463ac2c", + "startup_script_behavior": "non-blocking", + "token": "de14eb76-34b5-4da6-b40c-6c44aaeed2e1", "troubleshooting_url": null }, "sensitive_values": { @@ -57,8 +54,8 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "0389c8a5-cc5c-485d-959c-8738bada65ff", - "id": "0ae6bb98-871c-4091-8098-d32f256d8c05", + "agent_id": "9d3d75ba-4c12-42e5-bb49-62c258096d79", + "id": "2561b9a8-27a4-420f-aaf5-7b1641b2f7a6", "instance_id": "example" }, "sensitive_values": {}, @@ -74,7 +71,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5569763710827889183", + "id": "3284638190727292821", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/kubernetes-metadata/kubernetes-metadata.tf b/provisioner/terraform/testdata/kubernetes-metadata/kubernetes-metadata.tf index 2ae1298904fbb..faa08706de380 100644 --- a/provisioner/terraform/testdata/kubernetes-metadata/kubernetes-metadata.tf +++ b/provisioner/terraform/testdata/kubernetes-metadata/kubernetes-metadata.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } kubernetes = { source = "hashicorp/kubernetes" diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf index 1e13495d6ebc7..7664ead2b4962 100644 --- a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json index 2151b4631647a..2fe0bf0425712 100644 --- a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -42,16 +39,16 @@ "name": "apps", "index": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": "app1", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -68,16 +65,16 @@ "name": "apps", "index": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": "app2", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": null, @@ -120,16 +117,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -164,10 +158,10 @@ "display_name": "app1", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -201,10 +195,10 @@ "display_name": "app2", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": null, @@ -248,7 +242,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -271,7 +265,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.apps", @@ -298,7 +292,7 @@ ] } }, - "schema_version": 0, + "schema_version": 1, "for_each_expression": { "references": [ "local.apps_map" @@ -327,7 +321,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:02Z", + "timestamp": "2025-03-04T19:25:15Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json index 9aaa7b352f518..1483341440ff5 100644 --- a/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json +++ b/provisioner/terraform/testdata/mapped-apps/mapped-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "b3d3e1d7-1f1f-4abf-8475-2058f73f3437", + "id": "0faf2a12-1797-43f9-8ad0-46e085d9810e", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "56420fd5-57e5-44e0-a264-53395b74505a", + "startup_script_behavior": "non-blocking", + "token": "5f5cf1c0-37d4-4809-a227-92bd64fcfb64", "troubleshooting_url": null }, "sensitive_values": { @@ -56,18 +53,18 @@ "name": "apps", "index": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3d3e1d7-1f1f-4abf-8475-2058f73f3437", + "agent_id": "0faf2a12-1797-43f9-8ad0-46e085d9810e", "command": null, "display_name": "app1", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "e8163eb0-e56e-46e7-8848-8c6c250ce5b9", - "name": null, + "id": "ac8a8769-9b4d-4028-82b5-cce53b952fb8", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -87,18 +84,18 @@ "name": "apps", "index": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3d3e1d7-1f1f-4abf-8475-2058f73f3437", + "agent_id": "0faf2a12-1797-43f9-8ad0-46e085d9810e", "command": null, "display_name": "app2", "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "0971e625-7a23-4108-9765-78f7ad045b38", - "name": null, + "id": "403e8671-2ab1-4a16-b578-6436e4090844", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": null, @@ -119,7 +116,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "60927265551659604", + "id": "198476735862845239", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tf b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tf index 02c6ff6c1b67f..8ac412b5b3894 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tf +++ b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfplan.json b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfplan.json index d8f5a4763518b..94b81267d85b6 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -41,23 +38,20 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -72,16 +66,16 @@ "type": "coder_app", "name": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -97,7 +91,7 @@ "type": "coder_app", "name": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, @@ -109,10 +103,10 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -130,16 +124,16 @@ "type": "coder_app", "name": "app3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -194,16 +188,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -238,16 +229,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -281,10 +269,10 @@ "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -323,10 +311,10 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -363,10 +351,10 @@ "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -431,7 +419,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -454,7 +442,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent.dev2", @@ -470,7 +458,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app1", @@ -489,7 +477,7 @@ "constant_value": "app1" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app2", @@ -524,7 +512,7 @@ "constant_value": true } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app3", @@ -546,7 +534,7 @@ "constant_value": false } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev1", @@ -587,7 +575,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:05Z", + "timestamp": "2025-03-04T19:25:17Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfstate.json b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfstate.json index 4a94e05baa29d..12ae6f84b2c08 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "571523c7-e7a3-420a-b65d-39d15f5f3267", + "id": "9cd3c7d2-75fb-4ad0-acba-40ae78434154", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "c18d762d-062d-43d4-b7c2-98be546b39a6", + "startup_script_behavior": "non-blocking", + "token": "fcf46094-a739-4acc-9d64-7b40b2fbdc5c", "troubleshooting_url": null }, "sensitive_values": { @@ -55,7 +52,7 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -71,19 +68,16 @@ } ], "env": null, - "id": "e94994f2-cab5-4288-8ff3-a290c95e4e25", + "id": "37b0b6fd-6f9e-4ad4-a273-6c4334d4bc02", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "c0757e3a-4be4-4643-b3ba-b27234169eb1", + "startup_script_behavior": "non-blocking", + "token": "ed7abd75-cdf7-4b4f-b092-3288cae29f80", "troubleshooting_url": null }, "sensitive_values": { @@ -100,18 +94,18 @@ "type": "coder_app", "name": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "571523c7-e7a3-420a-b65d-39d15f5f3267", + "agent_id": "9cd3c7d2-75fb-4ad0-acba-40ae78434154", "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "bf2b3c44-1b1d-49c5-9149-4f2f18590c60", - "name": null, + "id": "c267f275-9d8b-40da-a78b-cc3ad0cc4e94", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -130,9 +124,9 @@ "type": "coder_app", "name": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "571523c7-e7a3-420a-b65d-39d15f5f3267", + "agent_id": "9cd3c7d2-75fb-4ad0-acba-40ae78434154", "command": null, "display_name": null, "external": false, @@ -143,11 +137,11 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "id": "580cf864-a64d-4430-98b7-fa37c44083f8", - "name": null, + "id": "dd509891-e05b-4f3c-b860-bd240b4771f8", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -168,18 +162,18 @@ "type": "coder_app", "name": "app3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "e94994f2-cab5-4288-8ff3-a290c95e4e25", + "agent_id": "37b0b6fd-6f9e-4ad4-a273-6c4334d4bc02", "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "182dca7b-12ab-4c58-9424-23b7d61135a9", - "name": null, + "id": "7b2bf8ad-c1ec-445d-acad-cfd046d6b950", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -200,7 +194,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3778543820798621894", + "id": "8605362455735888550", "triggers": null }, "sensitive_values": {}, @@ -216,7 +210,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "1094622314762410115", + "id": "2566554723161358367", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tf b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tf index d167d44942776..e12a895d14baa 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tf +++ b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfplan.json b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfplan.json index 4cb28ae592516..96a20f87f89d9 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -41,23 +38,20 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -72,7 +66,7 @@ "type": "coder_env", "name": "env1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "name": "ENV_1", "value": "Env 1" @@ -85,7 +79,7 @@ "type": "coder_env", "name": "env2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "name": "ENV_2", "value": "Env 2" @@ -98,7 +92,7 @@ "type": "coder_env", "name": "env3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "name": "ENV_3", "value": "Env 3" @@ -150,16 +144,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -194,16 +185,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -338,7 +326,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -361,7 +349,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent.dev2", @@ -377,7 +365,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_env.env1", @@ -399,7 +387,7 @@ "constant_value": "Env 1" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_env.env2", @@ -421,7 +409,7 @@ "constant_value": "Env 2" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_env.env3", @@ -443,7 +431,7 @@ "constant_value": "Env 3" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev1", @@ -484,7 +472,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:06Z", + "timestamp": "2025-03-04T19:25:19Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfstate.json b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfstate.json index f87b6f0a9eb56..64ed4878e2213 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-envs/multiple-agents-multiple-envs.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "702e7cd2-95a0-46cf-8ef7-c1dfbd3e56b9", + "id": "a1aa25b4-5c7d-44f6-95f8-2f32205f9625", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "1cfd79e3-3f9c-4d66-b7c2-42c385c26012", + "startup_script_behavior": "non-blocking", + "token": "274d380f-9d26-4cc4-8122-0591bef43b4e", "troubleshooting_url": null }, "sensitive_values": { @@ -55,7 +52,7 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -71,19 +68,16 @@ } ], "env": null, - "id": "ca137ba9-45ce-44ff-8e30-59a86565fa7d", + "id": "6e515c26-ca96-46ca-a220-83f17b2fcd95", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "0d3aa4f8-025c-4044-8053-d077484355fb", + "startup_script_behavior": "non-blocking", + "token": "794c95b3-3404-4f0e-a7a7-278f8bbfb5f0", "troubleshooting_url": null }, "sensitive_values": { @@ -100,10 +94,10 @@ "type": "coder_env", "name": "env1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "702e7cd2-95a0-46cf-8ef7-c1dfbd3e56b9", - "id": "e3d37294-2407-4286-a519-7551b901ba54", + "agent_id": "a1aa25b4-5c7d-44f6-95f8-2f32205f9625", + "id": "bce78af7-55f1-4868-8d1a-7aac6ea85090", "name": "ENV_1", "value": "Env 1" }, @@ -118,10 +112,10 @@ "type": "coder_env", "name": "env2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "702e7cd2-95a0-46cf-8ef7-c1dfbd3e56b9", - "id": "9451575b-da89-4297-a42d-4aaf0a23775d", + "agent_id": "a1aa25b4-5c7d-44f6-95f8-2f32205f9625", + "id": "33a6c102-c871-46ce-a78b-32c518d0b387", "name": "ENV_2", "value": "Env 2" }, @@ -136,10 +130,10 @@ "type": "coder_env", "name": "env3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "ca137ba9-45ce-44ff-8e30-59a86565fa7d", - "id": "948e3fb5-12a1-454b-b85e-d4dc1f01838f", + "agent_id": "6e515c26-ca96-46ca-a220-83f17b2fcd95", + "id": "3e746cf3-5a3f-446a-9643-4dffb344009c", "name": "ENV_3", "value": "Env 3" }, @@ -156,7 +150,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "7502424400840788651", + "id": "8646180706267246123", "triggers": null }, "sensitive_values": {}, @@ -172,7 +166,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "3916143681500058654", + "id": "3657189128073189353", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tf b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tf new file mode 100644 index 0000000000000..f86ceb180edb5 --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tf @@ -0,0 +1,67 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.2.0-pre0" + } + } +} + +resource "coder_agent" "dev1" { + os = "linux" + arch = "amd64" + resources_monitoring { + memory { + enabled = true + threshold = 80 + } + } +} + +resource "coder_agent" "dev2" { + os = "linux" + arch = "amd64" + resources_monitoring { + memory { + enabled = true + threshold = 99 + } + volume { + path = "/volume1" + enabled = true + threshold = 80 + } + volume { + path = "/volume2" + enabled = false + threshold = 50 + } + } +} + +# app1 is for testing subdomain default. +resource "coder_app" "app1" { + agent_id = coder_agent.dev1.id + slug = "app1" + # subdomain should default to false. + # subdomain = false +} + +# app2 tests that subdomaincan be true, and that healthchecks work. +resource "coder_app" "app2" { + agent_id = coder_agent.dev1.id + slug = "app2" + subdomain = true + healthcheck { + url = "http://localhost:13337/healthz" + interval = 5 + threshold = 6 + } +} + +resource "null_resource" "dev" { + depends_on = [ + coder_agent.dev1, + coder_agent.dev2 + ] +} diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfplan.json b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfplan.json new file mode 100644 index 0000000000000..5fb45060be911 --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfplan.json @@ -0,0 +1,625 @@ +{ + "format_version": "1.2", + "terraform_version": "1.10.5", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 80 + } + ], + "volume": [] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [] + } + ], + "token": true + } + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 99 + } + ], + "volume": [ + { + "enabled": false, + "path": "/volume2", + "threshold": 50 + }, + { + "enabled": true, + "path": "/volume1", + "threshold": 80 + } + ] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [ + {}, + {} + ] + } + ], + "token": true + } + }, + { + "address": "coder_app.app1", + "mode": "managed", + "type": "coder_app", + "name": "app1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "command": null, + "display_name": null, + "external": false, + "healthcheck": [], + "hidden": false, + "icon": null, + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app1", + "subdomain": null, + "url": null + }, + "sensitive_values": { + "healthcheck": [] + } + }, + { + "address": "coder_app.app2", + "mode": "managed", + "type": "coder_app", + "name": "app2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "command": null, + "display_name": null, + "external": false, + "healthcheck": [ + { + "interval": 5, + "threshold": 6, + "url": "http://localhost:13337/healthz" + } + ], + "hidden": false, + "icon": null, + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app2", + "subdomain": true, + "url": null + }, + "sensitive_values": { + "healthcheck": [ + {} + ] + } + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "triggers": null + }, + "sensitive_values": {} + } + ] + } + }, + "resource_changes": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 80 + } + ], + "volume": [] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "after_unknown": { + "display_apps": true, + "id": true, + "init_script": true, + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [] + } + ], + "token": true + }, + "before_sensitive": false, + "after_sensitive": { + "display_apps": [], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [] + } + ], + "token": true + } + } + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "env": null, + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 99 + } + ], + "volume": [ + { + "enabled": false, + "path": "/volume2", + "threshold": 50 + }, + { + "enabled": true, + "path": "/volume1", + "threshold": 80 + } + ] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "troubleshooting_url": null + }, + "after_unknown": { + "display_apps": true, + "id": true, + "init_script": true, + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [ + {}, + {} + ] + } + ], + "token": true + }, + "before_sensitive": false, + "after_sensitive": { + "display_apps": [], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [ + {}, + {} + ] + } + ], + "token": true + } + } + }, + { + "address": "coder_app.app1", + "mode": "managed", + "type": "coder_app", + "name": "app1", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "command": null, + "display_name": null, + "external": false, + "healthcheck": [], + "hidden": false, + "icon": null, + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app1", + "subdomain": null, + "url": null + }, + "after_unknown": { + "agent_id": true, + "healthcheck": [], + "id": true + }, + "before_sensitive": false, + "after_sensitive": { + "healthcheck": [] + } + } + }, + { + "address": "coder_app.app2", + "mode": "managed", + "type": "coder_app", + "name": "app2", + "provider_name": "registry.terraform.io/coder/coder", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "command": null, + "display_name": null, + "external": false, + "healthcheck": [ + { + "interval": 5, + "threshold": 6, + "url": "http://localhost:13337/healthz" + } + ], + "hidden": false, + "icon": null, + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app2", + "subdomain": true, + "url": null + }, + "after_unknown": { + "agent_id": true, + "healthcheck": [ + {} + ], + "id": true + }, + "before_sensitive": false, + "after_sensitive": { + "healthcheck": [ + {} + ] + } + } + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "configuration": { + "provider_config": { + "coder": { + "name": "coder", + "full_name": "registry.terraform.io/coder/coder", + "version_constraint": "2.2.0-pre0" + }, + "null": { + "name": "null", + "full_name": "registry.terraform.io/hashicorp/null" + } + }, + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "amd64" + }, + "os": { + "constant_value": "linux" + }, + "resources_monitoring": [ + { + "memory": [ + { + "enabled": { + "constant_value": true + }, + "threshold": { + "constant_value": 80 + } + } + ] + } + ] + }, + "schema_version": 1 + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_config_key": "coder", + "expressions": { + "arch": { + "constant_value": "amd64" + }, + "os": { + "constant_value": "linux" + }, + "resources_monitoring": [ + { + "memory": [ + { + "enabled": { + "constant_value": true + }, + "threshold": { + "constant_value": 99 + } + } + ], + "volume": [ + { + "enabled": { + "constant_value": true + }, + "path": { + "constant_value": "/volume1" + }, + "threshold": { + "constant_value": 80 + } + }, + { + "enabled": { + "constant_value": false + }, + "path": { + "constant_value": "/volume2" + }, + "threshold": { + "constant_value": 50 + } + } + ] + } + ] + }, + "schema_version": 1 + }, + { + "address": "coder_app.app1", + "mode": "managed", + "type": "coder_app", + "name": "app1", + "provider_config_key": "coder", + "expressions": { + "agent_id": { + "references": [ + "coder_agent.dev1.id", + "coder_agent.dev1" + ] + }, + "slug": { + "constant_value": "app1" + } + }, + "schema_version": 1 + }, + { + "address": "coder_app.app2", + "mode": "managed", + "type": "coder_app", + "name": "app2", + "provider_config_key": "coder", + "expressions": { + "agent_id": { + "references": [ + "coder_agent.dev1.id", + "coder_agent.dev1" + ] + }, + "healthcheck": [ + { + "interval": { + "constant_value": 5 + }, + "threshold": { + "constant_value": 6 + }, + "url": { + "constant_value": "http://localhost:13337/healthz" + } + } + ], + "slug": { + "constant_value": "app2" + }, + "subdomain": { + "constant_value": true + } + }, + "schema_version": 1 + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_config_key": "null", + "schema_version": 0, + "depends_on": [ + "coder_agent.dev1", + "coder_agent.dev2" + ] + } + ] + } + }, + "relevant_attributes": [ + { + "resource": "coder_agent.dev1", + "attribute": [ + "id" + ] + } + ], + "timestamp": "2025-03-04T19:25:21Z", + "applyable": true, + "complete": true, + "errored": false +} diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfstate.json b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfstate.json new file mode 100644 index 0000000000000..fa19d308dff44 --- /dev/null +++ b/provisioner/terraform/testdata/multiple-agents-multiple-monitors/multiple-agents-multiple-monitors.tfstate.json @@ -0,0 +1,231 @@ +{ + "format_version": "1.0", + "terraform_version": "1.10.5", + "values": { + "root_module": { + "resources": [ + { + "address": "coder_agent.dev1", + "mode": "managed", + "type": "coder_agent", + "name": "dev1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "display_apps": [ + { + "port_forwarding_helper": true, + "ssh_helper": true, + "vscode": true, + "vscode_insiders": false, + "web_terminal": true + } + ], + "env": null, + "id": "baa57f58-60fb-4490-9f11-1f7e17ae698a", + "init_script": "", + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 80 + } + ], + "volume": [] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "token": "6a54f7c6-93b5-4d2f-a194-7b6f968b9293", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [ + {} + ], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [] + } + ], + "token": true + } + }, + { + "address": "coder_agent.dev2", + "mode": "managed", + "type": "coder_agent", + "name": "dev2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "arch": "amd64", + "auth": "token", + "connection_timeout": 120, + "dir": null, + "display_apps": [ + { + "port_forwarding_helper": true, + "ssh_helper": true, + "vscode": true, + "vscode_insiders": false, + "web_terminal": true + } + ], + "env": null, + "id": "60fa4e03-a7b1-47fc-9977-7cf43bb04f1f", + "init_script": "", + "metadata": [], + "motd_file": null, + "order": null, + "os": "linux", + "resources_monitoring": [ + { + "memory": [ + { + "enabled": true, + "threshold": 99 + } + ], + "volume": [ + { + "enabled": false, + "path": "/volume2", + "threshold": 50 + }, + { + "enabled": true, + "path": "/volume1", + "threshold": 80 + } + ] + } + ], + "shutdown_script": null, + "startup_script": null, + "startup_script_behavior": "non-blocking", + "token": "753eb1c8-e032-4d9c-baea-4534040f68d9", + "troubleshooting_url": null + }, + "sensitive_values": { + "display_apps": [ + {} + ], + "metadata": [], + "resources_monitoring": [ + { + "memory": [ + {} + ], + "volume": [ + {}, + {} + ] + } + ], + "token": true + } + }, + { + "address": "coder_app.app1", + "mode": "managed", + "type": "coder_app", + "name": "app1", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "agent_id": "baa57f58-60fb-4490-9f11-1f7e17ae698a", + "command": null, + "display_name": null, + "external": false, + "healthcheck": [], + "hidden": false, + "icon": null, + "id": "9e46f6e5-72c3-4502-b0f8-36bcec12baba", + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app1", + "subdomain": null, + "url": null + }, + "sensitive_values": { + "healthcheck": [] + }, + "depends_on": [ + "coder_agent.dev1" + ] + }, + { + "address": "coder_app.app2", + "mode": "managed", + "type": "coder_app", + "name": "app2", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 1, + "values": { + "agent_id": "baa57f58-60fb-4490-9f11-1f7e17ae698a", + "command": null, + "display_name": null, + "external": false, + "healthcheck": [ + { + "interval": 5, + "threshold": 6, + "url": "http://localhost:13337/healthz" + } + ], + "hidden": false, + "icon": null, + "id": "e3591c31-401b-44c6-bbba-df36d02199c8", + "open_in": "slim-window", + "order": null, + "share": "owner", + "slug": "app2", + "subdomain": true, + "url": null + }, + "sensitive_values": { + "healthcheck": [ + {} + ] + }, + "depends_on": [ + "coder_agent.dev1" + ] + }, + { + "address": "null_resource.dev", + "mode": "managed", + "type": "null_resource", + "name": "dev", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "4101480329869104281", + "triggers": null + }, + "sensitive_values": {}, + "depends_on": [ + "coder_agent.dev1", + "coder_agent.dev2" + ] + } + ] + } + } +} diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tf b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tf index af041e2da350d..c0aee0d2d97e5 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tf +++ b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfplan.json b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfplan.json index ab14e49f02989..8a6311e03958b 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -41,23 +38,20 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -72,7 +66,7 @@ "type": "coder_script", "name": "script1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "cron": null, "display_name": "Foobar Script 1", @@ -92,7 +86,7 @@ "type": "coder_script", "name": "script2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "cron": null, "display_name": "Foobar Script 2", @@ -112,7 +106,7 @@ "type": "coder_script", "name": "script3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "cron": null, "display_name": "Foobar Script 3", @@ -171,16 +165,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -215,16 +206,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -380,7 +368,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -403,7 +391,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_agent.dev2", @@ -419,7 +407,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_script.script1", @@ -444,7 +432,7 @@ "constant_value": "echo foobar 1" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_script.script2", @@ -469,7 +457,7 @@ "constant_value": "echo foobar 2" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_script.script3", @@ -494,7 +482,7 @@ "constant_value": "echo foobar 3" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev1", @@ -523,19 +511,19 @@ }, "relevant_attributes": [ { - "resource": "coder_agent.dev1", + "resource": "coder_agent.dev2", "attribute": [ "id" ] }, { - "resource": "coder_agent.dev2", + "resource": "coder_agent.dev1", "attribute": [ "id" ] } ], - "timestamp": "2024-10-28T20:08:08Z", + "timestamp": "2025-03-04T19:25:23Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfstate.json b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfstate.json index 37c4ef13ee6fb..26c719f7d730a 100644 --- a/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents-multiple-scripts/multiple-agents-multiple-scripts.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "753eb8c0-e2b7-4cbc-b0ff-1370ce2e4022", + "id": "0ae6e69a-d5ca-4b40-9a79-67e11b317742", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "77b179b6-0e2d-4307-9ba0-98325fc96e37", + "startup_script_behavior": "non-blocking", + "token": "0a20b766-43d1-4074-a445-384257e5de27", "troubleshooting_url": null }, "sensitive_values": { @@ -55,7 +52,7 @@ "type": "coder_agent", "name": "dev2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -71,19 +68,16 @@ } ], "env": null, - "id": "86f7e422-1798-4de5-8209-69b023808241", + "id": "622c6b37-cea1-4f6d-9f01-55e5cdd541a6", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "aa4ae02d-4084-4dff-951c-af10f78a98c2", + "startup_script_behavior": "non-blocking", + "token": "055f173e-755a-4829-b817-1d5ccf054f70", "troubleshooting_url": null }, "sensitive_values": { @@ -100,13 +94,13 @@ "type": "coder_script", "name": "script1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "753eb8c0-e2b7-4cbc-b0ff-1370ce2e4022", + "agent_id": "0ae6e69a-d5ca-4b40-9a79-67e11b317742", "cron": null, "display_name": "Foobar Script 1", "icon": null, - "id": "eb1eb8f4-3a4a-4040-bd6a-0abce01d6330", + "id": "34f6c49b-8c99-4bd1-a40f-24ee011fb490", "log_path": null, "run_on_start": true, "run_on_stop": false, @@ -125,13 +119,13 @@ "type": "coder_script", "name": "script2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "753eb8c0-e2b7-4cbc-b0ff-1370ce2e4022", + "agent_id": "0ae6e69a-d5ca-4b40-9a79-67e11b317742", "cron": null, "display_name": "Foobar Script 2", "icon": null, - "id": "1de43abc-8416-4455-87ca-23fb425b4eeb", + "id": "4c6bc57b-23fc-41c2-86c5-4f3a899e171c", "log_path": null, "run_on_start": true, "run_on_stop": false, @@ -150,13 +144,13 @@ "type": "coder_script", "name": "script3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "86f7e422-1798-4de5-8209-69b023808241", + "agent_id": "622c6b37-cea1-4f6d-9f01-55e5cdd541a6", "cron": null, "display_name": "Foobar Script 3", "icon": null, - "id": "ede835f7-4018-464c-807d-7e07af7de9d3", + "id": "8ff5a1e9-ca8c-442f-89c2-dd7e2d8e74d7", "log_path": null, "run_on_start": true, "run_on_stop": false, @@ -177,7 +171,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4207133259459553257", + "id": "7783428659239328742", "triggers": null }, "sensitive_values": {}, @@ -193,7 +187,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5647997484430231619", + "id": "8319259774862919105", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json index 67da167932aa4..2ce46aa6dabff 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -464,7 +464,7 @@ ] } }, - "timestamp": "2024-10-28T20:08:03Z", + "timestamp": "2025-03-04T19:25:25Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json index cd8edc0ae29bc..fbdf9416a3e88 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -26,7 +26,7 @@ } ], "env": null, - "id": "c76ed902-d4cb-4905-9961-4d58dda135f9", + "id": "996be323-1195-4cee-9d00-778a025fbaf9", "init_script": "", "login_before_ready": true, "metadata": [], @@ -38,7 +38,7 @@ "startup_script": null, "startup_script_behavior": null, "startup_script_timeout": 300, - "token": "f1aa99ea-570d-49cf-aef9-a4241e3cb023", + "token": "df89f5a4-3349-40eb-a628-d20f5e8ffb93", "troubleshooting_url": null }, "sensitive_values": { @@ -71,7 +71,7 @@ } ], "env": null, - "id": "1b037439-4eb3-408e-83da-28dc93645944", + "id": "8c9bbbf8-0049-47d6-afef-555f2e4c8f31", "init_script": "", "login_before_ready": true, "metadata": [], @@ -83,7 +83,7 @@ "startup_script": null, "startup_script_behavior": "non-blocking", "startup_script_timeout": 30, - "token": "20d4e89e-d6de-4eb7-8877-f9186d684aa5", + "token": "26d616b2-9df3-43b2-b89b-dc8665dbcff4", "troubleshooting_url": null }, "sensitive_values": { @@ -116,7 +116,7 @@ } ], "env": null, - "id": "453b5404-8ea4-4197-8664-3638e6a012ca", + "id": "33ed3e88-edf5-4bc8-a0bb-1f743ffb169d", "init_script": "", "login_before_ready": true, "metadata": [], @@ -128,7 +128,7 @@ "startup_script": null, "startup_script_behavior": "blocking", "startup_script_timeout": 300, - "token": "0355cb42-9da0-4bad-b2aa-74db1df76fef", + "token": "42292f6c-6fc4-43f4-8de9-219990cc4a93", "troubleshooting_url": "https://coder.com/troubleshoot" }, "sensitive_values": { @@ -161,7 +161,7 @@ } ], "env": null, - "id": "c0a68e9b-5b29-4d95-b664-5ac71dd633cf", + "id": "f1ff7240-3700-494d-a0fd-6a88459d8ab7", "init_script": "", "login_before_ready": false, "metadata": [], @@ -173,7 +173,7 @@ "startup_script": null, "startup_script_behavior": null, "startup_script_timeout": 300, - "token": "34b78439-5d6e-431b-b06c-339f97a1e9cf", + "token": "2d133297-d1b2-486e-8b3e-078df4ed0de7", "troubleshooting_url": null }, "sensitive_values": { @@ -192,7 +192,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5109814714394194897", + "id": "1432868857711807382", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf index c7c4f9968b5c3..c52f4a58b36f4 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json index b156c3b5068b6..c0c11ddf7cde7 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -41,16 +38,16 @@ "type": "coder_app", "name": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -66,7 +63,7 @@ "type": "coder_app", "name": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, @@ -78,10 +75,10 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -99,16 +96,16 @@ "type": "coder_app", "name": "app3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -151,16 +148,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -194,10 +188,10 @@ "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -236,10 +230,10 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -276,10 +270,10 @@ "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "name": null, + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -323,7 +317,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -346,7 +340,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app1", @@ -365,7 +359,7 @@ "constant_value": "app1" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app2", @@ -400,7 +394,7 @@ "constant_value": true } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_app.app3", @@ -422,7 +416,7 @@ "constant_value": false } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -446,7 +440,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:10Z", + "timestamp": "2025-03-04T19:25:27Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json index d3fc254bf40b0..d0c33353e4e18 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "dev1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,19 +26,16 @@ } ], "env": null, - "id": "b3ea3cb0-176c-4642-9bf5-cfa72e0782cc", + "id": "d71e5147-0345-4be6-aa12-a90ca441db12", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "30533677-f04a-493b-b6cb-314d9abf7769", + "startup_script_behavior": "non-blocking", + "token": "3a74d687-a18c-4988-9d65-6659577553dd", "troubleshooting_url": null }, "sensitive_values": { @@ -55,18 +52,18 @@ "type": "coder_app", "name": "app1", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3ea3cb0-176c-4642-9bf5-cfa72e0782cc", + "agent_id": "d71e5147-0345-4be6-aa12-a90ca441db12", "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "537e9069-492b-4721-96dd-cffba275ecd9", - "name": null, + "id": "3d1b6545-ab67-458d-abb1-7a7f3ef2c8a6", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app1", "subdomain": null, @@ -85,9 +82,9 @@ "type": "coder_app", "name": "app2", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3ea3cb0-176c-4642-9bf5-cfa72e0782cc", + "agent_id": "d71e5147-0345-4be6-aa12-a90ca441db12", "command": null, "display_name": null, "external": false, @@ -98,11 +95,11 @@ "url": "http://localhost:13337/healthz" } ], + "hidden": false, "icon": null, - "id": "3a4c78a0-7ea3-44aa-9ea8-4e08e387b4b6", - "name": null, + "id": "0ec8ec9c-67b2-4e1a-8f18-346bda58a06f", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app2", "subdomain": true, @@ -123,18 +120,18 @@ "type": "coder_app", "name": "app3", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { - "agent_id": "b3ea3cb0-176c-4642-9bf5-cfa72e0782cc", + "agent_id": "d71e5147-0345-4be6-aa12-a90ca441db12", "command": null, "display_name": null, "external": false, "healthcheck": [], + "hidden": false, "icon": null, - "id": "23555681-0ecb-4962-8e85-367d3a9d0228", - "name": null, + "id": "97559bb9-cf0f-4b83-9e53-25cd7c785f34", + "open_in": "slim-window", "order": null, - "relative_path": null, "share": "owner", "slug": "app3", "subdomain": false, @@ -155,7 +152,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2905101599123333983", + "id": "8869597861449516263", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf index b316db7c3cdf1..b88a672f0047a 100644 --- a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json index 3b7881701038c..04f3fe1056037 100644 --- a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,14 +10,13 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -32,10 +31,8 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -52,7 +49,7 @@ "type": "coder_metadata", "name": "about_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 29, "hide": true, @@ -83,7 +80,7 @@ "type": "coder_metadata", "name": "other_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 20, "hide": true, @@ -135,7 +132,6 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -150,10 +146,8 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -291,7 +285,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -333,7 +327,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_metadata.about_info", @@ -373,7 +367,7 @@ ] } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_metadata.other_info", @@ -408,7 +402,7 @@ ] } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.about", @@ -432,7 +426,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:13Z", + "timestamp": "2025-03-04T19:25:29Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json index 170630d0e3103..a13acd6b519b2 100644 --- a/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json +++ b/provisioner/terraform/testdata/resource-metadata-duplicate/resource-metadata-duplicate.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,9 +26,8 @@ } ], "env": null, - "id": "0cbc2449-fbaa-447a-8487-6c47367af0be", + "id": "3d4f1a36-68cd-4c91-92dd-d07ccb515553", "init_script": "", - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -43,11 +42,9 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "b03606cc-1ed3-4187-964d-389cf2ef223f", + "startup_script_behavior": "non-blocking", + "token": "95c49419-c343-4185-9132-4f7b64105e6c", "troubleshooting_url": null }, "sensitive_values": { @@ -66,12 +63,12 @@ "type": "coder_metadata", "name": "about_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 29, "hide": true, "icon": "/icon/server.svg", - "id": "d6c33b98-addd-4d97-8659-405350bc06c1", + "id": "49d732e1-2d98-457d-84f2-088660d6e80b", "item": [ { "is_null": false, @@ -86,7 +83,7 @@ "value": "" } ], - "resource_id": "5673227143105805783" + "resource_id": "771326507239132201" }, "sensitive_values": { "item": [ @@ -105,12 +102,12 @@ "type": "coder_metadata", "name": "other_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 20, "hide": true, "icon": "/icon/server.svg", - "id": "76594f08-2261-4114-a61f-e07107a86f89", + "id": "a2a626bc-7e5b-48cb-919b-449f40d7b145", "item": [ { "is_null": false, @@ -119,7 +116,7 @@ "value": "world" } ], - "resource_id": "5673227143105805783" + "resource_id": "771326507239132201" }, "sensitive_values": { "item": [ @@ -139,7 +136,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5673227143105805783", + "id": "771326507239132201", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf index cd46057ce8526..eb9f2eff89877 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json index f9c24830c6ef3..05d87f4fc7c4b 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,14 +10,13 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -32,10 +31,8 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -52,7 +49,7 @@ "type": "coder_metadata", "name": "about_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 29, "hide": true, @@ -122,7 +119,6 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -137,10 +133,8 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -256,7 +250,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -301,7 +295,7 @@ "constant_value": "linux" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "coder_metadata.about_info", @@ -360,7 +354,7 @@ ] } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.about", @@ -384,7 +378,7 @@ ] } ], - "timestamp": "2024-10-28T20:08:11Z", + "timestamp": "2025-03-04T19:25:30Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json index a41aff216b11c..a2b1be2257a3f 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -10,7 +10,7 @@ "type": "coder_agent", "name": "main", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "amd64", "auth": "token", @@ -26,9 +26,8 @@ } ], "env": null, - "id": "3bcbc547-b434-4dbd-b5ed-551edfba1b5c", + "id": "e7fbb3ac-d754-44b5-aa2b-bef6b9f358ff", "init_script": "", - "login_before_ready": true, "metadata": [ { "display_name": "Process Count", @@ -43,11 +42,9 @@ "order": null, "os": "linux", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "2d25fcc3-a355-4e92-98c6-ab780894ffee", + "startup_script_behavior": "non-blocking", + "token": "e82b787b-e97e-4555-8680-cf6a887dfc6a", "troubleshooting_url": null }, "sensitive_values": { @@ -66,12 +63,12 @@ "type": "coder_metadata", "name": "about_info", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "daily_cost": 29, "hide": true, "icon": "/icon/server.svg", - "id": "d9ce721c-dff3-44fd-92d1-155f37c84a56", + "id": "4eed85bc-cefc-487a-b7f4-ef39ada0d3e0", "item": [ { "is_null": false, @@ -98,7 +95,7 @@ "value": "squirrel" } ], - "resource_id": "4099397325680267994" + "resource_id": "4279195833018001043" }, "sensitive_values": { "item": [ @@ -121,7 +118,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4099397325680267994", + "id": "4279195833018001043", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tf b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tf index 82e7a6f95694e..fc684a6e583ee 100644 --- a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tf +++ b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfplan.json b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfplan.json index 72120dfaabeec..58e3d054bba5b 100644 --- a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfplan.json +++ b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -68,16 +65,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -119,7 +113,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -136,7 +130,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "e8805d7c-1636-4416-9520-b83234d68ddc", + "id": "965c2bc9-936f-43d5-9287-36603538790a", "mutable": false, "name": "Example", "option": null, @@ -163,7 +157,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "df43829a-49ce-4911-97ef-2fca78456c9f", + "id": "6472f1a0-e75c-45a2-9a49-0bb7adcb3e23", "mutable": false, "name": "Sample", "option": null, @@ -186,7 +180,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -209,7 +203,7 @@ "constant_value": "windows" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -269,7 +263,7 @@ ] } }, - "timestamp": "2024-10-28T20:08:17Z", + "timestamp": "2025-03-04T19:25:32Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfstate.json b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfstate.json index 1d675d685a37c..c2a3fda518070 100644 --- a/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfstate.json +++ b/provisioner/terraform/testdata/rich-parameters-order/rich-parameters-order.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -17,7 +17,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "81ada233-3a30-49d3-a56f-aca92f19c411", + "id": "03899941-4e7d-4e74-bcfb-92dc9619a85d", "mutable": false, "name": "Example", "option": null, @@ -44,7 +44,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "4dc1049f-0d54-408a-a412-95629ae5cd84", + "id": "632aa89d-d903-44c3-b44c-eb131498c579", "mutable": false, "name": "Sample", "option": null, @@ -64,7 +64,7 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", @@ -80,19 +80,16 @@ } ], "env": null, - "id": "86cc4d6e-23b3-4632-9bc9-d3a321e8b906", + "id": "f67efb88-ae6d-4f33-8065-50b420c1ce80", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "0c3e7639-bafc-4e62-8e38-cb4e1b44e3f3", + "startup_script_behavior": "non-blocking", + "token": "164cba1c-98ce-4590-8ebb-f966a66a2301", "troubleshooting_url": null }, "sensitive_values": { @@ -111,7 +108,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2501594036325466407", + "id": "2247834665085956471", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tf b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tf index c05e8d5d4ae32..8067c0fa9337c 100644 --- a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tf +++ b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfplan.json b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfplan.json index 66153605ee4a0..bd103658ad3d7 100644 --- a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfplan.json +++ b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -68,16 +65,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -119,7 +113,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -136,7 +130,7 @@ "display_name": null, "ephemeral": true, "icon": null, - "id": "df8ad066-047d-434d-baa3-e19517ee7395", + "id": "fd220d2f-a3e7-4f6b-9870-ebe0ccb3e5c9", "mutable": true, "name": "number_example", "option": null, @@ -163,7 +157,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "7d9658aa-ff69-477a-9063-e9fd49fd9a9b", + "id": "b8288523-b649-48d0-928c-ae99069adda2", "mutable": false, "name": "number_example_max", "option": null, @@ -202,7 +196,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "bd6fcaac-db7f-4c4d-a664-fe7f47fad28a", + "id": "07c9e8e7-3dd2-4674-875b-ac6369cf5420", "mutable": false, "name": "number_example_max_zero", "option": null, @@ -241,7 +235,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "8d42942d-5a10-43c9-a31d-d3fe9a7814e8", + "id": "b8f0a3fc-277d-47b2-a7a2-1267430b8cd8", "mutable": false, "name": "number_example_min", "option": null, @@ -280,7 +274,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "695301d0-8325-4685-824d-1ca9591689e3", + "id": "260e9363-5c18-4e22-a1e2-9a28f0a0d14a", "mutable": false, "name": "number_example_min_max", "option": null, @@ -319,7 +313,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "cd921934-d1b1-4370-8a73-2d43658ea877", + "id": "79a04ae2-4098-4482-a83b-2de95174ff22", "mutable": false, "name": "number_example_min_zero", "option": null, @@ -354,7 +348,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "null": { "name": "null", @@ -377,7 +371,7 @@ "constant_value": "windows" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -551,7 +545,7 @@ ] } }, - "timestamp": "2024-10-28T20:08:18Z", + "timestamp": "2025-03-04T19:25:34Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfstate.json b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfstate.json index 35b981c3a9b54..1ba6a2b297eb0 100644 --- a/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfstate.json +++ b/provisioner/terraform/testdata/rich-parameters-validation/rich-parameters-validation.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -17,7 +17,7 @@ "display_name": null, "ephemeral": true, "icon": null, - "id": "e09e9110-2f11-4a45-bc9f-dc7a12834ef0", + "id": "9f9ddc8e-bd35-4fa9-ade3-8f206c96d2e8", "mutable": true, "name": "number_example", "option": null, @@ -44,7 +44,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "7ba6324d-d8fd-43b8-91d2-d970a424db8b", + "id": "1a989e82-d7a9-460f-9d0d-fd3adcbf0aeb", "mutable": false, "name": "number_example_max", "option": null, @@ -83,7 +83,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "64e12007-8479-43bf-956b-86fe7ae73066", + "id": "ded0470e-2a18-4509-a675-adba973eb034", "mutable": false, "name": "number_example_max_zero", "option": null, @@ -122,7 +122,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "32681b2b-682f-4a5f-9aa6-c05be9d41a89", + "id": "1fd9df82-d747-4a35-a717-87e867efff71", "mutable": false, "name": "number_example_min", "option": null, @@ -161,7 +161,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "03b67b89-0d35-449d-8997-f5ce4b7c1518", + "id": "22450c89-855a-43ff-9757-0516c72ab437", "mutable": false, "name": "number_example_min_max", "option": null, @@ -200,7 +200,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "2201fc53-38c6-4a68-b3b9-4f6ef3390962", + "id": "32ffe953-8cf6-4637-bf00-171012629ea5", "mutable": false, "name": "number_example_min_zero", "option": null, @@ -232,7 +232,7 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", @@ -248,19 +248,16 @@ } ], "env": null, - "id": "060ffd05-39a9-4fa3-81a3-7d9d8e655bf8", + "id": "d930846f-c291-4d31-8f5e-7f1080831b38", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "58ed35b2-6124-4183-a493-40cb0174f4d2", + "startup_script_behavior": "non-blocking", + "token": "03f35d2f-6476-407f-9ac8-bbe0892b5de8", "troubleshooting_url": null }, "sensitive_values": { @@ -279,7 +276,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "4610812354433374355", + "id": "6098797437627753082", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/rich-parameters/external-module/child-external-module/main.tf b/provisioner/terraform/testdata/rich-parameters/external-module/child-external-module/main.tf index ac6f4c621a9d0..e8afbbf917fb5 100644 --- a/provisioner/terraform/testdata/rich-parameters/external-module/child-external-module/main.tf +++ b/provisioner/terraform/testdata/rich-parameters/external-module/child-external-module/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } docker = { source = "kreuzwerker/docker" diff --git a/provisioner/terraform/testdata/rich-parameters/external-module/main.tf b/provisioner/terraform/testdata/rich-parameters/external-module/main.tf index 55e942ec24e1f..0cf81d0162d07 100644 --- a/provisioner/terraform/testdata/rich-parameters/external-module/main.tf +++ b/provisioner/terraform/testdata/rich-parameters/external-module/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } docker = { source = "kreuzwerker/docker" diff --git a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tf b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tf index fc85769c8e9cc..24582eac30a5d 100644 --- a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tf +++ b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.22.0" + version = ">=2.0.0" } } } diff --git a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json index 1ec2927a40ad1..63cb47bb25279 100644 --- a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json +++ b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "planned_values": { "root_module": { "resources": [ @@ -10,23 +10,20 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "sensitive_values": { @@ -68,16 +65,13 @@ "connection_timeout": 120, "dir": null, "env": null, - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, + "startup_script_behavior": "non-blocking", "troubleshooting_url": null }, "after_unknown": { @@ -119,7 +113,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -136,7 +130,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "cbec5bff-b81a-4815-99c0-40c0629779fb", + "id": "d15a5f2d-00a6-4d44-ba1d-340c4172e882", "mutable": false, "name": "Example", "option": [ @@ -180,7 +174,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "dd1c36b7-a961-4eb2-9687-c32b5ee54fbc", + "id": "fbdc4337-1adc-4ec7-8157-5065a4523356", "mutable": false, "name": "number_example", "option": null, @@ -207,7 +201,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "f1bcac54-a58c-44b2-94f5-243a0b1492d3", + "id": "e0555e6b-3899-4c51-a755-398e7369314f", "mutable": false, "name": "number_example_max_zero", "option": null, @@ -246,7 +240,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "79c76ac1-8e71-4872-9107-d7a9529f7dce", + "id": "718dc4d2-62ac-4ca0-b303-16d511e0a0e8", "mutable": false, "name": "number_example_min_max", "option": null, @@ -285,7 +279,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "da7a8aff-ffe3-402f-bf7e-b369ae04b041", + "id": "3d4d7d00-698b-46f7-904d-acfd84c7822a", "mutable": false, "name": "number_example_min_zero", "option": null, @@ -324,7 +318,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "5fe2dad0-e11f-46f0-80ae-c0c3a29cd1fd", + "id": "587f613d-2ec6-4cb7-8e9c-b61ca12ac9ca", "mutable": false, "name": "Sample", "option": null, @@ -355,7 +349,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "920f98a1-3a6f-4602-8c87-ebbbef0310c5", + "id": "a7f7ce22-ea95-45f9-80fa-774afd033d40", "mutable": true, "name": "First parameter from module", "option": null, @@ -382,7 +376,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "f438d9ad-6c3e-44f3-95cd-1d423a9b09e5", + "id": "86f108c2-91f1-4347-b667-e0afee5fa666", "mutable": true, "name": "Second parameter from module", "option": null, @@ -414,7 +408,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "b2c53701-be53-4591-aacf-1c83f75bcf15", + "id": "f3c13a53-aebd-4a78-81a4-ad75874f2238", "mutable": true, "name": "First parameter from child module", "option": null, @@ -441,7 +435,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "038b18d4-d430-4703-886a-b7e10e01f856", + "id": "7c8a6913-382c-4874-87a9-76fac542f6e0", "mutable": true, "name": "Second parameter from child module", "option": null, @@ -469,7 +463,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.22.0" + "version_constraint": ">= 2.0.0" }, "module.this_is_external_module:docker": { "name": "docker", @@ -498,7 +492,7 @@ "constant_value": "windows" } }, - "schema_version": 0 + "schema_version": 1 }, { "address": "null_resource.dev", @@ -794,7 +788,7 @@ } } }, - "timestamp": "2024-10-28T20:08:15Z", + "timestamp": "2025-03-04T19:25:36Z", "applyable": true, "complete": true, "errored": false diff --git a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json index 1bfc1835dfcaf..60dec620c89a5 100644 --- a/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json +++ b/provisioner/terraform/testdata/rich-parameters/rich-parameters.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.9.8", + "terraform_version": "1.10.5", "values": { "root_module": { "resources": [ @@ -17,7 +17,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "8586d419-7e61-4e67-b8df-d98d8ac7ffd3", + "id": "5b27dba2-e9b1-408f-b8af-0ce18692c189", "mutable": false, "name": "Example", "option": [ @@ -61,7 +61,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "0cc54450-13a6-486c-b542-6e23a9f3596b", + "id": "5b357564-7fbb-4d32-9775-b0f0e8ec201a", "mutable": false, "name": "number_example", "option": null, @@ -88,7 +88,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "0c0b913a-0bde-4b9e-8a70-06d9b6d38a26", + "id": "41994818-572f-482d-a565-ce260ca414fc", "mutable": false, "name": "number_example_max_zero", "option": null, @@ -127,7 +127,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "37fd5372-2741-49dd-bf01-6ba29a24c9dd", + "id": "72683cb2-279f-4c59-a598-7d88da6b9cb7", "mutable": false, "name": "number_example_min_max", "option": null, @@ -166,7 +166,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "c0fd84ff-117f-442a-95f7-e8368ba7ce1d", + "id": "d5c94b27-982b-4d4f-a1c0-1f59a2b50f92", "mutable": false, "name": "number_example_min_zero", "option": null, @@ -205,7 +205,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "ab067ffc-99de-4705-97fe-16c713d2d115", + "id": "27867d73-1f2d-407b-bbc9-9dc612d1a0a7", "mutable": false, "name": "Sample", "option": null, @@ -225,7 +225,7 @@ "type": "coder_agent", "name": "dev", "provider_name": "registry.terraform.io/coder/coder", - "schema_version": 0, + "schema_version": 1, "values": { "arch": "arm64", "auth": "token", @@ -241,19 +241,16 @@ } ], "env": null, - "id": "7daab302-d00e-48d4-878c-47afbe3a13bc", + "id": "e8c8b049-5c00-421f-8a86-a608ddd35d0d", "init_script": "", - "login_before_ready": true, "metadata": [], "motd_file": null, "order": null, "os": "windows", "shutdown_script": null, - "shutdown_script_timeout": 300, "startup_script": null, - "startup_script_behavior": null, - "startup_script_timeout": 300, - "token": "e98c452d-cbe9-4ae1-8382-a986089dccb4", + "startup_script_behavior": "non-blocking", + "token": "b8d3d938-92ad-478f-85bd-fd5351cd2bbb", "troubleshooting_url": null }, "sensitive_values": { @@ -272,7 +269,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "2355126481625628137", + "id": "4692717259652730903", "triggers": null }, "sensitive_values": {}, @@ -297,7 +294,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "0978cc7c-f787-406c-a050-9272bbb52085", + "id": "ea4242b4-e62f-4741-8933-9dc574d79192", "mutable": true, "name": "First parameter from module", "option": null, @@ -324,7 +321,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "cd01d7da-9f56-460d-b163-e88a0a9a5f67", + "id": "df181723-65f5-4ad3-8f7a-058710a8c5ba", "mutable": true, "name": "Second parameter from module", "option": null, @@ -356,7 +353,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "528e845a-843b-48b3-a421-a22340726d5a", + "id": "beb63d1b-a3c1-447c-82fc-0158600033ab", "mutable": true, "name": "First parameter from child module", "option": null, @@ -383,7 +380,7 @@ "display_name": null, "ephemeral": false, "icon": null, - "id": "f486efbb-2fc6-4091-9eca-0088ac6cd3cc", + "id": "06e986b9-b440-4090-8c52-4f889d822aea", "mutable": true, "name": "Second parameter from child module", "option": null, diff --git a/provisioner/terraform/testdata/version.txt b/provisioner/terraform/testdata/version.txt index 66beabb5795e7..db77e0ee9760a 100644 --- a/provisioner/terraform/testdata/version.txt +++ b/provisioner/terraform/testdata/version.txt @@ -1 +1 @@ -1.9.8 +1.10.5 diff --git a/scripts/Dockerfile.base b/scripts/Dockerfile.base index 30ef6802ed716..f9d2bf6594b08 100644 --- a/scripts/Dockerfile.base +++ b/scripts/Dockerfile.base @@ -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.9.8/terraform_1.9.8_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.10.5/terraform_1.10.5_linux_${ARCH}.zip" && \ busybox unzip /tmp/terraform.zip -d /usr/local/bin && \ rm -f /tmp/terraform.zip && \ chmod +x /usr/local/bin/terraform && \ diff --git a/scripts/release/check_commit_metadata.sh b/scripts/release/check_commit_metadata.sh index dff4cb1c738fc..f53de8e107430 100755 --- a/scripts/release/check_commit_metadata.sh +++ b/scripts/release/check_commit_metadata.sh @@ -143,7 +143,12 @@ main() { for commit in "${renamed_cherry_pick_commits_pending[@]}"; do log "Checking if pending commit ${commit} has a corresponding cherry-pick..." if [[ ! -v renamed_cherry_pick_commits[${commit}] ]]; then - error "Invariant failed, cherry-picked commit ${commit} has no corresponding original commit" + if [[ ${CODER_IGNORE_MISSING_COMMIT_METADATA:-0} == 1 ]]; then + log "WARNING: Missing original commit for cherry-picked commit ${commit}, but continuing due to CODER_IGNORE_MISSING_COMMIT_METADATA being set." + continue + else + error "Invariant failed, cherry-picked commit ${commit} has no corresponding original commit" + fi fi log "Found matching cherry-pick commit ${commit} -> ${renamed_cherry_pick_commits[${commit}]}" done diff --git a/site/site.go b/site/site.go index af66c01c6f896..3a85f7b3963ad 100644 --- a/site/site.go +++ b/site/site.go @@ -34,6 +34,7 @@ import ( "golang.org/x/sync/singleflight" "golang.org/x/xerrors" + "cdr.dev/slog" "github.com/coder/coder/v2/coderd/appearance" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" @@ -41,6 +42,7 @@ import ( "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/codersdk" ) @@ -81,6 +83,8 @@ type Options struct { BuildInfo codersdk.BuildInfoResponse AppearanceFetcher *atomic.Pointer[appearance.Fetcher] Entitlements *entitlements.Set + Telemetry telemetry.Reporter + Logger slog.Logger } func New(opts *Options) *Handler { @@ -183,6 +187,8 @@ type Handler struct { Entitlements *entitlements.Set Experiments atomic.Pointer[codersdk.Experiments] + + telemetryHTMLServedOnce sync.Once } func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { @@ -321,12 +327,51 @@ func ShouldCacheFile(reqFile string) bool { return true } +// reportHTMLFirstServedAt sends a telemetry report when the first HTML is ever served. +// The purpose is to track the first time the first user opens the site. +func (h *Handler) reportHTMLFirstServedAt() { + // nolint:gocritic // Manipulating telemetry items is system-restricted. + // TODO(hugodutka): Add a telemetry context in RBAC. + ctx := dbauthz.AsSystemRestricted(context.Background()) + itemKey := string(telemetry.TelemetryItemKeyHTMLFirstServedAt) + _, err := h.opts.Database.GetTelemetryItem(ctx, itemKey) + if err == nil { + // If the value is already set, then we reported it before. + // We don't need to report it again. + return + } + if !errors.Is(err, sql.ErrNoRows) { + h.opts.Logger.Debug(ctx, "failed to get telemetry html first served at", slog.Error(err)) + return + } + if err := h.opts.Database.InsertTelemetryItemIfNotExists(ctx, database.InsertTelemetryItemIfNotExistsParams{ + Key: string(telemetry.TelemetryItemKeyHTMLFirstServedAt), + Value: time.Now().Format(time.RFC3339), + }); err != nil { + h.opts.Logger.Debug(ctx, "failed to set telemetry html first served at", slog.Error(err)) + return + } + item, err := h.opts.Database.GetTelemetryItem(ctx, itemKey) + if err != nil { + h.opts.Logger.Debug(ctx, "failed to get telemetry html first served at", slog.Error(err)) + return + } + h.opts.Telemetry.Report(&telemetry.Snapshot{ + TelemetryItems: []telemetry.TelemetryItem{telemetry.ConvertTelemetryItem(item)}, + }) +} + func (h *Handler) serveHTML(resp http.ResponseWriter, request *http.Request, reqPath string, state htmlState) bool { if data, err := h.renderHTMLWithState(request, reqPath, state); err == nil { if reqPath == "" { // Pass "index.html" to the ServeContent so the ServeContent sets the right content headers. reqPath = "index.html" } + // `Once` is used to reduce the volume of db calls and telemetry reports. + // It's fine to run the enclosed function multiple times, but it's unnecessary. + h.telemetryHTMLServedOnce.Do(func() { + go h.reportHTMLFirstServedAt() + }) http.ServeContent(resp, request, reqPath, time.Time{}, bytes.NewReader(data)) return true } diff --git a/site/site_test.go b/site/site_test.go index 8bee665a56ae3..63f3f9aa17226 100644 --- a/site/site_test.go +++ b/site/site_test.go @@ -27,8 +27,10 @@ import ( "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbmem" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/site" "github.com/coder/coder/v2/testutil" @@ -45,9 +47,10 @@ func TestInjection(t *testing.T) { binFs := http.FS(fstest.MapFS{}) db := dbmem.New() handler := site.New(&site.Options{ - BinFS: binFs, - Database: db, - SiteFS: siteFS, + Telemetry: telemetry.NewNoop(), + BinFS: binFs, + Database: db, + SiteFS: siteFS, }) user := dbgen.User(t, db, database.User{}) @@ -101,9 +104,10 @@ func TestInjectionFailureProducesCleanHTML(t *testing.T) { }, } handler := site.New(&site.Options{ - BinFS: binFs, - Database: db, - SiteFS: siteFS, + Telemetry: telemetry.NewNoop(), + BinFS: binFs, + Database: db, + SiteFS: siteFS, // No OAuth2 configs, refresh will fail. OAuth2Configs: &httpmw.OAuth2Configs{ @@ -147,9 +151,12 @@ func TestCaching(t *testing.T) { } binFS := http.FS(fstest.MapFS{}) + db, _ := dbtestutil.NewDB(t) srv := httptest.NewServer(site.New(&site.Options{ - BinFS: binFS, - SiteFS: rootFS, + Telemetry: telemetry.NewNoop(), + BinFS: binFS, + SiteFS: rootFS, + Database: db, })) defer srv.Close() @@ -213,9 +220,12 @@ func TestServingFiles(t *testing.T) { } binFS := http.FS(fstest.MapFS{}) + db, _ := dbtestutil.NewDB(t) srv := httptest.NewServer(site.New(&site.Options{ - BinFS: binFS, - SiteFS: rootFS, + Telemetry: telemetry.NewNoop(), + BinFS: binFS, + SiteFS: rootFS, + Database: db, })) defer srv.Close() @@ -473,6 +483,7 @@ func TestServingBin(t *testing.T) { } srv := httptest.NewServer(site.New(&site.Options{ + Telemetry: telemetry.NewNoop(), BinFS: binFS, BinHashes: binHashes, SiteFS: rootFS, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d5093587ad527..de879ee23daa5 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1055,6 +1055,12 @@ export interface HealthcheckReport { readonly coder_version: string; } +// From codersdk/idpsync.go +export interface IDPSyncMapping { + readonly Given: string; + readonly Gets: ResourceIdType; +} + // From codersdk/insights.go export type InsightsReportInterval = "day" | "week"; @@ -1449,6 +1455,19 @@ export interface Pagination { readonly offset?: number; } +// From codersdk/idpsync.go +export interface PatchGroupIDPSyncConfigRequest { + readonly field: string; + readonly regex_filter: string | null; + readonly auto_create_missing_groups: boolean; +} + +// From codersdk/idpsync.go +export interface PatchGroupIDPSyncMappingRequest { + readonly Add: readonly IDPSyncMapping[]; + readonly Remove: readonly IDPSyncMapping[]; +} + // From codersdk/groups.go export interface PatchGroupRequest { readonly add_users: readonly string[]; @@ -1459,6 +1478,29 @@ export interface PatchGroupRequest { readonly quota_allowance: number | null; } +// From codersdk/idpsync.go +export interface PatchOrganizationIDPSyncConfigRequest { + readonly field: string; + readonly assign_default: boolean; +} + +// From codersdk/idpsync.go +export interface PatchOrganizationIDPSyncMappingRequest { + readonly Add: readonly IDPSyncMapping[]; + readonly Remove: readonly IDPSyncMapping[]; +} + +// From codersdk/idpsync.go +export interface PatchRoleIDPSyncConfigRequest { + readonly field: string; +} + +// From codersdk/idpsync.go +export interface PatchRoleIDPSyncMappingRequest { + readonly Add: readonly IDPSyncMapping[]; + readonly Remove: readonly IDPSyncMapping[]; +} + // From codersdk/templateversions.go export interface PatchTemplateVersionRequest { readonly name: string; diff --git a/site/src/pages/TerminalPage/TerminalAlerts.tsx b/site/src/pages/TerminalPage/TerminalAlerts.tsx index 2876ad51aaa62..eb7369fc431b7 100644 --- a/site/src/pages/TerminalPage/TerminalAlerts.tsx +++ b/site/src/pages/TerminalPage/TerminalAlerts.tsx @@ -72,7 +72,9 @@ export const ErrorScriptAlert: FC = () => { The workspace{" "} diff --git a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx index 88f006681495e..52f3e725c6003 100644 --- a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx @@ -81,6 +81,7 @@ export const WorkspaceBuildProgress: FC = ({ useEffect(() => { const updateProgress = () => { if ( + job === undefined || job.status !== "running" || transitionStats.P50 === undefined || transitionStats.P95 === undefined ||