From f2963cd606ab0e5113a4e61808688693747b371d Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 30 Jul 2025 06:02:11 +0000 Subject: [PATCH 1/3] chore: add usage tracking package --- CODEOWNERS | 6 +- coderd/database/dbauthz/dbauthz.go | 22 + coderd/database/dbauthz/dbauthz_test.go | 24 + coderd/database/dbmetrics/querymetrics.go | 21 + coderd/database/dbmock/dbmock.go | 43 + coderd/database/dump.sql | 37 + .../000353_create_usage_events_table.down.sql | 2 + .../000353_create_usage_events_table.up.sql | 26 + coderd/database/modelmethods.go | 9 + coderd/database/models.go | 72 + coderd/database/querier.go | 9 + coderd/database/queries.sql.go | 145 + coderd/database/queries/usageevents.sql | 76 + coderd/database/unique_constraint.go | 1 + coderd/usage/collector.go | 29 + coderd/usage/events.go | 47 + docs/manifest.json | 3640 +++++++++-------- .../coderd/coderdenttest/coderdenttest.go | 32 +- enterprise/coderd/license/license.go | 1 + enterprise/coderd/usage/collector.go | 67 + enterprise/coderd/usage/collector_test.go | 85 + enterprise/coderd/usage/publisher.go | 427 ++ enterprise/coderd/usage/publisher_test.go | 702 ++++ 23 files changed, 3708 insertions(+), 1815 deletions(-) create mode 100644 coderd/database/migrations/000353_create_usage_events_table.down.sql create mode 100644 coderd/database/migrations/000353_create_usage_events_table.up.sql create mode 100644 coderd/database/queries/usageevents.sql create mode 100644 coderd/usage/collector.go create mode 100644 coderd/usage/events.go create mode 100644 enterprise/coderd/usage/collector.go create mode 100644 enterprise/coderd/usage/collector_test.go create mode 100644 enterprise/coderd/usage/publisher.go create mode 100644 enterprise/coderd/usage/publisher_test.go diff --git a/CODEOWNERS b/CODEOWNERS index e571a160b12b7..c94a6bd3f34af 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -7,7 +7,6 @@ tailnet/proto/ @spikecurtis @johnstcn vpn/vpn.proto @spikecurtis @johnstcn vpn/version.go @spikecurtis @johnstcn - # This caching code is particularly tricky, and one must be very careful when # altering it. coderd/files/ @aslilac @@ -29,3 +28,8 @@ site/src/api/countriesGenerated.ts site/src/api/rbacresourcesGenerated.ts site/src/api/typesGenerated.ts site/CLAUDE.md + +# Usage tracking code requires intimate knowledge of Tallyman and Metronome, as +# well as guidance from revenue. +coderd/usage/ @deansheather +enterprise/coderd/usage/ @deansheather diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 257cbc6e6b142..69ce7de485dd4 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3913,6 +3913,13 @@ func (q *querier) InsertTemplateVersionWorkspaceTag(ctx context.Context, arg dat return q.db.InsertTemplateVersionWorkspaceTag(ctx, arg) } +func (q *querier) InsertUsageEvent(ctx context.Context, arg database.InsertUsageEventParams) error { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.InsertUsageEvent(ctx, arg) +} + func (q *querier) InsertUser(ctx context.Context, arg database.InsertUserParams) (database.User, error) { // Always check if the assigned roles can actually be assigned by this actor. impliedRoles := append([]rbac.RoleIdentifier{rbac.RoleMember()}, q.convertToDeploymentRoles(arg.RBACRoles)...) @@ -4260,6 +4267,14 @@ func (q *querier) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) return q.db.RevokeDBCryptKey(ctx, activeKeyDigest) } +func (q *querier) SelectUsageEventsForPublishing(ctx context.Context, arg time.Time) ([]database.UsageEvent, error) { + // ActionUpdate because we're updating the publish_started_at column. + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.SelectUsageEventsForPublishing(ctx, arg) +} + func (q *querier) TryAcquireLock(ctx context.Context, id int64) (bool, error) { return q.db.TryAcquireLock(ctx, id) } @@ -4725,6 +4740,13 @@ func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg da return fetchAndExec(q.log, q.auth, policy.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg) } +func (q *querier) UpdateUsageEventsPostPublish(ctx context.Context, arg database.UpdateUsageEventsPostPublishParams) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.UpdateUsageEventsPostPublish(ctx, arg) +} + func (q *querier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { return deleteQ(q.log, q.auth, q.db.GetUserByID, q.db.UpdateUserDeletedByID)(ctx, id) } diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index bcf0caa95c365..ece99b30d9b4a 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -5845,3 +5845,27 @@ func (s *MethodTestSuite) TestAuthorizePrebuiltWorkspace() { }).Asserts(w, policy.ActionUpdate, w.AsPrebuild(), policy.ActionUpdate) })) } + +func (s *MethodTestSuite) TestUsageEvents() { + s.Run("InsertUsageEvent", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.InsertUsageEventParams{ + ID: "1", + EventType: database.UsageEventTypeDcManagedAgentsV1, + EventData: []byte("{}"), + CreatedAt: dbtime.Now(), + }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + })) + + s.Run("SelectUsageEventsForPublishing", s.Subtest(func(db database.Store, check *expects) { + check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + })) + + s.Run("UpdateUsageEventsPostPublish", s.Subtest(func(db database.Store, check *expects) { + check.Args(database.UpdateUsageEventsPostPublishParams{ + Now: dbtime.Now(), + IDs: []string{"1", "2"}, + FailureMessages: []string{"error", "error"}, + SetPublishedAts: []bool{false, false}, + }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + })) +} diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 811d945ac7da9..c126f4c23814f 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2371,6 +2371,13 @@ func (m queryMetricsStore) InsertTemplateVersionWorkspaceTag(ctx context.Context return r0, r1 } +func (m queryMetricsStore) InsertUsageEvent(ctx context.Context, arg database.InsertUsageEventParams) error { + start := time.Now() + r0 := m.s.InsertUsageEvent(ctx, arg) + m.queryLatencies.WithLabelValues("InsertUsageEvent").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) InsertUser(ctx context.Context, arg database.InsertUserParams) (database.User, error) { start := time.Now() user, err := m.s.InsertUser(ctx, arg) @@ -2623,6 +2630,13 @@ func (m queryMetricsStore) RevokeDBCryptKey(ctx context.Context, activeKeyDigest return r0 } +func (m queryMetricsStore) SelectUsageEventsForPublishing(ctx context.Context, arg time.Time) ([]database.UsageEvent, error) { + start := time.Now() + r0, r1 := m.s.SelectUsageEventsForPublishing(ctx, arg) + m.queryLatencies.WithLabelValues("SelectUsageEventsForPublishing").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) { start := time.Now() ok, err := m.s.TryAcquireLock(ctx, pgTryAdvisoryXactLock) @@ -2896,6 +2910,13 @@ func (m queryMetricsStore) UpdateTemplateWorkspacesLastUsedAt(ctx context.Contex return r0 } +func (m queryMetricsStore) UpdateUsageEventsPostPublish(ctx context.Context, arg database.UpdateUsageEventsPostPublishParams) error { + start := time.Now() + r0 := m.s.UpdateUsageEventsPostPublish(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateUsageEventsPostPublish").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { start := time.Now() r0 := m.s.UpdateUserDeletedByID(ctx, id) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index b20c3d06209b5..949061b178b7f 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -5063,6 +5063,20 @@ func (mr *MockStoreMockRecorder) InsertTemplateVersionWorkspaceTag(ctx, arg any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTemplateVersionWorkspaceTag", reflect.TypeOf((*MockStore)(nil).InsertTemplateVersionWorkspaceTag), ctx, arg) } +// InsertUsageEvent mocks base method. +func (m *MockStore) InsertUsageEvent(ctx context.Context, arg database.InsertUsageEventParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertUsageEvent", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertUsageEvent indicates an expected call of InsertUsageEvent. +func (mr *MockStoreMockRecorder) InsertUsageEvent(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUsageEvent", reflect.TypeOf((*MockStore)(nil).InsertUsageEvent), ctx, arg) +} + // InsertUser mocks base method. func (m *MockStore) InsertUser(ctx context.Context, arg database.InsertUserParams) (database.User, error) { m.ctrl.T.Helper() @@ -5623,6 +5637,21 @@ func (mr *MockStoreMockRecorder) RevokeDBCryptKey(ctx, activeKeyDigest any) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeDBCryptKey", reflect.TypeOf((*MockStore)(nil).RevokeDBCryptKey), ctx, activeKeyDigest) } +// SelectUsageEventsForPublishing mocks base method. +func (m *MockStore) SelectUsageEventsForPublishing(ctx context.Context, now time.Time) ([]database.UsageEvent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SelectUsageEventsForPublishing", ctx, now) + ret0, _ := ret[0].([]database.UsageEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SelectUsageEventsForPublishing indicates an expected call of SelectUsageEventsForPublishing. +func (mr *MockStoreMockRecorder) SelectUsageEventsForPublishing(ctx, now any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectUsageEventsForPublishing", reflect.TypeOf((*MockStore)(nil).SelectUsageEventsForPublishing), ctx, now) +} + // TryAcquireLock mocks base method. func (m *MockStore) TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) { m.ctrl.T.Helper() @@ -6183,6 +6212,20 @@ func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(ctx, arg any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateWorkspacesLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateTemplateWorkspacesLastUsedAt), ctx, arg) } +// UpdateUsageEventsPostPublish mocks base method. +func (m *MockStore) UpdateUsageEventsPostPublish(ctx context.Context, arg database.UpdateUsageEventsPostPublishParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUsageEventsPostPublish", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUsageEventsPostPublish indicates an expected call of UpdateUsageEventsPostPublish. +func (mr *MockStoreMockRecorder) UpdateUsageEventsPostPublish(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUsageEventsPostPublish", reflect.TypeOf((*MockStore)(nil).UpdateUsageEventsPostPublish), ctx, arg) +} + // UpdateUserDeletedByID mocks base method. func (m *MockStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 67d58ad05c802..3a1fa23c6595b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -281,6 +281,12 @@ CREATE TYPE tailnet_status AS ENUM ( 'lost' ); +CREATE TYPE usage_event_type AS ENUM ( + 'dc_managed_agents_v1' +); + +COMMENT ON TYPE usage_event_type IS 'The usage event type with version. "dc" means "discrete" (e.g. a single event, for counters), "hb" means "heartbeat" (e.g. a recurring event that contains a total count of usage generated from the database, for gauges).'; + CREATE TYPE user_status AS ENUM ( 'active', 'suspended', @@ -1815,6 +1821,28 @@ CREATE VIEW template_with_names AS COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.'; +CREATE TABLE usage_events ( + id text NOT NULL, + event_type usage_event_type NOT NULL, + event_data jsonb NOT NULL, + created_at timestamp with time zone NOT NULL, + publish_started_at timestamp with time zone, + published_at timestamp with time zone, + failure_message text +); + +COMMENT ON TABLE usage_events IS 'usage_events contains usage data that is collected from the product and potentially shipped to the usage collector service.'; + +COMMENT ON COLUMN usage_events.id IS 'For "discrete" event types, this is a random UUID. For "heartbeat" event types, this is a combination of the event type and a truncated timestamp.'; + +COMMENT ON COLUMN usage_events.event_data IS 'Event payload. Determined by the matching usage struct for this event type.'; + +COMMENT ON COLUMN usage_events.publish_started_at IS 'Set to a timestamp while the event is being published by a Coder replica to the usage collector service. Used to avoid duplicate publishes by multiple replicas. Timestamps older than 1 hour are considered expired.'; + +COMMENT ON COLUMN usage_events.published_at IS 'Set to a timestamp when the event is successfully (or permanently unsuccessfully) published to the usage collector service. If set, the event should never be attempted to be published again.'; + +COMMENT ON COLUMN usage_events.failure_message IS 'Set to an error message when the event is temporarily or permanently unsuccessfully published to the usage collector service.'; + CREATE TABLE user_configs ( user_id uuid NOT NULL, key character varying(256) NOT NULL, @@ -2647,6 +2675,9 @@ ALTER TABLE ONLY template_versions ALTER TABLE ONLY templates ADD CONSTRAINT templates_pkey PRIMARY KEY (id); +ALTER TABLE ONLY usage_events + ADD CONSTRAINT usage_events_pkey PRIMARY KEY (id); + ALTER TABLE ONLY user_configs ADD CONSTRAINT user_configs_pkey PRIMARY KEY (user_id, key); @@ -2812,6 +2843,12 @@ CREATE INDEX idx_template_versions_has_ai_task ON template_versions USING btree CREATE UNIQUE INDEX idx_unique_preset_name ON template_version_presets USING btree (name, template_version_id); +CREATE INDEX idx_usage_events_created_at ON usage_events USING btree (created_at); + +CREATE INDEX idx_usage_events_publish_started_at ON usage_events USING btree (publish_started_at); + +CREATE INDEX idx_usage_events_published_at ON usage_events USING btree (published_at); + CREATE INDEX idx_user_deleted_deleted_at ON user_deleted USING btree (deleted_at); CREATE INDEX idx_user_status_changes_changed_at ON user_status_changes USING btree (changed_at); diff --git a/coderd/database/migrations/000353_create_usage_events_table.down.sql b/coderd/database/migrations/000353_create_usage_events_table.down.sql new file mode 100644 index 0000000000000..eacce867e0acc --- /dev/null +++ b/coderd/database/migrations/000353_create_usage_events_table.down.sql @@ -0,0 +1,2 @@ +DROP TABLE usage_events; +DROP TYPE usage_event_type; diff --git a/coderd/database/migrations/000353_create_usage_events_table.up.sql b/coderd/database/migrations/000353_create_usage_events_table.up.sql new file mode 100644 index 0000000000000..d15dcbaaad050 --- /dev/null +++ b/coderd/database/migrations/000353_create_usage_events_table.up.sql @@ -0,0 +1,26 @@ +CREATE TYPE usage_event_type AS ENUM ( + 'dc_managed_agents_v1' +); + +COMMENT ON TYPE usage_event_type IS 'The usage event type with version. "dc" means "discrete" (e.g. a single event, for counters), "hb" means "heartbeat" (e.g. a recurring event that contains a total count of usage generated from the database, for gauges).'; + +CREATE TABLE usage_events ( + id TEXT PRIMARY KEY, + event_type usage_event_type NOT NULL, + event_data JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + publish_started_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + published_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + failure_message TEXT DEFAULT NULL +); + +COMMENT ON TABLE usage_events IS 'usage_events contains usage data that is collected from the product and potentially shipped to the usage collector service.'; +COMMENT ON COLUMN usage_events.id IS 'For "discrete" event types, this is a random UUID. For "heartbeat" event types, this is a combination of the event type and a truncated timestamp.'; +COMMENT ON COLUMN usage_events.event_data IS 'Event payload. Determined by the matching usage struct for this event type.'; +COMMENT ON COLUMN usage_events.publish_started_at IS 'Set to a timestamp while the event is being published by a Coder replica to the usage collector service. Used to avoid duplicate publishes by multiple replicas. Timestamps older than 1 hour are considered expired.'; +COMMENT ON COLUMN usage_events.published_at IS 'Set to a timestamp when the event is successfully (or permanently unsuccessfully) published to the usage collector service. If set, the event should never be attempted to be published again.'; +COMMENT ON COLUMN usage_events.failure_message IS 'Set to an error message when the event is temporarily or permanently unsuccessfully published to the usage collector service.'; + +CREATE INDEX idx_usage_events_created_at ON usage_events (created_at); +CREATE INDEX idx_usage_events_publish_started_at ON usage_events (publish_started_at); +CREATE INDEX idx_usage_events_published_at ON usage_events (published_at); diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index b49fa113d4b12..b326890d0f184 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "sort" "strconv" + "strings" "time" "github.com/google/uuid" @@ -628,3 +629,11 @@ func (m WorkspaceAgentVolumeResourceMonitor) Debounce( return m.DebouncedUntil, false } + +func (e UsageEventType) IsDiscrete() bool { + return e.Valid() && strings.HasPrefix(string(e), "dc_") +} + +func (e UsageEventType) IsHeartbeat() bool { + return e.Valid() && strings.HasPrefix(string(e), "hb_") +} diff --git a/coderd/database/models.go b/coderd/database/models.go index 094bc98c68373..f7c0d27199f9e 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2254,6 +2254,62 @@ func AllTailnetStatusValues() []TailnetStatus { } } +// The usage event type with version. "dc" means "discrete" (e.g. a single event, for counters), "hb" means "heartbeat" (e.g. a recurring event that contains a total count of usage generated from the database, for gauges). +type UsageEventType string + +const ( + UsageEventTypeDcManagedAgentsV1 UsageEventType = "dc_managed_agents_v1" +) + +func (e *UsageEventType) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = UsageEventType(s) + case string: + *e = UsageEventType(s) + default: + return fmt.Errorf("unsupported scan type for UsageEventType: %T", src) + } + return nil +} + +type NullUsageEventType struct { + UsageEventType UsageEventType `json:"usage_event_type"` + Valid bool `json:"valid"` // Valid is true if UsageEventType is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullUsageEventType) Scan(value interface{}) error { + if value == nil { + ns.UsageEventType, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.UsageEventType.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullUsageEventType) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.UsageEventType), nil +} + +func (e UsageEventType) Valid() bool { + switch e { + case UsageEventTypeDcManagedAgentsV1: + return true + } + return false +} + +func AllUsageEventTypeValues() []UsageEventType { + return []UsageEventType{ + UsageEventTypeDcManagedAgentsV1, + } +} + // Defines the users status: active, dormant, or suspended. type UserStatus string @@ -3693,6 +3749,22 @@ type TemplateVersionWorkspaceTag struct { Value string `db:"value" json:"value"` } +// usage_events contains usage data that is collected from the product and potentially shipped to the usage collector service. +type UsageEvent struct { + // For "discrete" event types, this is a random UUID. For "heartbeat" event types, this is a combination of the event type and a truncated timestamp. + ID string `db:"id" json:"id"` + EventType UsageEventType `db:"event_type" json:"event_type"` + // Event payload. Determined by the matching usage struct for this event type. + EventData json.RawMessage `db:"event_data" json:"event_data"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + // Set to a timestamp while the event is being published by a Coder replica to the usage collector service. Used to avoid duplicate publishes by multiple replicas. Timestamps older than 1 hour are considered expired. + PublishStartedAt sql.NullTime `db:"publish_started_at" json:"publish_started_at"` + // Set to a timestamp when the event is successfully (or permanently unsuccessfully) published to the usage collector service. If set, the event should never be attempted to be published again. + PublishedAt sql.NullTime `db:"published_at" json:"published_at"` + // Set to an error message when the event is temporarily or permanently unsuccessfully published to the usage collector service. + FailureMessage sql.NullString `db:"failure_message" json:"failure_message"` +} + type User struct { ID uuid.UUID `db:"id" json:"id"` Email string `db:"email" json:"email"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index baa5d8590b1d7..3585088055e6e 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -520,6 +520,9 @@ type sqlcQuerier interface { InsertTemplateVersionTerraformValuesByJobID(ctx context.Context, arg InsertTemplateVersionTerraformValuesByJobIDParams) error InsertTemplateVersionVariable(ctx context.Context, arg InsertTemplateVersionVariableParams) (TemplateVersionVariable, error) InsertTemplateVersionWorkspaceTag(ctx context.Context, arg InsertTemplateVersionWorkspaceTagParams) (TemplateVersionWorkspaceTag, error) + // Duplicate events are ignored intentionally to allow for multiple replicas to + // publish heartbeat events. + InsertUsageEvent(ctx context.Context, arg InsertUsageEventParams) error InsertUser(ctx context.Context, arg InsertUserParams) (User, error) // InsertUserGroupsByID adds a user to all provided groups, if they exist. // If there is a conflict, the user is already a member @@ -565,6 +568,11 @@ type sqlcQuerier interface { RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error + // Note that this selects from the CTE, not the original table. The CTE is named + // the same as the original table to trick sqlc into reusing the existing struct + // for the table. + // The CTE and the reorder is required because UPDATE doesn't guarantee order. + SelectUsageEventsForPublishing(ctx context.Context, now time.Time) ([]UsageEvent, error) // Non blocking lock. Returns true if the lock was acquired, false otherwise. // // This must be called from within a transaction. The lock will be automatically @@ -609,6 +617,7 @@ type sqlcQuerier interface { UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error + UpdateUsageEventsPostPublish(ctx context.Context, arg UpdateUsageEventsPostPublishParams) error UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error UpdateUserHashedOneTimePasscode(ctx context.Context, arg UpdateUserHashedOneTimePasscodeParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 80357b3731874..a6a7cefc59f6b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -13361,6 +13361,151 @@ func (q *sqlQuerier) DisableForeignKeysAndTriggers(ctx context.Context) error { return err } +const insertUsageEvent = `-- name: InsertUsageEvent :exec +INSERT INTO + usage_events ( + id, + event_type, + event_data, + created_at, + publish_started_at, + published_at, + failure_message + ) +VALUES + ($1, $2::usage_event_type, $3, $4, NULL, NULL, NULL) +ON CONFLICT (id) DO NOTHING +` + +type InsertUsageEventParams struct { + ID string `db:"id" json:"id"` + EventType UsageEventType `db:"event_type" json:"event_type"` + EventData json.RawMessage `db:"event_data" json:"event_data"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +// Duplicate events are ignored intentionally to allow for multiple replicas to +// publish heartbeat events. +func (q *sqlQuerier) InsertUsageEvent(ctx context.Context, arg InsertUsageEventParams) error { + _, err := q.db.ExecContext(ctx, insertUsageEvent, + arg.ID, + arg.EventType, + arg.EventData, + arg.CreatedAt, + ) + return err +} + +const selectUsageEventsForPublishing = `-- name: SelectUsageEventsForPublishing :many +WITH usage_events AS ( + UPDATE + usage_events + SET + publish_started_at = $1::timestamptz + WHERE + id IN ( + SELECT + potential_event.id + FROM + usage_events potential_event + WHERE + -- We do not publish events older than 30 days. Tallyman will + -- always permanently reject these events anyways. + -- The parenthesis around @now::timestamptz are necessary to + -- avoid sqlc from generating an extra argument. + potential_event.created_at > ($1::timestamptz) - INTERVAL '30 days' + AND potential_event.published_at IS NULL + AND ( + potential_event.publish_started_at IS NULL + -- If the event has publish_started_at set, it must be older + -- than an hour ago. This is so we can retry publishing + -- events where the replica exited or couldn't update the + -- row. + -- Also, same parenthesis thing here: + OR potential_event.publish_started_at < ($1::timestamptz) - INTERVAL '1 hour' + ) + ORDER BY potential_event.created_at ASC + FOR UPDATE SKIP LOCKED + LIMIT 100 + ) + RETURNING id, event_type, event_data, created_at, publish_started_at, published_at, failure_message +) +SELECT id, event_type, event_data, created_at, publish_started_at, published_at, failure_message +FROM usage_events +ORDER BY created_at ASC +` + +// Note that this selects from the CTE, not the original table. The CTE is named +// the same as the original table to trick sqlc into reusing the existing struct +// for the table. +// The CTE and the reorder is required because UPDATE doesn't guarantee order. +func (q *sqlQuerier) SelectUsageEventsForPublishing(ctx context.Context, now time.Time) ([]UsageEvent, error) { + rows, err := q.db.QueryContext(ctx, selectUsageEventsForPublishing, now) + if err != nil { + return nil, err + } + defer rows.Close() + var items []UsageEvent + for rows.Next() { + var i UsageEvent + if err := rows.Scan( + &i.ID, + &i.EventType, + &i.EventData, + &i.CreatedAt, + &i.PublishStartedAt, + &i.PublishedAt, + &i.FailureMessage, + ); 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 updateUsageEventsPostPublish = `-- name: UpdateUsageEventsPostPublish :exec +UPDATE + usage_events +SET + publish_started_at = NULL, + published_at = CASE WHEN input.set_published_at THEN $1::timestamptz ELSE NULL END, + failure_message = NULLIF(input.failure_message, '') +FROM ( + SELECT + UNNEST($2::text[]) AS id, + UNNEST($3::text[]) AS failure_message, + UNNEST($4::boolean[]) AS set_published_at +) input +WHERE + input.id = usage_events.id + AND cardinality($2::text[]) = cardinality($3::text[]) + AND cardinality($2::text[]) = cardinality($4::boolean[]) +` + +type UpdateUsageEventsPostPublishParams struct { + Now time.Time `db:"now" json:"now"` + IDs []string `db:"ids" json:"ids"` + FailureMessages []string `db:"failure_messages" json:"failure_messages"` + SetPublishedAts []bool `db:"set_published_ats" json:"set_published_ats"` +} + +func (q *sqlQuerier) UpdateUsageEventsPostPublish(ctx context.Context, arg UpdateUsageEventsPostPublishParams) error { + _, err := q.db.ExecContext(ctx, updateUsageEventsPostPublish, + arg.Now, + pq.Array(arg.IDs), + pq.Array(arg.FailureMessages), + pq.Array(arg.SetPublishedAts), + ) + return err +} + const getUserLinkByLinkedID = `-- name: GetUserLinkByLinkedID :one SELECT user_links.user_id, user_links.login_type, user_links.linked_id, user_links.oauth_access_token, user_links.oauth_refresh_token, user_links.oauth_expiry, user_links.oauth_access_token_key_id, user_links.oauth_refresh_token_key_id, user_links.claims diff --git a/coderd/database/queries/usageevents.sql b/coderd/database/queries/usageevents.sql new file mode 100644 index 0000000000000..2ec4ec419968e --- /dev/null +++ b/coderd/database/queries/usageevents.sql @@ -0,0 +1,76 @@ +-- name: InsertUsageEvent :exec +-- Duplicate events are ignored intentionally to allow for multiple replicas to +-- publish heartbeat events. +INSERT INTO + usage_events ( + id, + event_type, + event_data, + created_at, + publish_started_at, + published_at, + failure_message + ) +VALUES + (@id, @event_type::usage_event_type, @event_data, @created_at, NULL, NULL, NULL) +ON CONFLICT (id) DO NOTHING; + +-- name: SelectUsageEventsForPublishing :many +WITH usage_events AS ( + UPDATE + usage_events + SET + publish_started_at = @now::timestamptz + WHERE + id IN ( + SELECT + potential_event.id + FROM + usage_events potential_event + WHERE + -- We do not publish events older than 30 days. Tallyman will + -- always permanently reject these events anyways. + -- The parenthesis around @now::timestamptz are necessary to + -- avoid sqlc from generating an extra argument. + potential_event.created_at > (@now::timestamptz) - INTERVAL '30 days' + AND potential_event.published_at IS NULL + AND ( + potential_event.publish_started_at IS NULL + -- If the event has publish_started_at set, it must be older + -- than an hour ago. This is so we can retry publishing + -- events where the replica exited or couldn't update the + -- row. + -- Also, same parenthesis thing here: + OR potential_event.publish_started_at < (@now::timestamptz) - INTERVAL '1 hour' + ) + ORDER BY potential_event.created_at ASC + FOR UPDATE SKIP LOCKED + LIMIT 100 + ) + RETURNING * +) +SELECT * +-- Note that this selects from the CTE, not the original table. The CTE is named +-- the same as the original table to trick sqlc into reusing the existing struct +-- for the table. +FROM usage_events +-- The CTE and the reorder is required because UPDATE doesn't guarantee order. +ORDER BY created_at ASC; + +-- name: UpdateUsageEventsPostPublish :exec +UPDATE + usage_events +SET + publish_started_at = NULL, + published_at = CASE WHEN input.set_published_at THEN @now::timestamptz ELSE NULL END, + failure_message = NULLIF(input.failure_message, '') +FROM ( + SELECT + UNNEST(@ids::text[]) AS id, + UNNEST(@failure_messages::text[]) AS failure_message, + UNNEST(@set_published_ats::boolean[]) AS set_published_at +) input +WHERE + input.id = usage_events.id + AND cardinality(@ids::text[]) = cardinality(@failure_messages::text[]) + AND cardinality(@ids::text[]) = cardinality(@set_published_ats::boolean[]); diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 38c95e67410c9..45e7a5acf3980 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -67,6 +67,7 @@ const ( UniqueTemplateVersionsPkey UniqueConstraint = "template_versions_pkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_pkey PRIMARY KEY (id); UniqueTemplateVersionsTemplateIDNameKey UniqueConstraint = "template_versions_template_id_name_key" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_name_key UNIQUE (template_id, name); UniqueTemplatesPkey UniqueConstraint = "templates_pkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_pkey PRIMARY KEY (id); + UniqueUsageEventsPkey UniqueConstraint = "usage_events_pkey" // ALTER TABLE ONLY usage_events ADD CONSTRAINT usage_events_pkey PRIMARY KEY (id); UniqueUserConfigsPkey UniqueConstraint = "user_configs_pkey" // ALTER TABLE ONLY user_configs ADD CONSTRAINT user_configs_pkey PRIMARY KEY (user_id, key); UniqueUserDeletedPkey UniqueConstraint = "user_deleted_pkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_pkey PRIMARY KEY (id); UniqueUserLinksPkey UniqueConstraint = "user_links_pkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_pkey PRIMARY KEY (user_id, login_type); diff --git a/coderd/usage/collector.go b/coderd/usage/collector.go new file mode 100644 index 0000000000000..1a2e16ea43f01 --- /dev/null +++ b/coderd/usage/collector.go @@ -0,0 +1,29 @@ +package usage + +import ( + "context" + + "github.com/coder/coder/v2/coderd/database" +) + +// Collector is a sink for usage events generated by the product. +type Collector interface { + // CollectDiscreteUsageEvent writes a discrete usage event to the database + // with the given database or transaction. + CollectDiscreteUsageEvent(ctx context.Context, db database.Store, event DiscreteEvent) error +} + +// AGPLCollector is a no-op implementation of Collector. +type AGPLCollector struct{} + +var _ Collector = AGPLCollector{} + +func NewAGPLCollector() Collector { + return AGPLCollector{} +} + +// CollectDiscreteUsageEvent is a no-op implementation of +// CollectDiscreteUsageEvent. +func (AGPLCollector) CollectDiscreteUsageEvent(_ context.Context, _ database.Store, _ DiscreteEvent) error { + return nil +} diff --git a/coderd/usage/events.go b/coderd/usage/events.go new file mode 100644 index 0000000000000..705435a96a244 --- /dev/null +++ b/coderd/usage/events.go @@ -0,0 +1,47 @@ +package usage + +import ( + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" +) + +// Event is a usage event that can be collected by the usage collector. +// +// Note that the following event types should not be updated once they are +// merged into the product. Please consult Dean before making any changes. +type Event interface { + usageEvent() // to prevent external types from implementing this interface + EventType() database.UsageEventType + Valid() error +} + +// DiscreteEvent is a usage event that is collected as a discrete event. +type DiscreteEvent interface { + Event + discreteUsageEvent() // marker method, also prevents external types from implementing this interface +} + +// DCManagedAgentsV1 is a discrete usage event for the number of managed agents. +// This event is sent in the following situations: +// - Once on first startup after usage tracking is added to the product with +// the count of all existing managed agents (count=N) +// - A new managed agent is created (count=1) +type DCManagedAgentsV1 struct { + Count uint64 `json:"count"` +} + +var _ DiscreteEvent = DCManagedAgentsV1{} + +func (DCManagedAgentsV1) usageEvent() {} +func (DCManagedAgentsV1) discreteUsageEvent() {} +func (DCManagedAgentsV1) EventType() database.UsageEventType { + return database.UsageEventTypeDcManagedAgentsV1 +} + +func (e DCManagedAgentsV1) Valid() error { + if e.Count == 0 { + return xerrors.New("count must be greater than 0") + } + return nil +} diff --git a/docs/manifest.json b/docs/manifest.json index 0305105c029fd..965963cf0f2eb 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1,1800 +1,1842 @@ { - "versions": ["main"], - "routes": [ - { - "title": "About", - "description": "Coder docs", - "path": "./README.md", - "icon_path": "./images/icons/home.svg", - "children": [ - { - "title": "Screenshots", - "description": "View screenshots of the Coder platform", - "path": "./about/screenshots.md" - }, - { - "title": "Quickstart", - "description": "Learn how to install and run Coder quickly", - "path": "./tutorials/quickstart.md" - }, - { - "title": "Support", - "description": "How Coder supports your deployment and you", - "path": "./support/index.md", - "children": [ - { - "title": "Generate a Support Bundle", - "description": "Generate and upload a Support Bundle to Coder Support", - "path": "./support/support-bundle.md" - } - ] - }, - { - "title": "Contributing", - "description": "Learn how to contribute to Coder", - "path": "./about/contributing/CONTRIBUTING.md", - "icon_path": "./images/icons/contributing.svg", - "children": [ - { - "title": "Code of Conduct", - "description": "See the code of conduct for contributing to Coder", - "path": "./about/contributing/CODE_OF_CONDUCT.md", - "icon_path": "./images/icons/circle-dot.svg" - }, - { - "title": "Documentation", - "description": "Our style guide for use when authoring documentation", - "path": "./about/contributing/documentation.md", - "icon_path": "./images/icons/document.svg" - }, - { - "title": "Modules", - "description": "Learn how to contribute modules to Coder", - "path": "./about/contributing/modules.md", - "icon_path": "./images/icons/gear.svg" - }, - { - "title": "Templates", - "description": "Learn how to contribute templates to Coder", - "path": "./about/contributing/templates.md", - "icon_path": "./images/icons/picture.svg" - }, - { - "title": "Backend", - "description": "Our guide for backend development", - "path": "./about/contributing/backend.md", - "icon_path": "./images/icons/gear.svg" - }, - { - "title": "Frontend", - "description": "Our guide for frontend development", - "path": "./about/contributing/frontend.md", - "icon_path": "./images/icons/frontend.svg" - }, - { - "title": "Security", - "description": "Security vulnerability disclosure policy", - "path": "./about/contributing/SECURITY.md", - "icon_path": "./images/icons/lock.svg" - } - ] - } - ] - }, - { - "title": "Install", - "description": "Installing Coder", - "path": "./install/index.md", - "icon_path": "./images/icons/download.svg", - "children": [ - { - "title": "Coder CLI", - "description": "Install the standalone binary", - "path": "./install/cli.md", - "icon_path": "./images/icons/terminal.svg" - }, - { - "title": "Docker", - "description": "Install Coder using Docker", - "path": "./install/docker.md", - "icon_path": "./images/icons/docker.svg" - }, - { - "title": "Kubernetes", - "description": "Install Coder on Kubernetes", - "path": "./install/kubernetes.md", - "icon_path": "./images/icons/kubernetes.svg", - "children": [ - { - "title": "Deploy Coder on Azure with an Application Gateway", - "description": "Deploy Coder on Azure with an Application Gateway", - "path": "./install/kubernetes/kubernetes-azure-app-gateway.md" - } - ] - }, - { - "title": "Rancher", - "description": "Deploy Coder on Rancher", - "path": "./install/rancher.md", - "icon_path": "./images/icons/rancher.svg" - }, - { - "title": "OpenShift", - "description": "Install Coder on OpenShift", - "path": "./install/openshift.md", - "icon_path": "./images/icons/openshift.svg" - }, - { - "title": "Cloud Providers", - "description": "Install Coder on cloud providers", - "path": "./install/cloud/index.md", - "icon_path": "./images/icons/cloud.svg", - "children": [ - { - "title": "AWS EC2", - "description": "Install Coder on AWS EC2", - "path": "./install/cloud/ec2.md" - }, - { - "title": "GCP Compute Engine", - "description": "Install Coder on GCP Compute Engine", - "path": "./install/cloud/compute-engine.md" - }, - { - "title": "Azure VM", - "description": "Install Coder on an Azure VM", - "path": "./install/cloud/azure-vm.md" - } - ] - }, - { - "title": "Offline Deployments", - "description": "Run Coder in offline / air-gapped environments", - "path": "./install/offline.md", - "icon_path": "./images/icons/lan.svg" - }, - { - "title": "Unofficial Install Methods", - "description": "Other installation methods", - "path": "./install/other/index.md", - "icon_path": "./images/icons/generic.svg" - }, - { - "title": "Upgrading", - "description": "Learn how to upgrade Coder", - "path": "./install/upgrade.md", - "icon_path": "./images/icons/upgrade.svg" - }, - { - "title": "Uninstall", - "description": "Learn how to uninstall Coder", - "path": "./install/uninstall.md", - "icon_path": "./images/icons/trash.svg" - }, - { - "title": "Releases", - "description": "Learn about the Coder release channels and schedule", - "path": "./install/releases/index.md", - "icon_path": "./images/icons/star.svg", - "children": [ - { - "title": "Feature stages", - "description": "Information about pre-GA stages.", - "path": "./install/releases/feature-stages.md" - } - ] - } - ] - }, - { - "title": "User Guides", - "description": "Guides for end-users of Coder", - "path": "./user-guides/index.md", - "icon_path": "./images/icons/users.svg", - "children": [ - { - "title": "Access Workspaces", - "description": "Connect to your Coder workspaces", - "path": "./user-guides/workspace-access/index.md", - "icon_path": "./images/icons/access.svg", - "children": [ - { - "title": "Visual Studio Code", - "description": "Use VSCode with Coder in the desktop or browser", - "path": "./user-guides/workspace-access/vscode.md" - }, - { - "title": "JetBrains IDEs", - "description": "Use JetBrains IDEs with Coder", - "path": "./user-guides/workspace-access/jetbrains/index.md", - "children": [ - { - "title": "JetBrains Fleet", - "description": "Connect JetBrains Fleet to a Coder workspace", - "path": "./user-guides/workspace-access/jetbrains/fleet.md" - }, - { - "title": "JetBrains Gateway", - "description": "Use JetBrains Gateway to connect to Coder workspaces", - "path": "./user-guides/workspace-access/jetbrains/gateway.md" - }, - { - "title": "JetBrains Toolbox", - "description": "Access Coder workspaces from JetBrains Toolbox", - "path": "./user-guides/workspace-access/jetbrains/toolbox.md", - "state": ["beta"] - } - ] - }, - { - "title": "Remote Desktop", - "description": "Use RDP in Coder", - "path": "./user-guides/workspace-access/remote-desktops.md" - }, - { - "title": "Emacs TRAMP", - "description": "Use Emacs TRAMP in Coder", - "path": "./user-guides/workspace-access/emacs-tramp.md" - }, - { - "title": "Port Forwarding", - "description": "Access ports on your workspace", - "path": "./user-guides/workspace-access/port-forwarding.md" - }, - { - "title": "Filebrowser", - "description": "Access your workspace files", - "path": "./user-guides/workspace-access/filebrowser.md" - }, - { - "title": "Web IDEs and Coder Apps", - "description": "Access your workspace with IDEs in the browser", - "path": "./user-guides/workspace-access/web-ides.md" - }, - { - "title": "Zed", - "description": "Access your workspace with Zed", - "path": "./user-guides/workspace-access/zed.md" - }, - { - "title": "Cursor", - "description": "Access your workspace with Cursor", - "path": "./user-guides/workspace-access/cursor.md" - }, - { - "title": "Windsurf", - "description": "Access your workspace with Windsurf", - "path": "./user-guides/workspace-access/windsurf.md" - } - ] - }, - { - "title": "Coder Desktop", - "description": "Transform remote workspaces into seamless local development environments with no port forwarding required", - "path": "./user-guides/desktop/index.md", - "icon_path": "./images/icons/computer-code.svg", - "children": [ - { - "title": "Coder Desktop connect and sync", - "description": "Use Coder Desktop to manage your workspace code and files locally", - "path": "./user-guides/desktop/desktop-connect-sync.md" - } - ] - }, - { - "title": "Workspace Management", - "description": "Manage workspaces", - "path": "./user-guides/workspace-management.md", - "icon_path": "./images/icons/generic.svg" - }, - { - "title": "Workspace Scheduling", - "description": "Cost control with workspace schedules", - "path": "./user-guides/workspace-scheduling.md", - "icon_path": "./images/icons/stopwatch.svg" - }, - { - "title": "Workspace Lifecycle", - "description": "A guide to the workspace lifecycle, from creation and status through stopping and deletion.", - "path": "./user-guides/workspace-lifecycle.md", - "icon_path": "./images/icons/circle-dot.svg" - }, - { - "title": "Dev Containers Integration", - "description": "Run containerized development environments in your Coder workspace using the dev containers specification.", - "path": "./user-guides/devcontainers/index.md", - "icon_path": "./images/icons/container.svg", - "children": [ - { - "title": "Working with dev containers", - "description": "Access dev containers via SSH, your IDE, or web terminal.", - "path": "./user-guides/devcontainers/working-with-dev-containers.md" - }, - { - "title": "Troubleshooting dev containers", - "description": "Diagnose and resolve common issues with dev containers in your Coder workspace.", - "path": "./user-guides/devcontainers/troubleshooting-dev-containers.md" - } - ] - }, - { - "title": "Dotfiles", - "description": "Personalize your environment with dotfiles", - "path": "./user-guides/workspace-dotfiles.md", - "icon_path": "./images/icons/art-pad.svg" - } - ] - }, - { - "title": "Administration", - "description": "Guides for template and deployment administrators", - "path": "./admin/index.md", - "icon_path": "./images/icons/wrench.svg", - "children": [ - { - "title": "Setup", - "description": "Configure user access to your control plane.", - "path": "./admin/setup/index.md", - "icon_path": "./images/icons/toggle_on.svg", - "children": [ - { - "title": "Appearance", - "description": "Learn how to configure the appearance of Coder", - "path": "./admin/setup/appearance.md", - "state": ["premium"] - }, - { - "title": "Telemetry", - "description": "Learn what usage telemetry Coder collects", - "path": "./admin/setup/telemetry.md" - } - ] - }, - { - "title": "Infrastructure", - "description": "How to integrate Coder with your organization's compute", - "path": "./admin/infrastructure/index.md", - "icon_path": "./images/icons/container.svg", - "children": [ - { - "title": "Architecture", - "description": "Learn about Coder's architecture", - "path": "./admin/infrastructure/architecture.md" - }, - { - "title": "Validated Architectures", - "description": "Architectures for large Coder deployments", - "path": "./admin/infrastructure/validated-architectures/index.md", - "children": [ - { - "title": "Up to 1,000 Users", - "description": "Hardware specifications and architecture guidance for Coder deployments that support up to 1,000 users", - "path": "./admin/infrastructure/validated-architectures/1k-users.md" - }, - { - "title": "Up to 2,000 Users", - "description": "Hardware specifications and architecture guidance for Coder deployments that support up to 2,000 users", - "path": "./admin/infrastructure/validated-architectures/2k-users.md" - }, - { - "title": "Up to 3,000 Users", - "description": "Enterprise-scale architecture recommendations for Coder deployments that support up to 3,000 users", - "path": "./admin/infrastructure/validated-architectures/3k-users.md" - } - ] - }, - { - "title": "Scale Testing", - "description": "Ensure your deployment can handle your organization's needs", - "path": "./admin/infrastructure/scale-testing.md" - }, - { - "title": "Scaling Utilities", - "description": "Tools to help you scale your deployment", - "path": "./admin/infrastructure/scale-utility.md" - }, - { - "title": "Scaling best practices", - "description": "How to prepare a Coder deployment for scale", - "path": "./tutorials/best-practices/scale-coder.md" - } - ] - }, - { - "title": "Users", - "description": "Learn how to manage and audit users", - "path": "./admin/users/index.md", - "icon_path": "./images/icons/users.svg", - "children": [ - { - "title": "OIDC Authentication", - "description": "Configure OpenID Connect authentication with identity providers like Okta or Active Directory", - "path": "./admin/users/oidc-auth/index.md", - "children": [ - { - "title": "Configure OIDC refresh tokens", - "description": "How to configure OIDC refresh tokens", - "path": "./admin/users/oidc-auth/refresh-tokens.md" - } - ] - }, - { - "title": "GitHub Authentication", - "description": "Set up authentication through GitHub OAuth to enable secure user login and sign-up", - "path": "./admin/users/github-auth.md" - }, - { - "title": "Password Authentication", - "description": "Manage username/password authentication settings and user password reset workflows", - "path": "./admin/users/password-auth.md" - }, - { - "title": "Headless Authentication", - "description": "Create and manage headless service accounts for automated systems and API integrations", - "path": "./admin/users/headless-auth.md" - }, - { - "title": "Groups \u0026 Roles", - "description": "Manage access control with user groups and role-based permissions for Coder resources", - "path": "./admin/users/groups-roles.md", - "state": ["premium"] - }, - { - "title": "IdP Sync", - "description": "Synchronize user groups, roles, and organizations from your identity provider to Coder", - "path": "./admin/users/idp-sync.md", - "state": ["premium"] - }, - { - "title": "Organizations", - "description": "Segment and isolate resources by creating separate organizations for different teams or projects", - "path": "./admin/users/organizations.md", - "state": ["premium"] - }, - { - "title": "Quotas", - "description": "Control resource usage by implementing workspace budgets and credit-based cost management", - "path": "./admin/users/quotas.md", - "state": ["premium"] - }, - { - "title": "Sessions \u0026 API Tokens", - "description": "Manage authentication tokens for API access and configure session duration policies", - "path": "./admin/users/sessions-tokens.md" - } - ] - }, - { - "title": "Templates", - "description": "Learn how to author and maintain Coder templates", - "path": "./admin/templates/index.md", - "icon_path": "./images/icons/picture.svg", - "children": [ - { - "title": "Creating Templates", - "description": "Learn how to create templates with Terraform", - "path": "./admin/templates/creating-templates.md" - }, - { - "title": "Managing Templates", - "description": "Learn how to manage templates and best practices", - "path": "./admin/templates/managing-templates/index.md", - "children": [ - { - "title": "Image Management", - "description": "Learn about template image management", - "path": "./admin/templates/managing-templates/image-management.md" - }, - { - "title": "Change Management", - "description": "Learn about template change management and versioning", - "path": "./admin/templates/managing-templates/change-management.md" - }, - { - "title": "Dev containers", - "description": "Learn about using development containers in templates", - "path": "./admin/templates/managing-templates/devcontainers/index.md", - "children": [ - { - "title": "Add a dev container template", - "description": "How to add a dev container template to Coder", - "path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md" - }, - { - "title": "Dev container security and caching", - "description": "Configure dev container authentication and caching", - "path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md" - }, - { - "title": "Dev container releases and known issues", - "description": "Dev container releases and known issues", - "path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md" - } - ] - }, - { - "title": "Template Dependencies", - "description": "Learn how to manage template dependencies", - "path": "./admin/templates/managing-templates/dependencies.md" - }, - { - "title": "Workspace Scheduling", - "description": "Learn how to control how workspaces are started and stopped", - "path": "./admin/templates/managing-templates/schedule.md" - } - ] - }, - { - "title": "Extending Templates", - "description": "Learn best practices in extending templates", - "path": "./admin/templates/extending-templates/index.md", - "children": [ - { - "title": "Agent Metadata", - "description": "Retrieve real-time stats from the workspace agent", - "path": "./admin/templates/extending-templates/agent-metadata.md" - }, - { - "title": "Build Parameters", - "description": "Use parameters to customize workspaces at build", - "path": "./admin/templates/extending-templates/parameters.md" - }, - { - "title": "Dynamic Parameters", - "description": "Conditional, identity-aware parameter syntax for advanced users.", - "path": "./admin/templates/extending-templates/dynamic-parameters.md", - "state": ["beta"] - }, - { - "title": "Prebuilt workspaces", - "description": "Pre-provision a ready-to-deploy workspace with a defined set of parameters", - "path": "./admin/templates/extending-templates/prebuilt-workspaces.md", - "state": ["premium"] - }, - { - "title": "Icons", - "description": "Customize your template with built-in icons", - "path": "./admin/templates/extending-templates/icons.md" - }, - { - "title": "Resource Metadata", - "description": "Display resource state in the workspace dashboard", - "path": "./admin/templates/extending-templates/resource-metadata.md" - }, - { - "title": "Resource Monitoring", - "description": "Monitor resources in the workspace dashboard", - "path": "./admin/templates/extending-templates/resource-monitoring.md" - }, - { - "title": "Resource Ordering", - "description": "Design the UI of workspaces", - "path": "./admin/templates/extending-templates/resource-ordering.md" - }, - { - "title": "Resource Persistence", - "description": "Control resource persistence", - "path": "./admin/templates/extending-templates/resource-persistence.md" - }, - { - "title": "Terraform Variables", - "description": "Use variables to manage template state", - "path": "./admin/templates/extending-templates/variables.md" - }, - { - "title": "Terraform Modules", - "description": "Reuse terraform code across templates", - "path": "./admin/templates/extending-templates/modules.md" - }, - { - "title": "Web IDEs and Coder Apps", - "description": "Add and configure Web IDEs in your templates as coder apps", - "path": "./admin/templates/extending-templates/web-ides.md" - }, - { - "title": "Pre-install JetBrains IDEs", - "description": "Pre-install JetBrains IDEs in a template for faster IDE startup", - "path": "./admin/templates/extending-templates/jetbrains-preinstall.md" - }, - { - "title": "JetBrains IDEs in Air-Gapped Deployments", - "description": "Configure JetBrains IDEs for air-gapped deployments", - "path": "./admin/templates/extending-templates/jetbrains-airgapped.md" - }, - { - "title": "Docker in Workspaces", - "description": "Use Docker in your workspaces", - "path": "./admin/templates/extending-templates/docker-in-workspaces.md" - }, - { - "title": "Workspace Tags", - "description": "Control provisioning using Workspace Tags and Parameters", - "path": "./admin/templates/extending-templates/workspace-tags.md" - }, - { - "title": "Provider Authentication", - "description": "Authenticate with provider APIs to provision workspaces", - "path": "./admin/templates/extending-templates/provider-authentication.md" - }, - { - "title": "Configure a template for dev containers", - "description": "How to use configure your template for dev containers", - "path": "./admin/templates/extending-templates/devcontainers.md" - }, - { - "title": "Process Logging", - "description": "Log workspace processes", - "path": "./admin/templates/extending-templates/process-logging.md", - "state": ["premium"] - } - ] - }, - { - "title": "Open in Coder", - "description": "Open workspaces in Coder", - "path": "./admin/templates/open-in-coder.md" - }, - { - "title": "Permissions \u0026 Policies", - "description": "Learn how to create templates with Terraform", - "path": "./admin/templates/template-permissions.md", - "state": ["premium"] - }, - { - "title": "Troubleshooting Templates", - "description": "Learn how to troubleshoot template issues", - "path": "./admin/templates/troubleshooting.md" - } - ] - }, - { - "title": "External Provisioners", - "description": "Learn how to run external provisioners with Coder", - "path": "./admin/provisioners/index.md", - "icon_path": "./images/icons/key.svg", - "state": ["premium"], - "children": [ - { - "title": "Manage Provisioner Jobs", - "description": "Learn how to run external provisioners with Coder", - "path": "./admin/provisioners/manage-provisioner-jobs.md", - "state": ["premium"] - } - ] - }, - { - "title": "External Authentication", - "description": "Learn how to configure external authentication", - "path": "./admin/external-auth/index.md", - "icon_path": "./images/icons/plug.svg" - }, - { - "title": "Integrations", - "description": "Use integrations to extend Coder", - "path": "./admin/integrations/index.md", - "icon_path": "./images/icons/puzzle.svg", - "children": [ - { - "title": "Prometheus", - "description": "Collect deployment metrics with Prometheus", - "path": "./admin/integrations/prometheus.md" - }, - { - "title": "Kubernetes Logging", - "description": "Stream K8s event logs on workspace startup", - "path": "./admin/integrations/kubernetes-logs.md" - }, - { - "title": "Additional Kubernetes Clusters", - "description": "Deploy workspaces on additional Kubernetes clusters", - "path": "./admin/integrations/multiple-kube-clusters.md" - }, - { - "title": "JFrog Artifactory", - "description": "Integrate Coder with JFrog Artifactory", - "path": "./admin/integrations/jfrog-artifactory.md" - }, - { - "title": "JFrog Xray", - "description": "Integrate Coder with JFrog Xray", - "path": "./admin/integrations/jfrog-xray.md" - }, - { - "title": "Island Secure Browser", - "description": "Integrate Coder with Island's Secure Browser", - "path": "./admin/integrations/island.md" - }, - { - "title": "DX PlatformX", - "description": "Integrate Coder with DX PlatformX", - "path": "./admin/integrations/platformx.md" - }, - { - "title": "DX", - "description": "Tag Coder Users with DX", - "path": "./admin/integrations/dx-data-cloud.md" - }, - { - "title": "Hashicorp Vault", - "description": "Integrate Coder with Hashicorp Vault", - "path": "./admin/integrations/vault.md" - } - ] - }, - { - "title": "Networking", - "description": "Understand Coder's networking layer", - "path": "./admin/networking/index.md", - "icon_path": "./images/icons/networking.svg", - "children": [ - { - "title": "Port Forwarding", - "description": "Learn how to forward ports in Coder", - "path": "./admin/networking/port-forwarding.md" - }, - { - "title": "STUN and NAT", - "description": "Learn how to forward ports in Coder", - "path": "./admin/networking/stun.md" - }, - { - "title": "Workspace Proxies", - "description": "Run geo distributed workspace proxies", - "path": "./admin/networking/workspace-proxies.md", - "state": ["premium"] - }, - { - "title": "High Availability", - "description": "Learn how to configure Coder for High Availability", - "path": "./admin/networking/high-availability.md", - "state": ["premium"] - }, - { - "title": "Troubleshooting", - "description": "Troubleshoot networking issues in Coder", - "path": "./admin/networking/troubleshooting.md" - } - ] - }, - { - "title": "Monitoring", - "description": "Configure security policy and audit your deployment", - "path": "./admin/monitoring/index.md", - "icon_path": "./images/icons/speed.svg", - "children": [ - { - "title": "Logs", - "description": "Learn about Coder's logs", - "path": "./admin/monitoring/logs.md" - }, - { - "title": "Metrics", - "description": "Learn about Coder's logs", - "path": "./admin/monitoring/metrics.md" - }, - { - "title": "Health Check", - "description": "Learn about Coder's automated health checks", - "path": "./admin/monitoring/health-check.md" - }, - { - "title": "Connection Logs", - "description": "Monitor connections to workspaces", - "path": "./admin/monitoring/connection-logs.md", - "state": ["premium"] - }, - { - "title": "Notifications", - "description": "Configure notifications for your deployment", - "path": "./admin/monitoring/notifications/index.md", - "children": [ - { - "title": "Slack Notifications", - "description": "Learn how to setup Slack notifications", - "path": "./admin/monitoring/notifications/slack.md" - }, - { - "title": "Microsoft Teams Notifications", - "description": "Learn how to setup Microsoft Teams notifications", - "path": "./admin/monitoring/notifications/teams.md" - } - ] - } - ] - }, - { - "title": "Security", - "description": "Configure security policy and audit your deployment", - "path": "./admin/security/index.md", - "icon_path": "./images/icons/lock.svg", - "children": [ - { - "title": "Audit Logs", - "description": "Audit actions taken inside Coder", - "path": "./admin/security/audit-logs.md", - "state": ["premium"] - }, - { - "title": "Secrets", - "description": "Use sensitive variables in your workspaces", - "path": "./admin/security/secrets.md" - }, - { - "title": "Database Encryption", - "description": "Encrypt the database to prevent unauthorized access", - "path": "./admin/security/database-encryption.md", - "state": ["premium"] - } - ] - }, - { - "title": "Licensing", - "description": "Configure licensing for your deployment", - "path": "./admin/licensing/index.md", - "icon_path": "./images/icons/licensing.svg" - } - ] - }, - { - "title": "Run AI Coding Agents in Coder", - "description": "Learn how to run and integrate agentic AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", - "path": "./ai-coder/index.md", - "icon_path": "./images/icons/wand.svg", - "children": [ - { - "title": "Best Practices", - "description": "Best Practices running Coding Agents", - "path": "./ai-coder/best-practices.md" - }, - { - "title": "In the IDE", - "description": "Run IDE agents with Coder", - "path": "./ai-coder/ide-agents.md" - }, - { - "title": "Coder Tasks", - "description": "Run Coding Agents on your Own Infrastructure", - "path": "./ai-coder/tasks.md", - "state": ["beta"], - "children": [ - { - "title": "Custom Agents", - "description": "Run custom agents with Coder Tasks", - "path": "./ai-coder/custom-agents.md", - "state": ["beta"] - }, - { - "title": "Security \u0026 Boundaries", - "description": "Learn about security and boundaries when running AI coding agents in Coder", - "path": "./ai-coder/security.md" - } - ] - }, - { - "title": "MCP Server", - "description": "Connect to agents Coder with a MCP server", - "path": "./ai-coder/mcp-server.md", - "state": ["beta"] - } - ] - }, - { - "title": "Tutorials", - "description": "Coder knowledgebase for administrating your deployment", - "path": "./tutorials/index.md", - "icon_path": "./images/icons/generic.svg", - "children": [ - { - "title": "Quickstart", - "description": "Learn how to install and run Coder quickly", - "path": "./tutorials/quickstart.md" - }, - { - "title": "Write a Template from Scratch", - "description": "Learn how to author Coder templates", - "path": "./tutorials/template-from-scratch.md" - }, - { - "title": "Using an External Database", - "description": "Use Coder with an external database", - "path": "./tutorials/external-database.md" - }, - { - "title": "Image Management", - "description": "Learn about image management with Coder", - "path": "./admin/templates/managing-templates/image-management.md" - }, - { - "title": "Configuring Okta", - "description": "Custom claims/scopes with Okta for group/role sync", - "path": "./tutorials/configuring-okta.md" - }, - { - "title": "Google to AWS Federation", - "description": "Federating a Google Cloud service account to AWS", - "path": "./tutorials/gcp-to-aws.md" - }, - { - "title": "JFrog Artifactory Integration", - "description": "Integrate Coder with JFrog Artifactory", - "path": "./admin/integrations/jfrog-artifactory.md" - }, - { - "title": "Istio Integration", - "description": "Integrate Coder with Istio", - "path": "./admin/integrations/istio.md" - }, - { - "title": "Island Secure Browser Integration", - "description": "Integrate Coder with Island's Secure Browser", - "path": "./admin/integrations/island.md" - }, - { - "title": "Template ImagePullSecrets", - "description": "Creating ImagePullSecrets for private registries", - "path": "./tutorials/image-pull-secret.md" - }, - { - "title": "Postgres SSL", - "description": "Configure Coder to connect to Postgres over SSL", - "path": "./tutorials/postgres-ssl.md" - }, - { - "title": "Azure Federation", - "description": "Federating Coder to Azure", - "path": "./tutorials/azure-federation.md" - }, - { - "title": "Deploy Coder on Azure with an Application Gateway", - "description": "Deploy Coder on Azure with an Application Gateway", - "path": "./install/kubernetes/kubernetes-azure-app-gateway.md" - }, - { - "title": "Scanning Workspaces with JFrog Xray", - "description": "Integrate Coder with JFrog Xray", - "path": "./admin/integrations/jfrog-xray.md" - }, - { - "title": "Cloning Git Repositories", - "description": "Learn how to clone Git repositories in Coder", - "path": "./tutorials/cloning-git-repositories.md" - }, - { - "title": "Test Templates Through CI/CD", - "description": "Learn how to test and publish Coder templates in a CI/CD pipeline", - "path": "./tutorials/testing-templates.md" - }, - { - "title": "Use Apache as a Reverse Proxy", - "description": "Learn how to use Apache as a reverse proxy", - "path": "./tutorials/reverse-proxy-apache.md" - }, - { - "title": "Use Caddy as a Reverse Proxy", - "description": "Learn how to use Caddy as a reverse proxy", - "path": "./tutorials/reverse-proxy-caddy.md" - }, - { - "title": "Use NGINX as a Reverse Proxy", - "description": "Learn how to use NGINX as a reverse proxy", - "path": "./tutorials/reverse-proxy-nginx.md" - }, - { - "title": "Pre-install JetBrains IDEs in Workspaces", - "description": "Pre-install JetBrains IDEs in workspaces", - "path": "./admin/templates/extending-templates/jetbrains-preinstall.md" - }, - { - "title": "Use JetBrains IDEs in Air-Gapped Deployments", - "description": "Configure JetBrains IDEs for air-gapped deployments", - "path": "./admin/templates/extending-templates/jetbrains-airgapped.md" - }, - { - "title": "FAQs", - "description": "Miscellaneous FAQs from our community", - "path": "./tutorials/faqs.md" - }, - { - "title": "Best practices", - "description": "Guides to help you make the most of your Coder experience", - "path": "./tutorials/best-practices/index.md", - "children": [ - { - "title": "Organizations - best practices", - "description": "How to make the best use of Coder Organizations", - "path": "./tutorials/best-practices/organizations.md" - }, - { - "title": "Scale Coder", - "description": "How to prepare a Coder deployment for scale", - "path": "./tutorials/best-practices/scale-coder.md" - }, - { - "title": "Security - best practices", - "description": "Make your Coder deployment more secure", - "path": "./tutorials/best-practices/security-best-practices.md" - }, - { - "title": "Speed up your workspaces", - "description": "Speed up your Coder templates and workspaces", - "path": "./tutorials/best-practices/speed-up-templates.md" - } - ] - } - ] - }, - { - "title": "Reference", - "description": "Reference", - "path": "./reference/index.md", - "icon_path": "./images/icons/notes.svg", - "children": [ - { - "title": "REST API", - "description": "Learn how to use Coderd API", - "path": "./reference/api/index.md", - "icon_path": "./images/icons/api.svg", - "children": [ - { - "title": "General", - "path": "./reference/api/general.md" - }, - { - "title": "Agents", - "path": "./reference/api/agents.md" - }, - { - "title": "Applications", - "path": "./reference/api/applications.md" - }, - { - "title": "Audit", - "path": "./reference/api/audit.md" - }, - { - "title": "Authentication", - "path": "./reference/api/authentication.md" - }, - { - "title": "Authorization", - "path": "./reference/api/authorization.md" - }, - { - "title": "Builds", - "path": "./reference/api/builds.md" - }, - { - "title": "Debug", - "path": "./reference/api/debug.md" - }, - { - "title": "Enterprise", - "path": "./reference/api/enterprise.md" - }, - { - "title": "Files", - "path": "./reference/api/files.md" - }, - { - "title": "Git", - "path": "./reference/api/git.md" - }, - { - "title": "Insights", - "path": "./reference/api/insights.md" - }, - { - "title": "Members", - "path": "./reference/api/members.md" - }, - { - "title": "Organizations", - "path": "./reference/api/organizations.md" - }, - { - "title": "PortSharing", - "path": "./reference/api/portsharing.md" - }, - { - "title": "Schemas", - "path": "./reference/api/schemas.md" - }, - { - "title": "Templates", - "path": "./reference/api/templates.md" - }, - { - "title": "Users", - "path": "./reference/api/users.md" - }, - { - "title": "WorkspaceProxies", - "path": "./reference/api/workspaceproxies.md" - }, - { - "title": "Workspaces", - "path": "./reference/api/workspaces.md" - } - ] - }, - { - "title": "Command Line", - "description": "Learn how to use Coder CLI", - "path": "./reference/cli/index.md", - "icon_path": "./images/icons/terminal.svg", - "children": [ - { - "title": "autoupdate", - "description": "Toggle auto-update policy for a workspace", - "path": "reference/cli/autoupdate.md" - }, - { - "title": "coder", - "path": "reference/cli/index.md" - }, - { - "title": "completion", - "description": "Install or update shell completion scripts for the detected or chosen shell.", - "path": "reference/cli/completion.md" - }, - { - "title": "config-ssh", - "description": "Add an SSH Host entry for your workspaces \"ssh workspace.coder\"", - "path": "reference/cli/config-ssh.md" - }, - { - "title": "create", - "description": "Create a workspace", - "path": "reference/cli/create.md" - }, - { - "title": "delete", - "description": "Delete a workspace", - "path": "reference/cli/delete.md" - }, - { - "title": "dotfiles", - "description": "Personalize your workspace by applying a canonical dotfiles repository", - "path": "reference/cli/dotfiles.md" - }, - { - "title": "external-auth", - "description": "Manage external authentication", - "path": "reference/cli/external-auth.md" - }, - { - "title": "external-auth access-token", - "description": "Print auth for an external provider", - "path": "reference/cli/external-auth_access-token.md" - }, - { - "title": "favorite", - "description": "Add a workspace to your favorites", - "path": "reference/cli/favorite.md" - }, - { - "title": "features", - "description": "List Enterprise features", - "path": "reference/cli/features.md" - }, - { - "title": "features list", - "path": "reference/cli/features_list.md" - }, - { - "title": "groups", - "description": "Manage groups", - "path": "reference/cli/groups.md" - }, - { - "title": "groups create", - "description": "Create a user group", - "path": "reference/cli/groups_create.md" - }, - { - "title": "groups delete", - "description": "Delete a user group", - "path": "reference/cli/groups_delete.md" - }, - { - "title": "groups edit", - "description": "Edit a user group", - "path": "reference/cli/groups_edit.md" - }, - { - "title": "groups list", - "description": "List user groups", - "path": "reference/cli/groups_list.md" - }, - { - "title": "licenses", - "description": "Add, delete, and list licenses", - "path": "reference/cli/licenses.md" - }, - { - "title": "licenses add", - "description": "Add license to Coder deployment", - "path": "reference/cli/licenses_add.md" - }, - { - "title": "licenses delete", - "description": "Delete license by ID", - "path": "reference/cli/licenses_delete.md" - }, - { - "title": "licenses list", - "description": "List licenses (including expired)", - "path": "reference/cli/licenses_list.md" - }, - { - "title": "list", - "description": "List workspaces", - "path": "reference/cli/list.md" - }, - { - "title": "login", - "description": "Authenticate with Coder deployment", - "path": "reference/cli/login.md" - }, - { - "title": "logout", - "description": "Unauthenticate your local session", - "path": "reference/cli/logout.md" - }, - { - "title": "netcheck", - "description": "Print network debug information for DERP and STUN", - "path": "reference/cli/netcheck.md" - }, - { - "title": "notifications", - "description": "Manage Coder notifications", - "path": "reference/cli/notifications.md" - }, - { - "title": "notifications pause", - "description": "Pause notifications", - "path": "reference/cli/notifications_pause.md" - }, - { - "title": "notifications resume", - "description": "Resume notifications", - "path": "reference/cli/notifications_resume.md" - }, - { - "title": "notifications test", - "description": "Send a test notification", - "path": "reference/cli/notifications_test.md" - }, - { - "title": "open", - "description": "Open a workspace", - "path": "reference/cli/open.md" - }, - { - "title": "open app", - "description": "Open a workspace application.", - "path": "reference/cli/open_app.md" - }, - { - "title": "open vscode", - "description": "Open a workspace in VS Code Desktop", - "path": "reference/cli/open_vscode.md" - }, - { - "title": "organizations", - "description": "Organization related commands", - "path": "reference/cli/organizations.md" - }, - { - "title": "organizations create", - "description": "Create a new organization.", - "path": "reference/cli/organizations_create.md" - }, - { - "title": "organizations members", - "description": "Manage organization members", - "path": "reference/cli/organizations_members.md" - }, - { - "title": "organizations members add", - "description": "Add a new member to the current organization", - "path": "reference/cli/organizations_members_add.md" - }, - { - "title": "organizations members edit-roles", - "description": "Edit organization member's roles", - "path": "reference/cli/organizations_members_edit-roles.md" - }, - { - "title": "organizations members list", - "description": "List all organization members", - "path": "reference/cli/organizations_members_list.md" - }, - { - "title": "organizations members remove", - "description": "Remove a new member to the current organization", - "path": "reference/cli/organizations_members_remove.md" - }, - { - "title": "organizations roles", - "description": "Manage organization roles.", - "path": "reference/cli/organizations_roles.md" - }, - { - "title": "organizations roles create", - "description": "Create a new organization custom role", - "path": "reference/cli/organizations_roles_create.md" - }, - { - "title": "organizations roles show", - "description": "Show role(s)", - "path": "reference/cli/organizations_roles_show.md" - }, - { - "title": "organizations roles update", - "description": "Update an organization custom role", - "path": "reference/cli/organizations_roles_update.md" - }, - { - "title": "organizations settings", - "description": "Manage organization settings.", - "path": "reference/cli/organizations_settings.md" - }, - { - "title": "organizations settings set", - "description": "Update specified organization setting.", - "path": "reference/cli/organizations_settings_set.md" - }, - { - "title": "organizations settings set group-sync", - "description": "Group sync settings to sync groups from an IdP.", - "path": "reference/cli/organizations_settings_set_group-sync.md" - }, - { - "title": "organizations settings set organization-sync", - "description": "Organization sync settings to sync organization memberships from an IdP.", - "path": "reference/cli/organizations_settings_set_organization-sync.md" - }, - { - "title": "organizations settings set role-sync", - "description": "Role sync settings to sync organization roles from an IdP.", - "path": "reference/cli/organizations_settings_set_role-sync.md" - }, - { - "title": "organizations settings show", - "description": "Outputs specified organization setting.", - "path": "reference/cli/organizations_settings_show.md" - }, - { - "title": "organizations settings show group-sync", - "description": "Group sync settings to sync groups from an IdP.", - "path": "reference/cli/organizations_settings_show_group-sync.md" - }, - { - "title": "organizations settings show organization-sync", - "description": "Organization sync settings to sync organization memberships from an IdP.", - "path": "reference/cli/organizations_settings_show_organization-sync.md" - }, - { - "title": "organizations settings show role-sync", - "description": "Role sync settings to sync organization roles from an IdP.", - "path": "reference/cli/organizations_settings_show_role-sync.md" - }, - { - "title": "organizations show", - "description": "Show the organization. Using \"selected\" will show the selected organization from the \"--org\" flag. Using \"me\" will show all organizations you are a member of.", - "path": "reference/cli/organizations_show.md" - }, - { - "title": "ping", - "description": "Ping a workspace", - "path": "reference/cli/ping.md" - }, - { - "title": "port-forward", - "description": "Forward ports from a workspace to the local machine. For reverse port forwarding, use \"coder ssh -R\".", - "path": "reference/cli/port-forward.md" - }, - { - "title": "prebuilds", - "description": "Manage Coder prebuilds", - "path": "reference/cli/prebuilds.md" - }, - { - "title": "prebuilds pause", - "description": "Pause prebuilds", - "path": "reference/cli/prebuilds_pause.md" - }, - { - "title": "prebuilds resume", - "description": "Resume prebuilds", - "path": "reference/cli/prebuilds_resume.md" - }, - { - "title": "provisioner", - "description": "View and manage provisioner daemons and jobs", - "path": "reference/cli/provisioner.md" - }, - { - "title": "provisioner jobs", - "description": "View and manage provisioner jobs", - "path": "reference/cli/provisioner_jobs.md" - }, - { - "title": "provisioner jobs cancel", - "description": "Cancel a provisioner job", - "path": "reference/cli/provisioner_jobs_cancel.md" - }, - { - "title": "provisioner jobs list", - "description": "List provisioner jobs", - "path": "reference/cli/provisioner_jobs_list.md" - }, - { - "title": "provisioner keys", - "description": "Manage provisioner keys", - "path": "reference/cli/provisioner_keys.md" - }, - { - "title": "provisioner keys create", - "description": "Create a new provisioner key", - "path": "reference/cli/provisioner_keys_create.md" - }, - { - "title": "provisioner keys delete", - "description": "Delete a provisioner key", - "path": "reference/cli/provisioner_keys_delete.md" - }, - { - "title": "provisioner keys list", - "description": "List provisioner keys in an organization", - "path": "reference/cli/provisioner_keys_list.md" - }, - { - "title": "provisioner list", - "description": "List provisioner daemons in an organization", - "path": "reference/cli/provisioner_list.md" - }, - { - "title": "provisioner start", - "description": "Run a provisioner daemon", - "path": "reference/cli/provisioner_start.md" - }, - { - "title": "publickey", - "description": "Output your Coder public key used for Git operations", - "path": "reference/cli/publickey.md" - }, - { - "title": "rename", - "description": "Rename a workspace", - "path": "reference/cli/rename.md" - }, - { - "title": "reset-password", - "description": "Directly connect to the database to reset a user's password", - "path": "reference/cli/reset-password.md" - }, - { - "title": "restart", - "description": "Restart a workspace", - "path": "reference/cli/restart.md" - }, - { - "title": "schedule", - "description": "Schedule automated start and stop times for workspaces", - "path": "reference/cli/schedule.md" - }, - { - "title": "schedule extend", - "description": "Extend the stop time of a currently running workspace instance.", - "path": "reference/cli/schedule_extend.md" - }, - { - "title": "schedule show", - "description": "Show workspace schedules", - "path": "reference/cli/schedule_show.md" - }, - { - "title": "schedule start", - "description": "Edit workspace start schedule", - "path": "reference/cli/schedule_start.md" - }, - { - "title": "schedule stop", - "description": "Edit workspace stop schedule", - "path": "reference/cli/schedule_stop.md" - }, - { - "title": "server", - "description": "Start a Coder server", - "path": "reference/cli/server.md" - }, - { - "title": "server create-admin-user", - "description": "Create a new admin user with the given username, email and password and adds it to every organization.", - "path": "reference/cli/server_create-admin-user.md" - }, - { - "title": "server dbcrypt", - "description": "Manage database encryption.", - "path": "reference/cli/server_dbcrypt.md" - }, - { - "title": "server dbcrypt decrypt", - "description": "Decrypt a previously encrypted database.", - "path": "reference/cli/server_dbcrypt_decrypt.md" - }, - { - "title": "server dbcrypt delete", - "description": "Delete all encrypted data from the database. THIS IS A DESTRUCTIVE OPERATION.", - "path": "reference/cli/server_dbcrypt_delete.md" - }, - { - "title": "server dbcrypt rotate", - "description": "Rotate database encryption keys.", - "path": "reference/cli/server_dbcrypt_rotate.md" - }, - { - "title": "server postgres-builtin-serve", - "description": "Run the built-in PostgreSQL deployment.", - "path": "reference/cli/server_postgres-builtin-serve.md" - }, - { - "title": "server postgres-builtin-url", - "description": "Output the connection URL for the built-in PostgreSQL deployment.", - "path": "reference/cli/server_postgres-builtin-url.md" - }, - { - "title": "show", - "description": "Display details of a workspace's resources and agents", - "path": "reference/cli/show.md" - }, - { - "title": "speedtest", - "description": "Run upload and download tests from your machine to a workspace", - "path": "reference/cli/speedtest.md" - }, - { - "title": "ssh", - "description": "Start a shell into a workspace or run a command", - "path": "reference/cli/ssh.md" - }, - { - "title": "start", - "description": "Start a workspace", - "path": "reference/cli/start.md" - }, - { - "title": "stat", - "description": "Show resource usage for the current workspace.", - "path": "reference/cli/stat.md" - }, - { - "title": "stat cpu", - "description": "Show CPU usage, in cores.", - "path": "reference/cli/stat_cpu.md" - }, - { - "title": "stat disk", - "description": "Show disk usage, in gigabytes.", - "path": "reference/cli/stat_disk.md" - }, - { - "title": "stat mem", - "description": "Show memory usage, in gigabytes.", - "path": "reference/cli/stat_mem.md" - }, - { - "title": "state", - "description": "Manually manage Terraform state to fix broken workspaces", - "path": "reference/cli/state.md" - }, - { - "title": "state pull", - "description": "Pull a Terraform state file from a workspace.", - "path": "reference/cli/state_pull.md" - }, - { - "title": "state push", - "description": "Push a Terraform state file to a workspace.", - "path": "reference/cli/state_push.md" - }, - { - "title": "stop", - "description": "Stop a workspace", - "path": "reference/cli/stop.md" - }, - { - "title": "support", - "description": "Commands for troubleshooting issues with a Coder deployment.", - "path": "reference/cli/support.md" - }, - { - "title": "support bundle", - "description": "Generate a support bundle to troubleshoot issues connecting to a workspace.", - "path": "reference/cli/support_bundle.md" - }, - { - "title": "templates", - "description": "Manage templates", - "path": "reference/cli/templates.md" - }, - { - "title": "templates archive", - "description": "Archive unused or failed template versions from a given template(s)", - "path": "reference/cli/templates_archive.md" - }, - { - "title": "templates create", - "description": "DEPRECATED: Create a template from the current directory or as specified by flag", - "path": "reference/cli/templates_create.md" - }, - { - "title": "templates delete", - "description": "Delete templates", - "path": "reference/cli/templates_delete.md" - }, - { - "title": "templates edit", - "description": "Edit the metadata of a template by name.", - "path": "reference/cli/templates_edit.md" - }, - { - "title": "templates init", - "description": "Get started with a templated template.", - "path": "reference/cli/templates_init.md" - }, - { - "title": "templates list", - "description": "List all the templates available for the organization", - "path": "reference/cli/templates_list.md" - }, - { - "title": "templates presets", - "description": "Manage presets of the specified template", - "path": "reference/cli/templates_presets.md" - }, - { - "title": "templates presets list", - "description": "List all presets of the specified template. Defaults to the active template version.", - "path": "reference/cli/templates_presets_list.md" - }, - { - "title": "templates pull", - "description": "Download the active, latest, or specified version of a template to a path.", - "path": "reference/cli/templates_pull.md" - }, - { - "title": "templates push", - "description": "Create or update a template from the current directory or as specified by flag", - "path": "reference/cli/templates_push.md" - }, - { - "title": "templates versions", - "description": "Manage different versions of the specified template", - "path": "reference/cli/templates_versions.md" - }, - { - "title": "templates versions archive", - "description": "Archive a template version(s).", - "path": "reference/cli/templates_versions_archive.md" - }, - { - "title": "templates versions list", - "description": "List all the versions of the specified template", - "path": "reference/cli/templates_versions_list.md" - }, - { - "title": "templates versions promote", - "description": "Promote a template version to active.", - "path": "reference/cli/templates_versions_promote.md" - }, - { - "title": "templates versions unarchive", - "description": "Unarchive a template version(s).", - "path": "reference/cli/templates_versions_unarchive.md" - }, - { - "title": "tokens", - "description": "Manage personal access tokens", - "path": "reference/cli/tokens.md" - }, - { - "title": "tokens create", - "description": "Create a token", - "path": "reference/cli/tokens_create.md" - }, - { - "title": "tokens list", - "description": "List tokens", - "path": "reference/cli/tokens_list.md" - }, - { - "title": "tokens remove", - "description": "Delete a token", - "path": "reference/cli/tokens_remove.md" - }, - { - "title": "unfavorite", - "description": "Remove a workspace from your favorites", - "path": "reference/cli/unfavorite.md" - }, - { - "title": "update", - "description": "Will update and start a given workspace if it is out of date. If the workspace is already running, it will be stopped first.", - "path": "reference/cli/update.md" - }, - { - "title": "users", - "description": "Manage users", - "path": "reference/cli/users.md" - }, - { - "title": "users activate", - "description": "Update a user's status to 'active'. Active users can fully interact with the platform", - "path": "reference/cli/users_activate.md" - }, - { - "title": "users create", - "description": "Create a new user.", - "path": "reference/cli/users_create.md" - }, - { - "title": "users delete", - "description": "Delete a user by username or user_id.", - "path": "reference/cli/users_delete.md" - }, - { - "title": "users edit-roles", - "description": "Edit a user's roles by username or id", - "path": "reference/cli/users_edit-roles.md" - }, - { - "title": "users list", - "description": "Prints the list of users.", - "path": "reference/cli/users_list.md" - }, - { - "title": "users show", - "description": "Show a single user. Use 'me' to indicate the currently authenticated user.", - "path": "reference/cli/users_show.md" - }, - { - "title": "users suspend", - "description": "Update a user's status to 'suspended'. A suspended user cannot log into the platform", - "path": "reference/cli/users_suspend.md" - }, - { - "title": "version", - "description": "Show coder version", - "path": "reference/cli/version.md" - }, - { - "title": "whoami", - "description": "Fetch authenticated user info for Coder deployment", - "path": "reference/cli/whoami.md" - } - ] - }, - { - "title": "Agent API", - "description": "Learn how to use Coder Agent API", - "path": "./reference/agent-api/index.md", - "icon_path": "./images/icons/api.svg", - "children": [ - { - "title": "Debug", - "path": "./reference/agent-api/debug.md" - }, - { - "title": "Schemas", - "path": "./reference/agent-api/schemas.md" - } - ] - } - ] - } - ] -} + "versions": [ + "main" + ], + "routes": [ + { + "title": "About", + "description": "Coder docs", + "path": "./README.md", + "icon_path": "./images/icons/home.svg", + "children": [ + { + "title": "Screenshots", + "description": "View screenshots of the Coder platform", + "path": "./about/screenshots.md" + }, + { + "title": "Quickstart", + "description": "Learn how to install and run Coder quickly", + "path": "./tutorials/quickstart.md" + }, + { + "title": "Support", + "description": "How Coder supports your deployment and you", + "path": "./support/index.md", + "children": [ + { + "title": "Generate a Support Bundle", + "description": "Generate and upload a Support Bundle to Coder Support", + "path": "./support/support-bundle.md" + } + ] + }, + { + "title": "Contributing", + "description": "Learn how to contribute to Coder", + "path": "./about/contributing/CONTRIBUTING.md", + "icon_path": "./images/icons/contributing.svg", + "children": [ + { + "title": "Code of Conduct", + "description": "See the code of conduct for contributing to Coder", + "path": "./about/contributing/CODE_OF_CONDUCT.md", + "icon_path": "./images/icons/circle-dot.svg" + }, + { + "title": "Documentation", + "description": "Our style guide for use when authoring documentation", + "path": "./about/contributing/documentation.md", + "icon_path": "./images/icons/document.svg" + }, + { + "title": "Modules", + "description": "Learn how to contribute modules to Coder", + "path": "./about/contributing/modules.md", + "icon_path": "./images/icons/gear.svg" + }, + { + "title": "Templates", + "description": "Learn how to contribute templates to Coder", + "path": "./about/contributing/templates.md", + "icon_path": "./images/icons/picture.svg" + }, + { + "title": "Backend", + "description": "Our guide for backend development", + "path": "./about/contributing/backend.md", + "icon_path": "./images/icons/gear.svg" + }, + { + "title": "Frontend", + "description": "Our guide for frontend development", + "path": "./about/contributing/frontend.md", + "icon_path": "./images/icons/frontend.svg" + }, + { + "title": "Security", + "description": "Security vulnerability disclosure policy", + "path": "./about/contributing/SECURITY.md", + "icon_path": "./images/icons/lock.svg" + } + ] + } + ] + }, + { + "title": "Install", + "description": "Installing Coder", + "path": "./install/index.md", + "icon_path": "./images/icons/download.svg", + "children": [ + { + "title": "Coder CLI", + "description": "Install the standalone binary", + "path": "./install/cli.md", + "icon_path": "./images/icons/terminal.svg" + }, + { + "title": "Docker", + "description": "Install Coder using Docker", + "path": "./install/docker.md", + "icon_path": "./images/icons/docker.svg" + }, + { + "title": "Kubernetes", + "description": "Install Coder on Kubernetes", + "path": "./install/kubernetes.md", + "icon_path": "./images/icons/kubernetes.svg", + "children": [ + { + "title": "Deploy Coder on Azure with an Application Gateway", + "description": "Deploy Coder on Azure with an Application Gateway", + "path": "./install/kubernetes/kubernetes-azure-app-gateway.md" + } + ] + }, + { + "title": "Rancher", + "description": "Deploy Coder on Rancher", + "path": "./install/rancher.md", + "icon_path": "./images/icons/rancher.svg" + }, + { + "title": "OpenShift", + "description": "Install Coder on OpenShift", + "path": "./install/openshift.md", + "icon_path": "./images/icons/openshift.svg" + }, + { + "title": "Cloud Providers", + "description": "Install Coder on cloud providers", + "path": "./install/cloud/index.md", + "icon_path": "./images/icons/cloud.svg", + "children": [ + { + "title": "AWS EC2", + "description": "Install Coder on AWS EC2", + "path": "./install/cloud/ec2.md" + }, + { + "title": "GCP Compute Engine", + "description": "Install Coder on GCP Compute Engine", + "path": "./install/cloud/compute-engine.md" + }, + { + "title": "Azure VM", + "description": "Install Coder on an Azure VM", + "path": "./install/cloud/azure-vm.md" + } + ] + }, + { + "title": "Offline Deployments", + "description": "Run Coder in offline / air-gapped environments", + "path": "./install/offline.md", + "icon_path": "./images/icons/lan.svg" + }, + { + "title": "Unofficial Install Methods", + "description": "Other installation methods", + "path": "./install/other/index.md", + "icon_path": "./images/icons/generic.svg" + }, + { + "title": "Upgrading", + "description": "Learn how to upgrade Coder", + "path": "./install/upgrade.md", + "icon_path": "./images/icons/upgrade.svg" + }, + { + "title": "Uninstall", + "description": "Learn how to uninstall Coder", + "path": "./install/uninstall.md", + "icon_path": "./images/icons/trash.svg" + }, + { + "title": "Releases", + "description": "Learn about the Coder release channels and schedule", + "path": "./install/releases/index.md", + "icon_path": "./images/icons/star.svg", + "children": [ + { + "title": "Feature stages", + "description": "Information about pre-GA stages.", + "path": "./install/releases/feature-stages.md" + } + ] + } + ] + }, + { + "title": "User Guides", + "description": "Guides for end-users of Coder", + "path": "./user-guides/index.md", + "icon_path": "./images/icons/users.svg", + "children": [ + { + "title": "Access Workspaces", + "description": "Connect to your Coder workspaces", + "path": "./user-guides/workspace-access/index.md", + "icon_path": "./images/icons/access.svg", + "children": [ + { + "title": "Visual Studio Code", + "description": "Use VSCode with Coder in the desktop or browser", + "path": "./user-guides/workspace-access/vscode.md" + }, + { + "title": "JetBrains IDEs", + "description": "Use JetBrains IDEs with Coder", + "path": "./user-guides/workspace-access/jetbrains/index.md", + "children": [ + { + "title": "JetBrains Fleet", + "description": "Connect JetBrains Fleet to a Coder workspace", + "path": "./user-guides/workspace-access/jetbrains/fleet.md" + }, + { + "title": "JetBrains Gateway", + "description": "Use JetBrains Gateway to connect to Coder workspaces", + "path": "./user-guides/workspace-access/jetbrains/gateway.md" + }, + { + "title": "JetBrains Toolbox", + "description": "Access Coder workspaces from JetBrains Toolbox", + "path": "./user-guides/workspace-access/jetbrains/toolbox.md", + "state": [ + "beta" + ] + } + ] + }, + { + "title": "Remote Desktop", + "description": "Use RDP in Coder", + "path": "./user-guides/workspace-access/remote-desktops.md" + }, + { + "title": "Emacs TRAMP", + "description": "Use Emacs TRAMP in Coder", + "path": "./user-guides/workspace-access/emacs-tramp.md" + }, + { + "title": "Port Forwarding", + "description": "Access ports on your workspace", + "path": "./user-guides/workspace-access/port-forwarding.md" + }, + { + "title": "Filebrowser", + "description": "Access your workspace files", + "path": "./user-guides/workspace-access/filebrowser.md" + }, + { + "title": "Web IDEs and Coder Apps", + "description": "Access your workspace with IDEs in the browser", + "path": "./user-guides/workspace-access/web-ides.md" + }, + { + "title": "Zed", + "description": "Access your workspace with Zed", + "path": "./user-guides/workspace-access/zed.md" + }, + { + "title": "Cursor", + "description": "Access your workspace with Cursor", + "path": "./user-guides/workspace-access/cursor.md" + }, + { + "title": "Windsurf", + "description": "Access your workspace with Windsurf", + "path": "./user-guides/workspace-access/windsurf.md" + } + ] + }, + { + "title": "Coder Desktop", + "description": "Transform remote workspaces into seamless local development environments with no port forwarding required", + "path": "./user-guides/desktop/index.md", + "icon_path": "./images/icons/computer-code.svg", + "children": [ + { + "title": "Coder Desktop connect and sync", + "description": "Use Coder Desktop to manage your workspace code and files locally", + "path": "./user-guides/desktop/desktop-connect-sync.md" + } + ] + }, + { + "title": "Workspace Management", + "description": "Manage workspaces", + "path": "./user-guides/workspace-management.md", + "icon_path": "./images/icons/generic.svg" + }, + { + "title": "Workspace Scheduling", + "description": "Cost control with workspace schedules", + "path": "./user-guides/workspace-scheduling.md", + "icon_path": "./images/icons/stopwatch.svg" + }, + { + "title": "Workspace Lifecycle", + "description": "A guide to the workspace lifecycle, from creation and status through stopping and deletion.", + "path": "./user-guides/workspace-lifecycle.md", + "icon_path": "./images/icons/circle-dot.svg" + }, + { + "title": "Dev Containers Integration", + "description": "Run containerized development environments in your Coder workspace using the dev containers specification.", + "path": "./user-guides/devcontainers/index.md", + "icon_path": "./images/icons/container.svg", + "children": [ + { + "title": "Working with dev containers", + "description": "Access dev containers via SSH, your IDE, or web terminal.", + "path": "./user-guides/devcontainers/working-with-dev-containers.md" + }, + { + "title": "Troubleshooting dev containers", + "description": "Diagnose and resolve common issues with dev containers in your Coder workspace.", + "path": "./user-guides/devcontainers/troubleshooting-dev-containers.md" + } + ] + }, + { + "title": "Dotfiles", + "description": "Personalize your environment with dotfiles", + "path": "./user-guides/workspace-dotfiles.md", + "icon_path": "./images/icons/art-pad.svg" + } + ] + }, + { + "title": "Administration", + "description": "Guides for template and deployment administrators", + "path": "./admin/index.md", + "icon_path": "./images/icons/wrench.svg", + "children": [ + { + "title": "Setup", + "description": "Configure user access to your control plane.", + "path": "./admin/setup/index.md", + "icon_path": "./images/icons/toggle_on.svg", + "children": [ + { + "title": "Appearance", + "description": "Learn how to configure the appearance of Coder", + "path": "./admin/setup/appearance.md", + "state": [ + "premium" + ] + }, + { + "title": "Telemetry", + "description": "Learn what usage telemetry Coder collects", + "path": "./admin/setup/telemetry.md" + } + ] + }, + { + "title": "Infrastructure", + "description": "How to integrate Coder with your organization's compute", + "path": "./admin/infrastructure/index.md", + "icon_path": "./images/icons/container.svg", + "children": [ + { + "title": "Architecture", + "description": "Learn about Coder's architecture", + "path": "./admin/infrastructure/architecture.md" + }, + { + "title": "Validated Architectures", + "description": "Architectures for large Coder deployments", + "path": "./admin/infrastructure/validated-architectures/index.md", + "children": [ + { + "title": "Up to 1,000 Users", + "description": "Hardware specifications and architecture guidance for Coder deployments that support up to 1,000 users", + "path": "./admin/infrastructure/validated-architectures/1k-users.md" + }, + { + "title": "Up to 2,000 Users", + "description": "Hardware specifications and architecture guidance for Coder deployments that support up to 2,000 users", + "path": "./admin/infrastructure/validated-architectures/2k-users.md" + }, + { + "title": "Up to 3,000 Users", + "description": "Enterprise-scale architecture recommendations for Coder deployments that support up to 3,000 users", + "path": "./admin/infrastructure/validated-architectures/3k-users.md" + } + ] + }, + { + "title": "Scale Testing", + "description": "Ensure your deployment can handle your organization's needs", + "path": "./admin/infrastructure/scale-testing.md" + }, + { + "title": "Scaling Utilities", + "description": "Tools to help you scale your deployment", + "path": "./admin/infrastructure/scale-utility.md" + }, + { + "title": "Scaling best practices", + "description": "How to prepare a Coder deployment for scale", + "path": "./tutorials/best-practices/scale-coder.md" + } + ] + }, + { + "title": "Users", + "description": "Learn how to manage and audit users", + "path": "./admin/users/index.md", + "icon_path": "./images/icons/users.svg", + "children": [ + { + "title": "OIDC Authentication", + "description": "Configure OpenID Connect authentication with identity providers like Okta or Active Directory", + "path": "./admin/users/oidc-auth/index.md", + "children": [ + { + "title": "Configure OIDC refresh tokens", + "description": "How to configure OIDC refresh tokens", + "path": "./admin/users/oidc-auth/refresh-tokens.md" + } + ] + }, + { + "title": "GitHub Authentication", + "description": "Set up authentication through GitHub OAuth to enable secure user login and sign-up", + "path": "./admin/users/github-auth.md" + }, + { + "title": "Password Authentication", + "description": "Manage username/password authentication settings and user password reset workflows", + "path": "./admin/users/password-auth.md" + }, + { + "title": "Headless Authentication", + "description": "Create and manage headless service accounts for automated systems and API integrations", + "path": "./admin/users/headless-auth.md" + }, + { + "title": "Groups \u0026 Roles", + "description": "Manage access control with user groups and role-based permissions for Coder resources", + "path": "./admin/users/groups-roles.md", + "state": [ + "premium" + ] + }, + { + "title": "IdP Sync", + "description": "Synchronize user groups, roles, and organizations from your identity provider to Coder", + "path": "./admin/users/idp-sync.md", + "state": [ + "premium" + ] + }, + { + "title": "Organizations", + "description": "Segment and isolate resources by creating separate organizations for different teams or projects", + "path": "./admin/users/organizations.md", + "state": [ + "premium" + ] + }, + { + "title": "Quotas", + "description": "Control resource usage by implementing workspace budgets and credit-based cost management", + "path": "./admin/users/quotas.md", + "state": [ + "premium" + ] + }, + { + "title": "Sessions \u0026 API Tokens", + "description": "Manage authentication tokens for API access and configure session duration policies", + "path": "./admin/users/sessions-tokens.md" + } + ] + }, + { + "title": "Templates", + "description": "Learn how to author and maintain Coder templates", + "path": "./admin/templates/index.md", + "icon_path": "./images/icons/picture.svg", + "children": [ + { + "title": "Creating Templates", + "description": "Learn how to create templates with Terraform", + "path": "./admin/templates/creating-templates.md" + }, + { + "title": "Managing Templates", + "description": "Learn how to manage templates and best practices", + "path": "./admin/templates/managing-templates/index.md", + "children": [ + { + "title": "Image Management", + "description": "Learn about template image management", + "path": "./admin/templates/managing-templates/image-management.md" + }, + { + "title": "Change Management", + "description": "Learn about template change management and versioning", + "path": "./admin/templates/managing-templates/change-management.md" + }, + { + "title": "Dev containers", + "description": "Learn about using development containers in templates", + "path": "./admin/templates/managing-templates/devcontainers/index.md", + "children": [ + { + "title": "Add a dev container template", + "description": "How to add a dev container template to Coder", + "path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md" + }, + { + "title": "Dev container security and caching", + "description": "Configure dev container authentication and caching", + "path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md" + }, + { + "title": "Dev container releases and known issues", + "description": "Dev container releases and known issues", + "path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md" + } + ] + }, + { + "title": "Template Dependencies", + "description": "Learn how to manage template dependencies", + "path": "./admin/templates/managing-templates/dependencies.md" + }, + { + "title": "Workspace Scheduling", + "description": "Learn how to control how workspaces are started and stopped", + "path": "./admin/templates/managing-templates/schedule.md" + } + ] + }, + { + "title": "Extending Templates", + "description": "Learn best practices in extending templates", + "path": "./admin/templates/extending-templates/index.md", + "children": [ + { + "title": "Agent Metadata", + "description": "Retrieve real-time stats from the workspace agent", + "path": "./admin/templates/extending-templates/agent-metadata.md" + }, + { + "title": "Build Parameters", + "description": "Use parameters to customize workspaces at build", + "path": "./admin/templates/extending-templates/parameters.md" + }, + { + "title": "Dynamic Parameters", + "description": "Conditional, identity-aware parameter syntax for advanced users.", + "path": "./admin/templates/extending-templates/dynamic-parameters.md", + "state": [ + "beta" + ] + }, + { + "title": "Prebuilt workspaces", + "description": "Pre-provision a ready-to-deploy workspace with a defined set of parameters", + "path": "./admin/templates/extending-templates/prebuilt-workspaces.md", + "state": [ + "premium" + ] + }, + { + "title": "Icons", + "description": "Customize your template with built-in icons", + "path": "./admin/templates/extending-templates/icons.md" + }, + { + "title": "Resource Metadata", + "description": "Display resource state in the workspace dashboard", + "path": "./admin/templates/extending-templates/resource-metadata.md" + }, + { + "title": "Resource Monitoring", + "description": "Monitor resources in the workspace dashboard", + "path": "./admin/templates/extending-templates/resource-monitoring.md" + }, + { + "title": "Resource Ordering", + "description": "Design the UI of workspaces", + "path": "./admin/templates/extending-templates/resource-ordering.md" + }, + { + "title": "Resource Persistence", + "description": "Control resource persistence", + "path": "./admin/templates/extending-templates/resource-persistence.md" + }, + { + "title": "Terraform Variables", + "description": "Use variables to manage template state", + "path": "./admin/templates/extending-templates/variables.md" + }, + { + "title": "Terraform Modules", + "description": "Reuse terraform code across templates", + "path": "./admin/templates/extending-templates/modules.md" + }, + { + "title": "Web IDEs and Coder Apps", + "description": "Add and configure Web IDEs in your templates as coder apps", + "path": "./admin/templates/extending-templates/web-ides.md" + }, + { + "title": "Pre-install JetBrains IDEs", + "description": "Pre-install JetBrains IDEs in a template for faster IDE startup", + "path": "./admin/templates/extending-templates/jetbrains-preinstall.md" + }, + { + "title": "JetBrains IDEs in Air-Gapped Deployments", + "description": "Configure JetBrains IDEs for air-gapped deployments", + "path": "./admin/templates/extending-templates/jetbrains-airgapped.md" + }, + { + "title": "Docker in Workspaces", + "description": "Use Docker in your workspaces", + "path": "./admin/templates/extending-templates/docker-in-workspaces.md" + }, + { + "title": "Workspace Tags", + "description": "Control provisioning using Workspace Tags and Parameters", + "path": "./admin/templates/extending-templates/workspace-tags.md" + }, + { + "title": "Provider Authentication", + "description": "Authenticate with provider APIs to provision workspaces", + "path": "./admin/templates/extending-templates/provider-authentication.md" + }, + { + "title": "Configure a template for dev containers", + "description": "How to use configure your template for dev containers", + "path": "./admin/templates/extending-templates/devcontainers.md" + }, + { + "title": "Process Logging", + "description": "Log workspace processes", + "path": "./admin/templates/extending-templates/process-logging.md", + "state": [ + "premium" + ] + } + ] + }, + { + "title": "Open in Coder", + "description": "Open workspaces in Coder", + "path": "./admin/templates/open-in-coder.md" + }, + { + "title": "Permissions \u0026 Policies", + "description": "Learn how to create templates with Terraform", + "path": "./admin/templates/template-permissions.md", + "state": [ + "premium" + ] + }, + { + "title": "Troubleshooting Templates", + "description": "Learn how to troubleshoot template issues", + "path": "./admin/templates/troubleshooting.md" + } + ] + }, + { + "title": "External Provisioners", + "description": "Learn how to run external provisioners with Coder", + "path": "./admin/provisioners/index.md", + "icon_path": "./images/icons/key.svg", + "state": [ + "premium" + ], + "children": [ + { + "title": "Manage Provisioner Jobs", + "description": "Learn how to run external provisioners with Coder", + "path": "./admin/provisioners/manage-provisioner-jobs.md", + "state": [ + "premium" + ] + } + ] + }, + { + "title": "External Authentication", + "description": "Learn how to configure external authentication", + "path": "./admin/external-auth/index.md", + "icon_path": "./images/icons/plug.svg" + }, + { + "title": "Integrations", + "description": "Use integrations to extend Coder", + "path": "./admin/integrations/index.md", + "icon_path": "./images/icons/puzzle.svg", + "children": [ + { + "title": "Prometheus", + "description": "Collect deployment metrics with Prometheus", + "path": "./admin/integrations/prometheus.md" + }, + { + "title": "Kubernetes Logging", + "description": "Stream K8s event logs on workspace startup", + "path": "./admin/integrations/kubernetes-logs.md" + }, + { + "title": "Additional Kubernetes Clusters", + "description": "Deploy workspaces on additional Kubernetes clusters", + "path": "./admin/integrations/multiple-kube-clusters.md" + }, + { + "title": "JFrog Artifactory", + "description": "Integrate Coder with JFrog Artifactory", + "path": "./admin/integrations/jfrog-artifactory.md" + }, + { + "title": "JFrog Xray", + "description": "Integrate Coder with JFrog Xray", + "path": "./admin/integrations/jfrog-xray.md" + }, + { + "title": "Island Secure Browser", + "description": "Integrate Coder with Island's Secure Browser", + "path": "./admin/integrations/island.md" + }, + { + "title": "DX PlatformX", + "description": "Integrate Coder with DX PlatformX", + "path": "./admin/integrations/platformx.md" + }, + { + "title": "DX", + "description": "Tag Coder Users with DX", + "path": "./admin/integrations/dx-data-cloud.md" + }, + { + "title": "Hashicorp Vault", + "description": "Integrate Coder with Hashicorp Vault", + "path": "./admin/integrations/vault.md" + } + ] + }, + { + "title": "Networking", + "description": "Understand Coder's networking layer", + "path": "./admin/networking/index.md", + "icon_path": "./images/icons/networking.svg", + "children": [ + { + "title": "Port Forwarding", + "description": "Learn how to forward ports in Coder", + "path": "./admin/networking/port-forwarding.md" + }, + { + "title": "STUN and NAT", + "description": "Learn how to forward ports in Coder", + "path": "./admin/networking/stun.md" + }, + { + "title": "Workspace Proxies", + "description": "Run geo distributed workspace proxies", + "path": "./admin/networking/workspace-proxies.md", + "state": [ + "premium" + ] + }, + { + "title": "High Availability", + "description": "Learn how to configure Coder for High Availability", + "path": "./admin/networking/high-availability.md", + "state": [ + "premium" + ] + }, + { + "title": "Troubleshooting", + "description": "Troubleshoot networking issues in Coder", + "path": "./admin/networking/troubleshooting.md" + } + ] + }, + { + "title": "Monitoring", + "description": "Configure security policy and audit your deployment", + "path": "./admin/monitoring/index.md", + "icon_path": "./images/icons/speed.svg", + "children": [ + { + "title": "Logs", + "description": "Learn about Coder's logs", + "path": "./admin/monitoring/logs.md" + }, + { + "title": "Metrics", + "description": "Learn about Coder's logs", + "path": "./admin/monitoring/metrics.md" + }, + { + "title": "Health Check", + "description": "Learn about Coder's automated health checks", + "path": "./admin/monitoring/health-check.md" + }, + { + "title": "Connection Logs", + "description": "Monitor connections to workspaces", + "path": "./admin/monitoring/connection-logs.md", + "state": [ + "premium" + ] + }, + { + "title": "Notifications", + "description": "Configure notifications for your deployment", + "path": "./admin/monitoring/notifications/index.md", + "children": [ + { + "title": "Slack Notifications", + "description": "Learn how to setup Slack notifications", + "path": "./admin/monitoring/notifications/slack.md" + }, + { + "title": "Microsoft Teams Notifications", + "description": "Learn how to setup Microsoft Teams notifications", + "path": "./admin/monitoring/notifications/teams.md" + } + ] + } + ] + }, + { + "title": "Security", + "description": "Configure security policy and audit your deployment", + "path": "./admin/security/index.md", + "icon_path": "./images/icons/lock.svg", + "children": [ + { + "title": "Audit Logs", + "description": "Audit actions taken inside Coder", + "path": "./admin/security/audit-logs.md", + "state": [ + "premium" + ] + }, + { + "title": "Secrets", + "description": "Use sensitive variables in your workspaces", + "path": "./admin/security/secrets.md" + }, + { + "title": "Database Encryption", + "description": "Encrypt the database to prevent unauthorized access", + "path": "./admin/security/database-encryption.md", + "state": [ + "premium" + ] + } + ] + }, + { + "title": "Licensing", + "description": "Configure licensing for your deployment", + "path": "./admin/licensing/index.md", + "icon_path": "./images/icons/licensing.svg" + } + ] + }, + { + "title": "Run AI Coding Agents in Coder", + "description": "Learn how to run and integrate agentic AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", + "path": "./ai-coder/index.md", + "icon_path": "./images/icons/wand.svg", + "children": [ + { + "title": "Best Practices", + "description": "Best Practices running Coding Agents", + "path": "./ai-coder/best-practices.md" + }, + { + "title": "In the IDE", + "description": "Run IDE agents with Coder", + "path": "./ai-coder/ide-agents.md" + }, + { + "title": "Coder Tasks", + "description": "Run Coding Agents on your Own Infrastructure", + "path": "./ai-coder/tasks.md", + "state": [ + "beta" + ], + "children": [ + { + "title": "Custom Agents", + "description": "Run custom agents with Coder Tasks", + "path": "./ai-coder/custom-agents.md", + "state": [ + "beta" + ] + }, + { + "title": "Security \u0026 Boundaries", + "description": "Learn about security and boundaries when running AI coding agents in Coder", + "path": "./ai-coder/security.md" + } + ] + }, + { + "title": "MCP Server", + "description": "Connect to agents Coder with a MCP server", + "path": "./ai-coder/mcp-server.md", + "state": [ + "beta" + ] + } + ] + }, + { + "title": "Tutorials", + "description": "Coder knowledgebase for administrating your deployment", + "path": "./tutorials/index.md", + "icon_path": "./images/icons/generic.svg", + "children": [ + { + "title": "Quickstart", + "description": "Learn how to install and run Coder quickly", + "path": "./tutorials/quickstart.md" + }, + { + "title": "Write a Template from Scratch", + "description": "Learn how to author Coder templates", + "path": "./tutorials/template-from-scratch.md" + }, + { + "title": "Using an External Database", + "description": "Use Coder with an external database", + "path": "./tutorials/external-database.md" + }, + { + "title": "Image Management", + "description": "Learn about image management with Coder", + "path": "./admin/templates/managing-templates/image-management.md" + }, + { + "title": "Configuring Okta", + "description": "Custom claims/scopes with Okta for group/role sync", + "path": "./tutorials/configuring-okta.md" + }, + { + "title": "Google to AWS Federation", + "description": "Federating a Google Cloud service account to AWS", + "path": "./tutorials/gcp-to-aws.md" + }, + { + "title": "JFrog Artifactory Integration", + "description": "Integrate Coder with JFrog Artifactory", + "path": "./admin/integrations/jfrog-artifactory.md" + }, + { + "title": "Istio Integration", + "description": "Integrate Coder with Istio", + "path": "./admin/integrations/istio.md" + }, + { + "title": "Island Secure Browser Integration", + "description": "Integrate Coder with Island's Secure Browser", + "path": "./admin/integrations/island.md" + }, + { + "title": "Template ImagePullSecrets", + "description": "Creating ImagePullSecrets for private registries", + "path": "./tutorials/image-pull-secret.md" + }, + { + "title": "Postgres SSL", + "description": "Configure Coder to connect to Postgres over SSL", + "path": "./tutorials/postgres-ssl.md" + }, + { + "title": "Azure Federation", + "description": "Federating Coder to Azure", + "path": "./tutorials/azure-federation.md" + }, + { + "title": "Deploy Coder on Azure with an Application Gateway", + "description": "Deploy Coder on Azure with an Application Gateway", + "path": "./install/kubernetes/kubernetes-azure-app-gateway.md" + }, + { + "title": "Scanning Workspaces with JFrog Xray", + "description": "Integrate Coder with JFrog Xray", + "path": "./admin/integrations/jfrog-xray.md" + }, + { + "title": "Cloning Git Repositories", + "description": "Learn how to clone Git repositories in Coder", + "path": "./tutorials/cloning-git-repositories.md" + }, + { + "title": "Test Templates Through CI/CD", + "description": "Learn how to test and publish Coder templates in a CI/CD pipeline", + "path": "./tutorials/testing-templates.md" + }, + { + "title": "Use Apache as a Reverse Proxy", + "description": "Learn how to use Apache as a reverse proxy", + "path": "./tutorials/reverse-proxy-apache.md" + }, + { + "title": "Use Caddy as a Reverse Proxy", + "description": "Learn how to use Caddy as a reverse proxy", + "path": "./tutorials/reverse-proxy-caddy.md" + }, + { + "title": "Use NGINX as a Reverse Proxy", + "description": "Learn how to use NGINX as a reverse proxy", + "path": "./tutorials/reverse-proxy-nginx.md" + }, + { + "title": "Pre-install JetBrains IDEs in Workspaces", + "description": "Pre-install JetBrains IDEs in workspaces", + "path": "./admin/templates/extending-templates/jetbrains-preinstall.md" + }, + { + "title": "Use JetBrains IDEs in Air-Gapped Deployments", + "description": "Configure JetBrains IDEs for air-gapped deployments", + "path": "./admin/templates/extending-templates/jetbrains-airgapped.md" + }, + { + "title": "FAQs", + "description": "Miscellaneous FAQs from our community", + "path": "./tutorials/faqs.md" + }, + { + "title": "Best practices", + "description": "Guides to help you make the most of your Coder experience", + "path": "./tutorials/best-practices/index.md", + "children": [ + { + "title": "Organizations - best practices", + "description": "How to make the best use of Coder Organizations", + "path": "./tutorials/best-practices/organizations.md" + }, + { + "title": "Scale Coder", + "description": "How to prepare a Coder deployment for scale", + "path": "./tutorials/best-practices/scale-coder.md" + }, + { + "title": "Security - best practices", + "description": "Make your Coder deployment more secure", + "path": "./tutorials/best-practices/security-best-practices.md" + }, + { + "title": "Speed up your workspaces", + "description": "Speed up your Coder templates and workspaces", + "path": "./tutorials/best-practices/speed-up-templates.md" + } + ] + } + ] + }, + { + "title": "Reference", + "description": "Reference", + "path": "./reference/index.md", + "icon_path": "./images/icons/notes.svg", + "children": [ + { + "title": "REST API", + "description": "Learn how to use Coderd API", + "path": "./reference/api/index.md", + "icon_path": "./images/icons/api.svg", + "children": [ + { + "title": "General", + "path": "./reference/api/general.md" + }, + { + "title": "Agents", + "path": "./reference/api/agents.md" + }, + { + "title": "Applications", + "path": "./reference/api/applications.md" + }, + { + "title": "Audit", + "path": "./reference/api/audit.md" + }, + { + "title": "Authentication", + "path": "./reference/api/authentication.md" + }, + { + "title": "Authorization", + "path": "./reference/api/authorization.md" + }, + { + "title": "Builds", + "path": "./reference/api/builds.md" + }, + { + "title": "Debug", + "path": "./reference/api/debug.md" + }, + { + "title": "Enterprise", + "path": "./reference/api/enterprise.md" + }, + { + "title": "Files", + "path": "./reference/api/files.md" + }, + { + "title": "Git", + "path": "./reference/api/git.md" + }, + { + "title": "Insights", + "path": "./reference/api/insights.md" + }, + { + "title": "Members", + "path": "./reference/api/members.md" + }, + { + "title": "Organizations", + "path": "./reference/api/organizations.md" + }, + { + "title": "PortSharing", + "path": "./reference/api/portsharing.md" + }, + { + "title": "Schemas", + "path": "./reference/api/schemas.md" + }, + { + "title": "Templates", + "path": "./reference/api/templates.md" + }, + { + "title": "Users", + "path": "./reference/api/users.md" + }, + { + "title": "WorkspaceProxies", + "path": "./reference/api/workspaceproxies.md" + }, + { + "title": "Workspaces", + "path": "./reference/api/workspaces.md" + } + ] + }, + { + "title": "Command Line", + "description": "Learn how to use Coder CLI", + "path": "./reference/cli/index.md", + "icon_path": "./images/icons/terminal.svg", + "children": [ + { + "title": "autoupdate", + "description": "Toggle auto-update policy for a workspace", + "path": "reference/cli/autoupdate.md" + }, + { + "title": "coder", + "path": "reference/cli/index.md" + }, + { + "title": "completion", + "description": "Install or update shell completion scripts for the detected or chosen shell.", + "path": "reference/cli/completion.md" + }, + { + "title": "config-ssh", + "description": "Add an SSH Host entry for your workspaces \"ssh workspace.coder\"", + "path": "reference/cli/config-ssh.md" + }, + { + "title": "create", + "description": "Create a workspace", + "path": "reference/cli/create.md" + }, + { + "title": "delete", + "description": "Delete a workspace", + "path": "reference/cli/delete.md" + }, + { + "title": "dotfiles", + "description": "Personalize your workspace by applying a canonical dotfiles repository", + "path": "reference/cli/dotfiles.md" + }, + { + "title": "external-auth", + "description": "Manage external authentication", + "path": "reference/cli/external-auth.md" + }, + { + "title": "external-auth access-token", + "description": "Print auth for an external provider", + "path": "reference/cli/external-auth_access-token.md" + }, + { + "title": "favorite", + "description": "Add a workspace to your favorites", + "path": "reference/cli/favorite.md" + }, + { + "title": "features", + "description": "List Enterprise features", + "path": "reference/cli/features.md" + }, + { + "title": "features list", + "path": "reference/cli/features_list.md" + }, + { + "title": "groups", + "description": "Manage groups", + "path": "reference/cli/groups.md" + }, + { + "title": "groups create", + "description": "Create a user group", + "path": "reference/cli/groups_create.md" + }, + { + "title": "groups delete", + "description": "Delete a user group", + "path": "reference/cli/groups_delete.md" + }, + { + "title": "groups edit", + "description": "Edit a user group", + "path": "reference/cli/groups_edit.md" + }, + { + "title": "groups list", + "description": "List user groups", + "path": "reference/cli/groups_list.md" + }, + { + "title": "licenses", + "description": "Add, delete, and list licenses", + "path": "reference/cli/licenses.md" + }, + { + "title": "licenses add", + "description": "Add license to Coder deployment", + "path": "reference/cli/licenses_add.md" + }, + { + "title": "licenses delete", + "description": "Delete license by ID", + "path": "reference/cli/licenses_delete.md" + }, + { + "title": "licenses list", + "description": "List licenses (including expired)", + "path": "reference/cli/licenses_list.md" + }, + { + "title": "list", + "description": "List workspaces", + "path": "reference/cli/list.md" + }, + { + "title": "login", + "description": "Authenticate with Coder deployment", + "path": "reference/cli/login.md" + }, + { + "title": "logout", + "description": "Unauthenticate your local session", + "path": "reference/cli/logout.md" + }, + { + "title": "netcheck", + "description": "Print network debug information for DERP and STUN", + "path": "reference/cli/netcheck.md" + }, + { + "title": "notifications", + "description": "Manage Coder notifications", + "path": "reference/cli/notifications.md" + }, + { + "title": "notifications pause", + "description": "Pause notifications", + "path": "reference/cli/notifications_pause.md" + }, + { + "title": "notifications resume", + "description": "Resume notifications", + "path": "reference/cli/notifications_resume.md" + }, + { + "title": "notifications test", + "description": "Send a test notification", + "path": "reference/cli/notifications_test.md" + }, + { + "title": "open", + "description": "Open a workspace", + "path": "reference/cli/open.md" + }, + { + "title": "open app", + "description": "Open a workspace application.", + "path": "reference/cli/open_app.md" + }, + { + "title": "open vscode", + "description": "Open a workspace in VS Code Desktop", + "path": "reference/cli/open_vscode.md" + }, + { + "title": "organizations", + "description": "Organization related commands", + "path": "reference/cli/organizations.md" + }, + { + "title": "organizations create", + "description": "Create a new organization.", + "path": "reference/cli/organizations_create.md" + }, + { + "title": "organizations members", + "description": "Manage organization members", + "path": "reference/cli/organizations_members.md" + }, + { + "title": "organizations members add", + "description": "Add a new member to the current organization", + "path": "reference/cli/organizations_members_add.md" + }, + { + "title": "organizations members edit-roles", + "description": "Edit organization member's roles", + "path": "reference/cli/organizations_members_edit-roles.md" + }, + { + "title": "organizations members list", + "description": "List all organization members", + "path": "reference/cli/organizations_members_list.md" + }, + { + "title": "organizations members remove", + "description": "Remove a new member to the current organization", + "path": "reference/cli/organizations_members_remove.md" + }, + { + "title": "organizations roles", + "description": "Manage organization roles.", + "path": "reference/cli/organizations_roles.md" + }, + { + "title": "organizations roles create", + "description": "Create a new organization custom role", + "path": "reference/cli/organizations_roles_create.md" + }, + { + "title": "organizations roles show", + "description": "Show role(s)", + "path": "reference/cli/organizations_roles_show.md" + }, + { + "title": "organizations roles update", + "description": "Update an organization custom role", + "path": "reference/cli/organizations_roles_update.md" + }, + { + "title": "organizations settings", + "description": "Manage organization settings.", + "path": "reference/cli/organizations_settings.md" + }, + { + "title": "organizations settings set", + "description": "Update specified organization setting.", + "path": "reference/cli/organizations_settings_set.md" + }, + { + "title": "organizations settings set group-sync", + "description": "Group sync settings to sync groups from an IdP.", + "path": "reference/cli/organizations_settings_set_group-sync.md" + }, + { + "title": "organizations settings set organization-sync", + "description": "Organization sync settings to sync organization memberships from an IdP.", + "path": "reference/cli/organizations_settings_set_organization-sync.md" + }, + { + "title": "organizations settings set role-sync", + "description": "Role sync settings to sync organization roles from an IdP.", + "path": "reference/cli/organizations_settings_set_role-sync.md" + }, + { + "title": "organizations settings show", + "description": "Outputs specified organization setting.", + "path": "reference/cli/organizations_settings_show.md" + }, + { + "title": "organizations settings show group-sync", + "description": "Group sync settings to sync groups from an IdP.", + "path": "reference/cli/organizations_settings_show_group-sync.md" + }, + { + "title": "organizations settings show organization-sync", + "description": "Organization sync settings to sync organization memberships from an IdP.", + "path": "reference/cli/organizations_settings_show_organization-sync.md" + }, + { + "title": "organizations settings show role-sync", + "description": "Role sync settings to sync organization roles from an IdP.", + "path": "reference/cli/organizations_settings_show_role-sync.md" + }, + { + "title": "organizations show", + "description": "Show the organization. Using \"selected\" will show the selected organization from the \"--org\" flag. Using \"me\" will show all organizations you are a member of.", + "path": "reference/cli/organizations_show.md" + }, + { + "title": "ping", + "description": "Ping a workspace", + "path": "reference/cli/ping.md" + }, + { + "title": "port-forward", + "description": "Forward ports from a workspace to the local machine. For reverse port forwarding, use \"coder ssh -R\".", + "path": "reference/cli/port-forward.md" + }, + { + "title": "prebuilds", + "description": "Manage Coder prebuilds", + "path": "reference/cli/prebuilds.md" + }, + { + "title": "prebuilds pause", + "description": "Pause prebuilds", + "path": "reference/cli/prebuilds_pause.md" + }, + { + "title": "prebuilds resume", + "description": "Resume prebuilds", + "path": "reference/cli/prebuilds_resume.md" + }, + { + "title": "provisioner", + "description": "View and manage provisioner daemons and jobs", + "path": "reference/cli/provisioner.md" + }, + { + "title": "provisioner jobs", + "description": "View and manage provisioner jobs", + "path": "reference/cli/provisioner_jobs.md" + }, + { + "title": "provisioner jobs cancel", + "description": "Cancel a provisioner job", + "path": "reference/cli/provisioner_jobs_cancel.md" + }, + { + "title": "provisioner jobs list", + "description": "List provisioner jobs", + "path": "reference/cli/provisioner_jobs_list.md" + }, + { + "title": "provisioner keys", + "description": "Manage provisioner keys", + "path": "reference/cli/provisioner_keys.md" + }, + { + "title": "provisioner keys create", + "description": "Create a new provisioner key", + "path": "reference/cli/provisioner_keys_create.md" + }, + { + "title": "provisioner keys delete", + "description": "Delete a provisioner key", + "path": "reference/cli/provisioner_keys_delete.md" + }, + { + "title": "provisioner keys list", + "description": "List provisioner keys in an organization", + "path": "reference/cli/provisioner_keys_list.md" + }, + { + "title": "provisioner list", + "description": "List provisioner daemons in an organization", + "path": "reference/cli/provisioner_list.md" + }, + { + "title": "provisioner start", + "description": "Run a provisioner daemon", + "path": "reference/cli/provisioner_start.md" + }, + { + "title": "publickey", + "description": "Output your Coder public key used for Git operations", + "path": "reference/cli/publickey.md" + }, + { + "title": "rename", + "description": "Rename a workspace", + "path": "reference/cli/rename.md" + }, + { + "title": "reset-password", + "description": "Directly connect to the database to reset a user's password", + "path": "reference/cli/reset-password.md" + }, + { + "title": "restart", + "description": "Restart a workspace", + "path": "reference/cli/restart.md" + }, + { + "title": "schedule", + "description": "Schedule automated start and stop times for workspaces", + "path": "reference/cli/schedule.md" + }, + { + "title": "schedule extend", + "description": "Extend the stop time of a currently running workspace instance.", + "path": "reference/cli/schedule_extend.md" + }, + { + "title": "schedule show", + "description": "Show workspace schedules", + "path": "reference/cli/schedule_show.md" + }, + { + "title": "schedule start", + "description": "Edit workspace start schedule", + "path": "reference/cli/schedule_start.md" + }, + { + "title": "schedule stop", + "description": "Edit workspace stop schedule", + "path": "reference/cli/schedule_stop.md" + }, + { + "title": "server", + "description": "Start a Coder server", + "path": "reference/cli/server.md" + }, + { + "title": "server create-admin-user", + "description": "Create a new admin user with the given username, email and password and adds it to every organization.", + "path": "reference/cli/server_create-admin-user.md" + }, + { + "title": "server dbcrypt", + "description": "Manage database encryption.", + "path": "reference/cli/server_dbcrypt.md" + }, + { + "title": "server dbcrypt decrypt", + "description": "Decrypt a previously encrypted database.", + "path": "reference/cli/server_dbcrypt_decrypt.md" + }, + { + "title": "server dbcrypt delete", + "description": "Delete all encrypted data from the database. THIS IS A DESTRUCTIVE OPERATION.", + "path": "reference/cli/server_dbcrypt_delete.md" + }, + { + "title": "server dbcrypt rotate", + "description": "Rotate database encryption keys.", + "path": "reference/cli/server_dbcrypt_rotate.md" + }, + { + "title": "server postgres-builtin-serve", + "description": "Run the built-in PostgreSQL deployment.", + "path": "reference/cli/server_postgres-builtin-serve.md" + }, + { + "title": "server postgres-builtin-url", + "description": "Output the connection URL for the built-in PostgreSQL deployment.", + "path": "reference/cli/server_postgres-builtin-url.md" + }, + { + "title": "show", + "description": "Display details of a workspace's resources and agents", + "path": "reference/cli/show.md" + }, + { + "title": "speedtest", + "description": "Run upload and download tests from your machine to a workspace", + "path": "reference/cli/speedtest.md" + }, + { + "title": "ssh", + "description": "Start a shell into a workspace or run a command", + "path": "reference/cli/ssh.md" + }, + { + "title": "start", + "description": "Start a workspace", + "path": "reference/cli/start.md" + }, + { + "title": "stat", + "description": "Show resource usage for the current workspace.", + "path": "reference/cli/stat.md" + }, + { + "title": "stat cpu", + "description": "Show CPU usage, in cores.", + "path": "reference/cli/stat_cpu.md" + }, + { + "title": "stat disk", + "description": "Show disk usage, in gigabytes.", + "path": "reference/cli/stat_disk.md" + }, + { + "title": "stat mem", + "description": "Show memory usage, in gigabytes.", + "path": "reference/cli/stat_mem.md" + }, + { + "title": "state", + "description": "Manually manage Terraform state to fix broken workspaces", + "path": "reference/cli/state.md" + }, + { + "title": "state pull", + "description": "Pull a Terraform state file from a workspace.", + "path": "reference/cli/state_pull.md" + }, + { + "title": "state push", + "description": "Push a Terraform state file to a workspace.", + "path": "reference/cli/state_push.md" + }, + { + "title": "stop", + "description": "Stop a workspace", + "path": "reference/cli/stop.md" + }, + { + "title": "support", + "description": "Commands for troubleshooting issues with a Coder deployment.", + "path": "reference/cli/support.md" + }, + { + "title": "support bundle", + "description": "Generate a support bundle to troubleshoot issues connecting to a workspace.", + "path": "reference/cli/support_bundle.md" + }, + { + "title": "templates", + "description": "Manage templates", + "path": "reference/cli/templates.md" + }, + { + "title": "templates archive", + "description": "Archive unused or failed template versions from a given template(s)", + "path": "reference/cli/templates_archive.md" + }, + { + "title": "templates create", + "description": "DEPRECATED: Create a template from the current directory or as specified by flag", + "path": "reference/cli/templates_create.md" + }, + { + "title": "templates delete", + "description": "Delete templates", + "path": "reference/cli/templates_delete.md" + }, + { + "title": "templates edit", + "description": "Edit the metadata of a template by name.", + "path": "reference/cli/templates_edit.md" + }, + { + "title": "templates init", + "description": "Get started with a templated template.", + "path": "reference/cli/templates_init.md" + }, + { + "title": "templates list", + "description": "List all the templates available for the organization", + "path": "reference/cli/templates_list.md" + }, + { + "title": "templates presets", + "description": "Manage presets of the specified template", + "path": "reference/cli/templates_presets.md" + }, + { + "title": "templates presets list", + "description": "List all presets of the specified template. Defaults to the active template version.", + "path": "reference/cli/templates_presets_list.md" + }, + { + "title": "templates pull", + "description": "Download the active, latest, or specified version of a template to a path.", + "path": "reference/cli/templates_pull.md" + }, + { + "title": "templates push", + "description": "Create or update a template from the current directory or as specified by flag", + "path": "reference/cli/templates_push.md" + }, + { + "title": "templates versions", + "description": "Manage different versions of the specified template", + "path": "reference/cli/templates_versions.md" + }, + { + "title": "templates versions archive", + "description": "Archive a template version(s).", + "path": "reference/cli/templates_versions_archive.md" + }, + { + "title": "templates versions list", + "description": "List all the versions of the specified template", + "path": "reference/cli/templates_versions_list.md" + }, + { + "title": "templates versions promote", + "description": "Promote a template version to active.", + "path": "reference/cli/templates_versions_promote.md" + }, + { + "title": "templates versions unarchive", + "description": "Unarchive a template version(s).", + "path": "reference/cli/templates_versions_unarchive.md" + }, + { + "title": "tokens", + "description": "Manage personal access tokens", + "path": "reference/cli/tokens.md" + }, + { + "title": "tokens create", + "description": "Create a token", + "path": "reference/cli/tokens_create.md" + }, + { + "title": "tokens list", + "description": "List tokens", + "path": "reference/cli/tokens_list.md" + }, + { + "title": "tokens remove", + "description": "Delete a token", + "path": "reference/cli/tokens_remove.md" + }, + { + "title": "unfavorite", + "description": "Remove a workspace from your favorites", + "path": "reference/cli/unfavorite.md" + }, + { + "title": "update", + "description": "Will update and start a given workspace if it is out of date. If the workspace is already running, it will be stopped first.", + "path": "reference/cli/update.md" + }, + { + "title": "users", + "description": "Manage users", + "path": "reference/cli/users.md" + }, + { + "title": "users activate", + "description": "Update a user's status to 'active'. Active users can fully interact with the platform", + "path": "reference/cli/users_activate.md" + }, + { + "title": "users create", + "description": "Create a new user.", + "path": "reference/cli/users_create.md" + }, + { + "title": "users delete", + "description": "Delete a user by username or user_id.", + "path": "reference/cli/users_delete.md" + }, + { + "title": "users edit-roles", + "description": "Edit a user's roles by username or id", + "path": "reference/cli/users_edit-roles.md" + }, + { + "title": "users list", + "description": "Prints the list of users.", + "path": "reference/cli/users_list.md" + }, + { + "title": "users show", + "description": "Show a single user. Use 'me' to indicate the currently authenticated user.", + "path": "reference/cli/users_show.md" + }, + { + "title": "users suspend", + "description": "Update a user's status to 'suspended'. A suspended user cannot log into the platform", + "path": "reference/cli/users_suspend.md" + }, + { + "title": "version", + "description": "Show coder version", + "path": "reference/cli/version.md" + }, + { + "title": "whoami", + "description": "Fetch authenticated user info for Coder deployment", + "path": "reference/cli/whoami.md" + } + ] + }, + { + "title": "Agent API", + "description": "Learn how to use Coder Agent API", + "path": "./reference/agent-api/index.md", + "icon_path": "./images/icons/api.svg", + "children": [ + { + "title": "Debug", + "path": "./reference/agent-api/debug.md" + }, + { + "title": "Schemas", + "path": "./reference/agent-api/schemas.md" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index 47d248335dda1..d8afa47fbb82a 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -161,12 +161,13 @@ func NewWithAPI(t *testing.T, options *Options) ( // LicenseOptions is used to generate a license for testing. // It supports the builder pattern for easy customization. type LicenseOptions struct { - AccountType string - AccountID string - DeploymentIDs []string - Trial bool - FeatureSet codersdk.FeatureSet - AllFeatures bool + AccountType string + AccountID string + DeploymentIDs []string + Trial bool + FeatureSet codersdk.FeatureSet + AllFeatures bool + PublishUsageData bool // GraceAt is the time at which the license will enter the grace period. GraceAt time.Time // ExpiresAt is the time at which the license will hard expire. @@ -279,15 +280,16 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { NotBefore: jwt.NewNumericDate(options.NotBefore), IssuedAt: jwt.NewNumericDate(issuedAt), }, - LicenseExpires: jwt.NewNumericDate(options.GraceAt), - AccountType: options.AccountType, - AccountID: options.AccountID, - DeploymentIDs: options.DeploymentIDs, - Trial: options.Trial, - Version: license.CurrentVersion, - AllFeatures: options.AllFeatures, - FeatureSet: options.FeatureSet, - Features: options.Features, + LicenseExpires: jwt.NewNumericDate(options.GraceAt), + AccountType: options.AccountType, + AccountID: options.AccountID, + DeploymentIDs: options.DeploymentIDs, + Trial: options.Trial, + Version: license.CurrentVersion, + AllFeatures: options.AllFeatures, + FeatureSet: options.FeatureSet, + Features: options.Features, + PublishUsageData: options.PublishUsageData, } return GenerateLicenseRaw(t, c) } diff --git a/enterprise/coderd/license/license.go b/enterprise/coderd/license/license.go index bc5c174d9fc3a..687a4aaf66746 100644 --- a/enterprise/coderd/license/license.go +++ b/enterprise/coderd/license/license.go @@ -584,6 +584,7 @@ type Claims struct { Version uint64 `json:"version"` Features Features `json:"features"` RequireTelemetry bool `json:"require_telemetry,omitempty"` + PublishUsageData bool `json:"publish_usage_data,omitempty"` } var _ jwt.Claims = &Claims{} diff --git a/enterprise/coderd/usage/collector.go b/enterprise/coderd/usage/collector.go new file mode 100644 index 0000000000000..99dcef6d10217 --- /dev/null +++ b/enterprise/coderd/usage/collector.go @@ -0,0 +1,67 @@ +package usage + +import ( + "context" + "encoding/json" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbtime" + agplusage "github.com/coder/coder/v2/coderd/usage" + "github.com/coder/quartz" +) + +// Collector collects usage events and stores them in the database for +// publishing. +type Collector struct { + clock quartz.Clock +} + +var _ agplusage.Collector = &Collector{} + +// NewCollector creates a new database-backed usage event collector. +func NewCollector(opts ...CollectorOption) *Collector { + c := &Collector{ + clock: quartz.NewReal(), + } + for _, opt := range opts { + opt(c) + } + return c +} + +type CollectorOption func(*Collector) + +// CollectorWithClock sets the quartz clock to use for the collector. +func CollectorWithClock(clock quartz.Clock) CollectorOption { + return func(c *Collector) { + c.clock = clock + } +} + +// CollectDiscreteUsageEvent implements agplusage.Collector. +func (c *Collector) CollectDiscreteUsageEvent(ctx context.Context, db database.Store, event agplusage.DiscreteEvent) error { + if !event.EventType().IsDiscrete() { + return xerrors.Errorf("event type %q is not a discrete event", event.EventType()) + } + if err := event.Valid(); err != nil { + return xerrors.Errorf("invalid %q event: %w", event.EventType(), err) + } + + jsonData, err := json.Marshal(event) + if err != nil { + return xerrors.Errorf("marshal event as JSON: %w", err) + } + + // Duplicate events are ignored by the query, so we don't need to check the + // error. + return db.InsertUsageEvent(ctx, database.InsertUsageEventParams{ + // Always generate a new UUID for discrete events. + ID: uuid.New().String(), + EventType: event.EventType(), + EventData: jsonData, + CreatedAt: dbtime.Time(c.clock.Now()), + }) +} diff --git a/enterprise/coderd/usage/collector_test.go b/enterprise/coderd/usage/collector_test.go new file mode 100644 index 0000000000000..6aa7bc605788b --- /dev/null +++ b/enterprise/coderd/usage/collector_test.go @@ -0,0 +1,85 @@ +package usage_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/database/dbtime" + agplusage "github.com/coder/coder/v2/coderd/usage" + "github.com/coder/coder/v2/enterprise/coderd/usage" + "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" +) + +func TestCollector(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + ctrl := gomock.NewController(t) + db := dbmock.NewMockStore(ctrl) + clock := quartz.NewMock(t) + collector := usage.NewCollector(usage.CollectorWithClock(clock)) + + now := dbtime.Now() + events := []struct { + time time.Time + event agplusage.DiscreteEvent + }{ + { + time: now, + event: agplusage.DCManagedAgentsV1{ + Count: 1, + }, + }, + { + time: now.Add(1 * time.Minute), + event: agplusage.DCManagedAgentsV1{ + Count: 2, + }, + }, + } + + for _, event := range events { + eventJSON := jsoninate(t, event.event) + db.EXPECT().InsertUsageEvent(ctx, gomock.Any()).DoAndReturn( + func(ctx interface{}, params database.InsertUsageEventParams) error { + _, err := uuid.Parse(params.ID) + assert.NoError(t, err) + assert.Equal(t, event.event.EventType(), params.EventType) + assert.JSONEq(t, eventJSON, string(params.EventData)) + assert.Equal(t, event.time, params.CreatedAt) + return nil + }, + ).Times(1) + + clock.Set(event.time) + err := collector.CollectDiscreteUsageEvent(ctx, db, event.event) + require.NoError(t, err) + } + }) + + t.Run("InvalidEvent", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + ctrl := gomock.NewController(t) + db := dbmock.NewMockStore(ctrl) + + // We should get an error if the event is invalid. + collector := usage.NewCollector() + err := collector.CollectDiscreteUsageEvent(ctx, db, agplusage.DCManagedAgentsV1{ + Count: 0, // invalid + }) + assert.ErrorContains(t, err, `invalid "dc_managed_agents_v1" event: count must be greater than 0`) + }) +} diff --git a/enterprise/coderd/usage/publisher.go b/enterprise/coderd/usage/publisher.go new file mode 100644 index 0000000000000..290691e44c4ed --- /dev/null +++ b/enterprise/coderd/usage/publisher.go @@ -0,0 +1,427 @@ +package usage + +import ( + "bytes" + "context" + "crypto/ed25519" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/cryptorand" + "github.com/coder/coder/v2/enterprise/coderd" + "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/quartz" +) + +const ( + CoderLicenseJWTHeader = "Coder-License-JWT" + + tallymanURL = "https://tallyman-ingress.coder.com" + tallymanIngestURLV1 = tallymanURL + "/api/v1/ingest" + + tallymanPublishInitialMinimumDelay = 5 * time.Minute + // Chosen to be a prime number and not a multiple of 5 like many other + // recurring tasks. + tallymanPublishInterval = 17 * time.Minute + tallymanPublishTimeout = 30 * time.Second + tallymanPublishBatchSize = 100 +) + +var errUsagePublishingDisabled = xerrors.New("usage publishing is not enabled by any license") + +// Publisher publishes usage events ***somewhere***. +type Publisher interface { + // Close closes the publisher and waits for it to finish. + io.Closer + // Start starts the publisher. It must only be called once. + Start() error +} + +type tallymanPublisher struct { + ctx context.Context + ctxCancel context.CancelFunc + log slog.Logger + db database.Store + done chan struct{} + + // Configured with options: + ingestURL string + httpClient *http.Client + clock quartz.Clock + licenseKeys map[string]ed25519.PublicKey + initialDelay time.Duration +} + +var _ Publisher = &tallymanPublisher{} + +// NewTallymanPublisher creates a Publisher that publishes usage events to +// Coder's Tallyman service. +func NewTallymanPublisher(ctx context.Context, log slog.Logger, db database.Store, opts ...TallymanPublisherOption) Publisher { + ctx, cancel := context.WithCancel(ctx) + publisher := &tallymanPublisher{ + ctx: ctx, + ctxCancel: cancel, + log: log, + db: db, + done: make(chan struct{}), + + ingestURL: tallymanIngestURLV1, + httpClient: http.DefaultClient, + clock: quartz.NewReal(), + licenseKeys: coderd.Keys, + } + for _, opt := range opts { + opt(publisher) + } + return publisher +} + +type TallymanPublisherOption func(*tallymanPublisher) + +// PublisherWithHTTPClient sets the HTTP client to use for publishing usage events. +func PublisherWithHTTPClient(httpClient *http.Client) TallymanPublisherOption { + return func(p *tallymanPublisher) { + p.httpClient = httpClient + } +} + +// PublisherWithClock sets the clock to use for publishing usage events. +func PublisherWithClock(clock quartz.Clock) TallymanPublisherOption { + return func(p *tallymanPublisher) { + p.clock = clock + } +} + +// PublisherWithLicenseKeys sets the license public keys to use for license +// validation. +func PublisherWithLicenseKeys(keys map[string]ed25519.PublicKey) TallymanPublisherOption { + return func(p *tallymanPublisher) { + p.licenseKeys = keys + } +} + +// PublisherWithIngestURL sets the ingest URL to use for publishing usage +// events. +func PublisherWithIngestURL(ingestURL string) TallymanPublisherOption { + return func(p *tallymanPublisher) { + p.ingestURL = ingestURL + } +} + +// PublisherWithInitialDelay sets the initial delay for the publisher. +func PublisherWithInitialDelay(initialDelay time.Duration) TallymanPublisherOption { + return func(p *tallymanPublisher) { + p.initialDelay = initialDelay + } +} + +// Start implements Publisher. +func (p *tallymanPublisher) Start() error { + deploymentID, err := p.db.GetDeploymentID(p.ctx) + if err != nil { + return xerrors.Errorf("get deployment ID: %w", err) + } + deploymentUUID, err := uuid.Parse(deploymentID) + if err != nil { + return xerrors.Errorf("parse deployment ID %q: %w", deploymentID, err) + } + + if p.initialDelay <= 0 { + // Pick a random time between tallymanPublishInitialMinimumDelay and + // tallymanPublishInterval. + maxPlusDelay := int(tallymanPublishInterval - tallymanPublishInitialMinimumDelay) + plusDelay, err := cryptorand.Intn(maxPlusDelay) + if err != nil { + return xerrors.Errorf("could not generate random start delay: %w", err) + } + p.initialDelay = tallymanPublishInitialMinimumDelay + time.Duration(plusDelay) + } + + go p.publishLoop(p.ctx, deploymentUUID) + return nil +} + +func (p *tallymanPublisher) publishLoop(ctx context.Context, deploymentID uuid.UUID) { + defer close(p.done) + + // Start the ticker with the initial delay. We will reset it to the interval + // after the first tick. + ticker := p.clock.NewTicker(p.initialDelay) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + + err := p.publish(ctx, deploymentID) + if err != nil { + p.log.Warn(ctx, "publish usage events to tallyman", slog.Error(err)) + } + ticker.Reset(tallymanPublishInterval) + } +} + +// publish publishes usage events to Tallyman in a loop until there is an error +// (or any rejection) or there are no more events to publish. +func (p *tallymanPublisher) publish(ctx context.Context, deploymentID uuid.UUID) error { + for { + publishCtx, publishCtxCancel := context.WithTimeout(ctx, tallymanPublishTimeout) + accepted, err := p.publishOnce(publishCtx, deploymentID) + publishCtxCancel() + if err != nil { + return xerrors.Errorf("publish usage events to tallyman: %w", err) + } + if accepted < tallymanPublishBatchSize { + // We published less than the batch size, so we're done. + return nil + } + } +} + +// publishOnce publishes up to tallymanPublishBatchSize usage events to +// tallyman. It returns the number of successfully published events. +func (p *tallymanPublisher) publishOnce(ctx context.Context, deploymentID uuid.UUID) (int, error) { + licenseJwt, err := p.getBestLicenseJWT(ctx) + if xerrors.Is(err, errUsagePublishingDisabled) { + return 0, nil + } else if err != nil { + return 0, xerrors.Errorf("find usage publishing license: %w", err) + } + + events, err := p.db.SelectUsageEventsForPublishing(ctx, dbtime.Time(p.clock.Now())) + if err != nil { + return 0, xerrors.Errorf("select usage events for publishing: %w", err) + } + if len(events) == 0 { + // No events to publish. + return 0, nil + } + + var ( + eventIDs = make(map[string]struct{}) + tallymanReq = TallymanIngestRequestV1{ + DeploymentID: deploymentID, + Events: make([]TallymanIngestEventV1, 0, len(events)), + } + ) + for _, event := range events { + eventIDs[event.ID] = struct{}{} + tallymanReq.Events = append(tallymanReq.Events, TallymanIngestEventV1{ + ID: event.ID, + EventType: event.EventType, + EventData: event.EventData, + CreatedAt: event.CreatedAt, + }) + } + if len(eventIDs) != len(events) { + // This should never happen due to the unique constraint in the + // database. + return 0, xerrors.Errorf("duplicate event IDs found in events for publishing") + } + + resp, err := p.sendPublishRequest(ctx, licenseJwt, tallymanReq) + allFailed := err != nil + if err != nil { + p.log.Warn(ctx, "failed to send publish request to tallyman", slog.F("count", len(events)), slog.Error(err)) + // Fake a response with all events temporarily rejected. + resp = TallymanIngestResponseV1{ + AcceptedEvents: []TallymanIngestAcceptedEventV1{}, + RejectedEvents: make([]TallymanIngestRejectedEventV1, len(events)), + } + for i, event := range events { + resp.RejectedEvents[i] = TallymanIngestRejectedEventV1{ + ID: event.ID, + Message: fmt.Sprintf("failed to publish to tallyman: %v", err), + Permanent: false, + } + } + } else { + p.log.Debug(ctx, "published usage events to tallyman", slog.F("accepted", len(resp.AcceptedEvents)), slog.F("rejected", len(resp.RejectedEvents))) + } + + if len(resp.AcceptedEvents)+len(resp.RejectedEvents) != len(events) { + p.log.Warn(ctx, "tallyman returned a different number of events than we sent", slog.F("sent", len(events)), slog.F("accepted", len(resp.AcceptedEvents)), slog.F("rejected", len(resp.RejectedEvents))) + } + + var ( + acceptedEvents = make(map[string]*TallymanIngestAcceptedEventV1) + rejectedEvents = make(map[string]*TallymanIngestRejectedEventV1) + ) + for _, event := range resp.AcceptedEvents { + acceptedEvents[event.ID] = &event + } + for _, event := range resp.RejectedEvents { + rejectedEvents[event.ID] = &event + } + + dbUpdate := database.UpdateUsageEventsPostPublishParams{ + Now: dbtime.Time(p.clock.Now()), + IDs: make([]string, len(events)), + FailureMessages: make([]string, len(events)), + SetPublishedAts: make([]bool, len(events)), + } + for i, event := range events { + dbUpdate.IDs[i] = event.ID + if _, ok := acceptedEvents[event.ID]; ok { + dbUpdate.FailureMessages[i] = "" + dbUpdate.SetPublishedAts[i] = true + } else if rejectedEvent, ok := rejectedEvents[event.ID]; ok { + if !allFailed { + // These are all going to have the same message, so don't log + // them. We already logged the overall error above. + p.log.Warn(ctx, "tallyman rejected usage event", slog.F("id", event.ID), slog.F("message", rejectedEvent.Message), slog.F("permanent", rejectedEvent.Permanent)) + } + dbUpdate.FailureMessages[i] = rejectedEvent.Message + dbUpdate.SetPublishedAts[i] = rejectedEvent.Permanent + } else { + // It's not good if this path gets hit, but we'll handle it as if it + // was a temporary rejection. + p.log.Warn(ctx, "tallyman did not include a usage event in the response, considering it temporarily rejected", slog.F("id", event.ID)) + dbUpdate.FailureMessages[i] = "tallyman did not include the event in the response" + dbUpdate.SetPublishedAts[i] = false + } + } + + err = p.db.UpdateUsageEventsPostPublish(ctx, dbUpdate) + if err != nil { + return 0, xerrors.Errorf("update usage events post publish: %w", err) + } + + var returnErr error + if len(resp.RejectedEvents) > 0 { + returnErr = xerrors.New("some events were rejected by tallyman") + } + return len(resp.AcceptedEvents), returnErr +} + +// getBestLicenseJWT returns the best license JWT to use for the request. The +// criteria is as follows: +// - The license must be valid and active (after nbf, before exp) +// - The license must have usage publishing enabled +// The most recently issued (iat) license is chosen. +// +// If no licenses are found or none have usage publishing enabled, +// errUsagePublishingDisabled is returned. +func (p *tallymanPublisher) getBestLicenseJWT(ctx context.Context) (string, error) { + licenses, err := p.db.GetUnexpiredLicenses(ctx) + if err != nil { + return "", xerrors.Errorf("get unexpired licenses: %w", err) + } + if len(licenses) == 0 { + return "", errUsagePublishingDisabled + } + + type licenseJWTWithClaims struct { + Claims *license.Claims + Raw string + } + + var bestLicense licenseJWTWithClaims + for _, dbLicense := range licenses { + claims, err := license.ParseClaims(dbLicense.JWT, p.licenseKeys) + if err != nil { + p.log.Warn(ctx, "failed to parse license claims", slog.F("license_id", dbLicense.ID), slog.Error(err)) + continue + } + + // IssuedAt is verified to be non-nil in license.ParseClaims. + if claims.PublishUsageData && (bestLicense.Claims == nil || claims.IssuedAt.Time.After(bestLicense.Claims.IssuedAt.Time)) { + bestLicense = licenseJWTWithClaims{ + Claims: claims, + Raw: dbLicense.JWT, + } + } + } + + if bestLicense.Raw == "" { + return "", errUsagePublishingDisabled + } + + return bestLicense.Raw, nil +} + +func (p *tallymanPublisher) sendPublishRequest(ctx context.Context, licenseJwt string, req TallymanIngestRequestV1) (TallymanIngestResponseV1, error) { + body, err := json.Marshal(req) + if err != nil { + return TallymanIngestResponseV1{}, err + } + + r, err := http.NewRequestWithContext(ctx, http.MethodPost, p.ingestURL, bytes.NewReader(body)) + if err != nil { + return TallymanIngestResponseV1{}, err + } + r.Header.Set(CoderLicenseJWTHeader, licenseJwt) + + resp, err := p.httpClient.Do(r) + if err != nil { + return TallymanIngestResponseV1{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errBody TallymanErrorV1 + if err := json.NewDecoder(resp.Body).Decode(&errBody); err != nil { + errBody = TallymanErrorV1{ + Message: fmt.Sprintf("could not decode error response body: %v", err), + } + } + return TallymanIngestResponseV1{}, xerrors.Errorf("unexpected status code %v, error: %s", resp.StatusCode, errBody.Message) + } + + var respBody TallymanIngestResponseV1 + if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil { + return TallymanIngestResponseV1{}, xerrors.Errorf("decode response body: %w", err) + } + + return respBody, nil +} + +// Close implements Publisher. +func (p *tallymanPublisher) Close() error { + p.ctxCancel() + <-p.done + return nil +} + +type TallymanErrorV1 struct { + Message string `json:"message"` +} + +type TallymanIngestRequestV1 struct { + DeploymentID uuid.UUID `json:"deployment_id"` + Events []TallymanIngestEventV1 `json:"events"` +} + +type TallymanIngestEventV1 struct { + ID string `json:"id"` + EventType database.UsageEventType `json:"event_type"` + EventData json.RawMessage `json:"event_data"` + CreatedAt time.Time `json:"created_at"` +} + +type TallymanIngestResponseV1 struct { + AcceptedEvents []TallymanIngestAcceptedEventV1 `json:"accepted_events"` + RejectedEvents []TallymanIngestRejectedEventV1 `json:"rejected_events"` +} + +type TallymanIngestAcceptedEventV1 struct { + ID string `json:"id"` +} + +type TallymanIngestRejectedEventV1 struct { + ID string `json:"id"` + Message string `json:"message"` + Permanent bool `json:"permanent"` +} diff --git a/enterprise/coderd/usage/publisher_test.go b/enterprise/coderd/usage/publisher_test.go new file mode 100644 index 0000000000000..5e2579a4089ba --- /dev/null +++ b/enterprise/coderd/usage/publisher_test.go @@ -0,0 +1,702 @@ +package usage_test + +import ( + "context" + "database/sql" + "encoding/json" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + "go.uber.org/mock/gomock" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/dbtime" + agplusage "github.com/coder/coder/v2/coderd/usage" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/usage" + "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m, testutil.GoleakOptions...) +} + +// TestIntegration tests the collector and publisher by running them with a real +// database. +func TestIntegration(t *testing.T) { + t.Parallel() + const eventCount = 3 + + ctx := testutil.Context(t, testutil.WaitLong) + log := slogtest.Make(t, nil) + db, _ := dbtestutil.NewDB(t) + clock := quartz.NewMock(t) + deploymentID, licenseJWT := configureDeployment(ctx, t, db) + now := time.Now() + + var ( + calls int64 + handler func(req usage.TallymanIngestRequestV1) any + ) + ingestURL := fakeServer(t, tallymanHandler(t, licenseJWT, func(req usage.TallymanIngestRequestV1) any { + callCount := atomic.AddInt64(&calls, 1) + t.Logf("tallyman backend received call %d", callCount) + assert.Equal(t, deploymentID, req.DeploymentID) + + if handler == nil { + t.Errorf("handler is nil") + return usage.TallymanIngestResponseV1{} + } + return handler(req) + })) + + collector := usage.NewCollector( + usage.CollectorWithClock(clock), + ) + // Insert an old event that should never be published. + clock.Set(now.Add(-31 * 24 * time.Hour)) + err := collector.CollectDiscreteUsageEvent(ctx, db, agplusage.DCManagedAgentsV1{ + Count: 31, + }) + require.NoError(t, err) + + // Insert the events we expect to be published. + clock.Set(now.Add(1 * time.Second)) + for i := 0; i < eventCount; i++ { + clock.Advance(time.Second) + err := collector.CollectDiscreteUsageEvent(ctx, db, agplusage.DCManagedAgentsV1{ + Count: uint64(i + 1), // nolint:gosec // these numbers are tiny and will not overflow + }) + require.NoErrorf(t, err, "collecting event %d", i) + } + + publisher := usage.NewTallymanPublisher(ctx, log, db, + usage.PublisherWithClock(clock), + usage.PublisherWithIngestURL(ingestURL), + usage.PublisherWithLicenseKeys(coderdenttest.Keys), + ) + defer publisher.Close() + + // Start the publisher with a trap. + tickerTrap := clock.Trap().NewTicker() + defer tickerTrap.Close() + startErr := make(chan error) + go func() { + err := publisher.Start() + testutil.RequireSend(ctx, t, startErr, err) + }() + tickerCall := tickerTrap.MustWait(ctx) + tickerCall.MustRelease(ctx) + // The initial duration will always be some time between 5m and 17m. + require.GreaterOrEqual(t, tickerCall.Duration, 5*time.Minute) + require.LessOrEqual(t, tickerCall.Duration, 17*time.Minute) + require.NoError(t, testutil.RequireReceive(ctx, t, startErr)) + + // Set up a trap for the ticker.Reset call. + tickerResetTrap := clock.Trap().TickerReset() + defer tickerResetTrap.Close() + + // Configure the handler for the first publish. This handler will accept the + // first event, temporarily reject the second, and permanently reject the + // third. + var temporarilyRejectedEventID string + handler = func(req usage.TallymanIngestRequestV1) any { + // On the first call, accept the first event, temporarily reject the + // second, and permanently reject the third. + acceptedEvents := make([]usage.TallymanIngestAcceptedEventV1, 1) + rejectedEvents := make([]usage.TallymanIngestRejectedEventV1, 2) + if assert.Len(t, req.Events, eventCount) { + assert.JSONEqf(t, jsoninate(t, agplusage.DCManagedAgentsV1{ + Count: 1, + }), string(req.Events[0].EventData), "event data did not match for event %d", 0) + acceptedEvents[0].ID = req.Events[0].ID + + temporarilyRejectedEventID = req.Events[1].ID + assert.JSONEqf(t, jsoninate(t, agplusage.DCManagedAgentsV1{ + Count: 2, + }), string(req.Events[1].EventData), "event data did not match for event %d", 1) + rejectedEvents[0].ID = req.Events[1].ID + rejectedEvents[0].Message = "temporarily rejected" + rejectedEvents[0].Permanent = false + + assert.JSONEqf(t, jsoninate(t, agplusage.DCManagedAgentsV1{ + Count: 3, + }), string(req.Events[2].EventData), "event data did not match for event %d", 2) + rejectedEvents[1].ID = req.Events[2].ID + rejectedEvents[1].Message = "permanently rejected" + rejectedEvents[1].Permanent = true + } + return usage.TallymanIngestResponseV1{ + AcceptedEvents: acceptedEvents, + RejectedEvents: rejectedEvents, + } + } + + // Advance the clock to the initial tick, which should trigger the first + // publish, then wait for the reset call. The duration will always be 17m + // for resets (only the initial tick is variable). + clock.Advance(tickerCall.Duration) + tickerResetCall := tickerResetTrap.MustWait(ctx) + require.Equal(t, 17*time.Minute, tickerResetCall.Duration) + tickerResetCall.MustRelease(ctx) + + // The publisher should have published the events once. + require.Equal(t, int64(1), atomic.LoadInt64(&calls)) + + // Set the handler for the next publish call. This call should only include + // the temporarily rejected event from earlier. This time we'll accept it. + handler = func(req usage.TallymanIngestRequestV1) any { + assert.Len(t, req.Events, 1) + acceptedEvents := make([]usage.TallymanIngestAcceptedEventV1, len(req.Events)) + for i, event := range req.Events { + assert.Equal(t, temporarilyRejectedEventID, event.ID) + acceptedEvents[i].ID = event.ID + } + return usage.TallymanIngestResponseV1{ + AcceptedEvents: acceptedEvents, + RejectedEvents: []usage.TallymanIngestRejectedEventV1{}, + } + } + + // Advance the clock to the next tick and wait for the reset call. + clock.Advance(tickerResetCall.Duration) + tickerResetCall = tickerResetTrap.MustWait(ctx) + tickerResetCall.MustRelease(ctx) + + // The publisher should have published the events again. + require.Equal(t, int64(2), atomic.LoadInt64(&calls)) + + // There should be no more publish calls after this, so set the handler to + // nil. + handler = nil + + // Advance the clock to the next tick. + clock.Advance(tickerResetCall.Duration) + tickerResetTrap.MustWait(ctx).MustRelease(ctx) + + // No publish should have taken place since there are no more events to + // publish. + require.Equal(t, int64(2), atomic.LoadInt64(&calls)) + + require.NoError(t, publisher.Close()) +} + +func TestPublisherNoEligibleLicenses(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + log := slogtest.Make(t, nil) + ctrl := gomock.NewController(t) + db := dbmock.NewMockStore(ctrl) + clock := quartz.NewMock(t) + + // Configure the deployment manually. + deploymentID := uuid.New() + db.EXPECT().GetDeploymentID(gomock.Any()).Return(deploymentID.String(), nil).Times(1) + + var calls int64 + ingestURL := fakeServer(t, tallymanHandler(t, "", func(req usage.TallymanIngestRequestV1) any { + atomic.AddInt64(&calls, 1) + return usage.TallymanIngestResponseV1{ + AcceptedEvents: []usage.TallymanIngestAcceptedEventV1{}, + RejectedEvents: []usage.TallymanIngestRejectedEventV1{}, + } + })) + + publisher := usage.NewTallymanPublisher(ctx, log, db, + usage.PublisherWithClock(clock), + usage.PublisherWithIngestURL(ingestURL), + usage.PublisherWithLicenseKeys(coderdenttest.Keys), + ) + defer publisher.Close() + + // Start the publisher with a trap. + tickerTrap := clock.Trap().NewTicker() + defer tickerTrap.Close() + startErr := make(chan error) + go func() { + err := publisher.Start() + testutil.RequireSend(ctx, t, startErr, err) + }() + tickerCall := tickerTrap.MustWait(ctx) + tickerCall.MustRelease(ctx) + require.NoError(t, testutil.RequireReceive(ctx, t, startErr)) + + // Mock zero licenses. + db.EXPECT().GetUnexpiredLicenses(gomock.Any()).Return([]database.License{}, nil).Times(1) + + // Tick and wait for the reset call. + tickerResetTrap := clock.Trap().TickerReset() + defer tickerResetTrap.Close() + clock.Advance(tickerCall.Duration) + tickerResetCall := tickerResetTrap.MustWait(ctx) + tickerResetCall.MustRelease(ctx) + + // The publisher should not have published the events. + require.Equal(t, int64(0), atomic.LoadInt64(&calls)) + + // Mock a single license with usage publishing disabled. + licenseJWT := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + PublishUsageData: false, + }) + db.EXPECT().GetUnexpiredLicenses(gomock.Any()).Return([]database.License{ + { + ID: 1, + JWT: licenseJWT, + UploadedAt: dbtime.Now(), + Exp: dbtime.Now().Add(48 * time.Hour), // fake + UUID: uuid.New(), + }, + }, nil).Times(1) + + // Tick and wait for the reset call. + clock.Advance(tickerResetCall.Duration) + tickerResetTrap.MustWait(ctx).MustRelease(ctx) + + // The publisher should still not have published the events. + require.Equal(t, int64(0), atomic.LoadInt64(&calls)) +} + +// TestPublisherClaimExpiry tests the claim query to ensure that events are not +// claimed if they've recently been claimed by another publisher. +func TestPublisherClaimExpiry(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + log := slogtest.Make(t, nil) + db, _ := dbtestutil.NewDB(t) + clock := quartz.NewMock(t) + _, licenseJWT := configureDeployment(ctx, t, db) + now := time.Now() + + var calls int64 + ingestURL := fakeServer(t, tallymanHandler(t, licenseJWT, func(req usage.TallymanIngestRequestV1) any { + atomic.AddInt64(&calls, 1) + return tallymanAcceptAllHandler(req) + })) + + collector := usage.NewCollector( + usage.CollectorWithClock(clock), + ) + + publisher := usage.NewTallymanPublisher(ctx, log, db, + usage.PublisherWithClock(clock), + usage.PublisherWithIngestURL(ingestURL), + usage.PublisherWithLicenseKeys(coderdenttest.Keys), + usage.PublisherWithInitialDelay(17*time.Minute), + ) + defer publisher.Close() + + // Create an event that was claimed 1h-18m ago. The ticker has a forced + // delay of 17m in this test. + clock.Set(now) + err := collector.CollectDiscreteUsageEvent(ctx, db, agplusage.DCManagedAgentsV1{ + Count: 1, + }) + require.NoError(t, err) + // Claim the event in the past. Claiming it this way via the database + // directly means it won't be marked as published or unclaimed. + events, err := db.SelectUsageEventsForPublishing(ctx, now.Add(-42*time.Minute)) + require.NoError(t, err) + require.Len(t, events, 1) + + // Start the publisher with a trap. + tickerTrap := clock.Trap().NewTicker() + defer tickerTrap.Close() + startErr := make(chan error) + go func() { + err := publisher.Start() + testutil.RequireSend(ctx, t, startErr, err) + }() + tickerCall := tickerTrap.MustWait(ctx) + require.Equal(t, 17*time.Minute, tickerCall.Duration) + tickerCall.MustRelease(ctx) + require.NoError(t, testutil.RequireReceive(ctx, t, startErr)) + + // Set up a trap for the ticker.Reset call. + tickerResetTrap := clock.Trap().TickerReset() + defer tickerResetTrap.Close() + + // Advance the clock to the initial tick, which should trigger the first + // publish, then wait for the reset call. The duration will always be 17m + // for resets (only the initial tick is variable). + clock.Advance(tickerCall.Duration) + tickerResetCall := tickerResetTrap.MustWait(ctx) + require.Equal(t, 17*time.Minute, tickerResetCall.Duration) + tickerResetCall.MustRelease(ctx) + + // No events should have been published since none are eligible. + require.Equal(t, int64(0), atomic.LoadInt64(&calls)) + + // Advance the clock to the next tick and wait for the reset call. + clock.Advance(tickerResetCall.Duration) + tickerResetCall = tickerResetTrap.MustWait(ctx) + tickerResetCall.MustRelease(ctx) + + // The publisher should have published the event, as it's now eligible. + require.Equal(t, int64(1), atomic.LoadInt64(&calls)) +} + +// TestPublisherMissingEvents tests that the publisher notices events that are +// not returned by the Tallyman server and marks them as temporarily rejected. +func TestPublisherMissingEvents(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + log := slogtest.Make(t, nil) + ctrl := gomock.NewController(t) + db := dbmock.NewMockStore(ctrl) + _, licenseJWT := configureMockDeployment(t, db) + clock := quartz.NewMock(t) + now := time.Now() + clock.Set(now) + + var calls int64 + ingestURL := fakeServer(t, tallymanHandler(t, licenseJWT, func(req usage.TallymanIngestRequestV1) any { + atomic.AddInt64(&calls, 1) + return usage.TallymanIngestResponseV1{ + AcceptedEvents: []usage.TallymanIngestAcceptedEventV1{}, + RejectedEvents: []usage.TallymanIngestRejectedEventV1{}, + } + })) + + publisher := usage.NewTallymanPublisher(ctx, log, db, + usage.PublisherWithClock(clock), + usage.PublisherWithIngestURL(ingestURL), + usage.PublisherWithLicenseKeys(coderdenttest.Keys), + ) + + // Expect the publisher to call SelectUsageEventsForPublishing, followed by + // UpdateUsageEventsPostPublish. + events := []database.UsageEvent{ + { + ID: uuid.New().String(), + EventType: database.UsageEventTypeDcManagedAgentsV1, + EventData: []byte(jsoninate(t, agplusage.DCManagedAgentsV1{ + Count: 1, + })), + CreatedAt: now, + PublishedAt: sql.NullTime{}, + PublishStartedAt: sql.NullTime{}, + FailureMessage: sql.NullString{}, + }, + } + db.EXPECT().SelectUsageEventsForPublishing(gomock.Any(), gomock.Any()).Return(events, nil).Times(1) + db.EXPECT().UpdateUsageEventsPostPublish(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, params database.UpdateUsageEventsPostPublishParams) error { + assert.Equal(t, []string{events[0].ID}, params.IDs) + assert.Equal(t, []string{"tallyman did not include the event in the response"}, params.FailureMessages) + assert.Equal(t, []bool{false}, params.SetPublishedAts) + return nil + }, + ).Times(1) + + // Start the publisher with a trap. + tickerTrap := clock.Trap().NewTicker() + defer tickerTrap.Close() + startErr := make(chan error) + go func() { + err := publisher.Start() + testutil.RequireSend(ctx, t, startErr, err) + }() + tickerCall := tickerTrap.MustWait(ctx) + tickerCall.MustRelease(ctx) + require.NoError(t, testutil.RequireReceive(ctx, t, startErr)) + + // Tick and wait for the reset call. + tickerResetTrap := clock.Trap().TickerReset() + defer tickerResetTrap.Close() + clock.Advance(tickerCall.Duration) + tickerResetTrap.MustWait(ctx).MustRelease(ctx) + + // The publisher should have published the events once. + require.Equal(t, int64(1), atomic.LoadInt64(&calls)) + + require.NoError(t, publisher.Close()) +} + +func TestPublisherLicenseSelection(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + log := slogtest.Make(t, nil) + ctrl := gomock.NewController(t) + db := dbmock.NewMockStore(ctrl) + clock := quartz.NewMock(t) + now := time.Now() + + // Configure the deployment manually. + deploymentID := uuid.New() + db.EXPECT().GetDeploymentID(gomock.Any()).Return(deploymentID.String(), nil).Times(1) + + // Insert multiple licenses: + // 1. PublishUsageData false, iat 30m ago (ineligible, publish not enabled) + // 2. PublishUsageData true, iat 1h ago (eligible) + // 3. PublishUsageData true, iat 30m ago, exp 10m ago (ineligible, expired) + badLicense1 := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + PublishUsageData: false, + IssuedAt: now.Add(-30 * time.Minute), + }) + expectedLicense := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + PublishUsageData: true, + IssuedAt: now.Add(-1 * time.Hour), + }) + badLicense2 := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + PublishUsageData: true, + IssuedAt: now.Add(-30 * time.Minute), + ExpiresAt: now.Add(-10 * time.Minute), + }) + // GetUnexpiredLicenses is not supposed to return expired licenses, but for + // the purposes of this test we're going to do it anyway. + db.EXPECT().GetUnexpiredLicenses(gomock.Any()).Return([]database.License{ + { + ID: 1, + JWT: badLicense1, + Exp: now.Add(48 * time.Hour), // fake, should be ignored by publisher code anyway + UUID: uuid.New(), + UploadedAt: now, + }, + { + ID: 2, + JWT: expectedLicense, + Exp: now.Add(48 * time.Hour), // fake + UUID: uuid.New(), + UploadedAt: now, + }, + { + ID: 3, + JWT: badLicense2, + Exp: now.Add(48 * time.Hour), // fake + UUID: uuid.New(), + UploadedAt: now, + }, + }, nil) + + var calls int64 + ingestURL := fakeServer(t, tallymanHandler(t, expectedLicense, func(req usage.TallymanIngestRequestV1) any { + atomic.AddInt64(&calls, 1) + assert.Equal(t, deploymentID, req.DeploymentID) + return tallymanAcceptAllHandler(req) + })) + + publisher := usage.NewTallymanPublisher(ctx, log, db, + usage.PublisherWithClock(clock), + usage.PublisherWithIngestURL(ingestURL), + usage.PublisherWithLicenseKeys(coderdenttest.Keys), + ) + defer publisher.Close() + + // Start the publisher with a trap. + tickerTrap := clock.Trap().NewTicker() + defer tickerTrap.Close() + startErr := make(chan error) + go func() { + err := publisher.Start() + testutil.RequireSend(ctx, t, startErr, err) + }() + tickerCall := tickerTrap.MustWait(ctx) + tickerCall.MustRelease(ctx) + require.NoError(t, testutil.RequireReceive(ctx, t, startErr)) + + // Mock events to be published. + events := []database.UsageEvent{ + { + ID: uuid.New().String(), + EventType: database.UsageEventTypeDcManagedAgentsV1, + EventData: []byte(jsoninate(t, agplusage.DCManagedAgentsV1{ + Count: 1, + })), + }, + } + db.EXPECT().SelectUsageEventsForPublishing(gomock.Any(), gomock.Any()).Return(events, nil).Times(1) + db.EXPECT().UpdateUsageEventsPostPublish(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, params database.UpdateUsageEventsPostPublishParams) error { + assert.Equal(t, []string{events[0].ID}, params.IDs) + assert.Equal(t, []string{""}, params.FailureMessages) + assert.Equal(t, []bool{true}, params.SetPublishedAts) + return nil + }, + ).Times(1) + + // Tick and wait for the reset call. + tickerResetTrap := clock.Trap().TickerReset() + defer tickerResetTrap.Close() + clock.Advance(tickerCall.Duration) + tickerResetTrap.MustWait(ctx).MustRelease(ctx) + + // The publisher should have published the events once. + require.Equal(t, int64(1), atomic.LoadInt64(&calls)) +} + +func TestPublisherTallymanError(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + log := slogtest.Make(t, nil) + ctrl := gomock.NewController(t) + db := dbmock.NewMockStore(ctrl) + clock := quartz.NewMock(t) + now := time.Now() + clock.Set(now) + + _, licenseJWT := configureMockDeployment(t, db) + const errorMessage = "tallyman error" + var calls int64 + ingestURL := fakeServer(t, tallymanHandler(t, licenseJWT, func(req usage.TallymanIngestRequestV1) any { + atomic.AddInt64(&calls, 1) + return usage.TallymanErrorV1{ + Message: errorMessage, + } + })) + + publisher := usage.NewTallymanPublisher(ctx, log, db, + usage.PublisherWithClock(clock), + usage.PublisherWithIngestURL(ingestURL), + usage.PublisherWithLicenseKeys(coderdenttest.Keys), + ) + defer publisher.Close() + + // Start the publisher with a trap. + tickerTrap := clock.Trap().NewTicker() + defer tickerTrap.Close() + startErr := make(chan error) + go func() { + err := publisher.Start() + testutil.RequireSend(ctx, t, startErr, err) + }() + tickerCall := tickerTrap.MustWait(ctx) + tickerCall.MustRelease(ctx) + require.NoError(t, testutil.RequireReceive(ctx, t, startErr)) + + // Mock events to be published. + events := []database.UsageEvent{ + { + ID: uuid.New().String(), + EventType: database.UsageEventTypeDcManagedAgentsV1, + EventData: []byte(jsoninate(t, agplusage.DCManagedAgentsV1{ + Count: 1, + })), + }, + } + db.EXPECT().SelectUsageEventsForPublishing(gomock.Any(), gomock.Any()).Return(events, nil).Times(1) + db.EXPECT().UpdateUsageEventsPostPublish(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, params database.UpdateUsageEventsPostPublishParams) error { + assert.Equal(t, []string{events[0].ID}, params.IDs) + assert.Contains(t, params.FailureMessages[0], errorMessage) + assert.Equal(t, []bool{false}, params.SetPublishedAts) + return nil + }, + ).Times(1) + + // Tick and wait for the reset call. + tickerResetTrap := clock.Trap().TickerReset() + defer tickerResetTrap.Close() + clock.Advance(tickerCall.Duration) + tickerResetTrap.MustWait(ctx).MustRelease(ctx) + + // The publisher should have published the events once. + require.Equal(t, int64(1), atomic.LoadInt64(&calls)) +} + +func jsoninate(t *testing.T, v any) string { + t.Helper() + buf, err := json.Marshal(v) + require.NoError(t, err) + return string(buf) +} + +func configureDeployment(ctx context.Context, t *testing.T, db database.Store) (uuid.UUID, string) { + t.Helper() + deploymentID := uuid.New() + err := db.InsertDeploymentID(ctx, deploymentID.String()) + require.NoError(t, err) + + licenseRaw := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + PublishUsageData: true, + }) + _, err = db.InsertLicense(ctx, database.InsertLicenseParams{ + UploadedAt: dbtime.Now(), + JWT: licenseRaw, + Exp: dbtime.Now().Add(48 * time.Hour), + UUID: uuid.New(), + }) + require.NoError(t, err) + + return deploymentID, licenseRaw +} + +func configureMockDeployment(t *testing.T, db *dbmock.MockStore) (uuid.UUID, string) { + t.Helper() + deploymentID := uuid.New() + db.EXPECT().GetDeploymentID(gomock.Any()).Return(deploymentID.String(), nil).Times(1) + + licenseRaw := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ + PublishUsageData: true, + }) + db.EXPECT().GetUnexpiredLicenses(gomock.Any()).Return([]database.License{ + { + ID: 1, + UploadedAt: dbtime.Now(), + JWT: licenseRaw, + Exp: dbtime.Now().Add(48 * time.Hour), + UUID: uuid.New(), + }, + }, nil) + + return deploymentID, licenseRaw +} + +func fakeServer(t *testing.T, handler http.Handler) string { + t.Helper() + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + return server.URL +} + +func tallymanHandler(t *testing.T, expectLicenseJWT string, handler func(req usage.TallymanIngestRequestV1) any) http.Handler { + t.Helper() + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + t.Helper() + licenseJWT := r.Header.Get(usage.CoderLicenseJWTHeader) + if expectLicenseJWT != "" && !assert.Equal(t, expectLicenseJWT, licenseJWT, "license JWT in request did not match") { + rw.WriteHeader(http.StatusUnauthorized) + err := json.NewEncoder(rw).Encode(usage.TallymanErrorV1{ + Message: "license JWT in request did not match", + }) + require.NoError(t, err) + return + } + + var req usage.TallymanIngestRequestV1 + err := json.NewDecoder(r.Body).Decode(&req) + require.NoError(t, err) + + resp := handler(req) + switch resp.(type) { + case usage.TallymanErrorV1: + rw.WriteHeader(http.StatusInternalServerError) + default: + rw.WriteHeader(http.StatusOK) + } + err = json.NewEncoder(rw).Encode(resp) + require.NoError(t, err) + }) +} + +func tallymanAcceptAllHandler(req usage.TallymanIngestRequestV1) usage.TallymanIngestResponseV1 { + acceptedEvents := make([]usage.TallymanIngestAcceptedEventV1, len(req.Events)) + for i, event := range req.Events { + acceptedEvents[i].ID = event.ID + } + + return usage.TallymanIngestResponseV1{ + AcceptedEvents: acceptedEvents, + RejectedEvents: []usage.TallymanIngestRejectedEventV1{}, + } +} From 939aba423ab6e77cdc71c19457e722a50f445a9d Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 30 Jul 2025 07:04:16 +0000 Subject: [PATCH 2/3] add fixture --- .../000353_create_usage_events_table.up.sql | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 coderd/database/migrations/testdata/fixtures/000353_create_usage_events_table.up.sql diff --git a/coderd/database/migrations/testdata/fixtures/000353_create_usage_events_table.up.sql b/coderd/database/migrations/testdata/fixtures/000353_create_usage_events_table.up.sql new file mode 100644 index 0000000000000..aa7c53f5eb94c --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000353_create_usage_events_table.up.sql @@ -0,0 +1,60 @@ +INSERT INTO usage_events ( + id, + event_type, + event_data, + created_at, + publish_started_at, + published_at, + failure_message +) +VALUES +-- Unpublished dc_managed_agents_v1 event. +( + 'event1', + 'dc_managed_agents_v1', + '{"count":1}', + '2023-01-01 00:00:00+00', + NULL, + NULL, + NULL +), +-- Successfully published dc_managed_agents_v1 event. +( + 'event2', + 'dc_managed_agents_v1', + '{"count":2}', + '2023-01-01 00:00:00+00', + NULL, + '2023-01-01 00:00:02+00', + NULL +), +-- Publish in progress dc_managed_agents_v1 event. +( + 'event3', + 'dc_managed_agents_v1', + '{"count":3}', + '2023-01-01 00:00:00+00', + '2023-01-01 00:00:01+00', + NULL, + NULL +), +-- Temporarily failed to publish dc_managed_agents_v1 event. +( + 'event4', + 'dc_managed_agents_v1', + '{"count":4}', + '2023-01-01 00:00:00+00', + NULL, + NULL, + 'publish failed temporarily' +), +-- Permanently failed to publish dc_managed_agents_v1 event. +( + 'event5', + 'dc_managed_agents_v1', + '{"count":5}', + '2023-01-01 00:00:00+00', + NULL, + '2023-01-01 00:00:02+00', + 'publish failed permanently' +) From e3763114022d160fb141fbf819a998b14e984b99 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 30 Jul 2025 07:07:58 +0000 Subject: [PATCH 3/3] fix manifest.json --- docs/manifest.json | 3640 ++++++++++++++++++++++---------------------- 1 file changed, 1799 insertions(+), 1841 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 965963cf0f2eb..0305105c029fd 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1,1842 +1,1800 @@ { - "versions": [ - "main" - ], - "routes": [ - { - "title": "About", - "description": "Coder docs", - "path": "./README.md", - "icon_path": "./images/icons/home.svg", - "children": [ - { - "title": "Screenshots", - "description": "View screenshots of the Coder platform", - "path": "./about/screenshots.md" - }, - { - "title": "Quickstart", - "description": "Learn how to install and run Coder quickly", - "path": "./tutorials/quickstart.md" - }, - { - "title": "Support", - "description": "How Coder supports your deployment and you", - "path": "./support/index.md", - "children": [ - { - "title": "Generate a Support Bundle", - "description": "Generate and upload a Support Bundle to Coder Support", - "path": "./support/support-bundle.md" - } - ] - }, - { - "title": "Contributing", - "description": "Learn how to contribute to Coder", - "path": "./about/contributing/CONTRIBUTING.md", - "icon_path": "./images/icons/contributing.svg", - "children": [ - { - "title": "Code of Conduct", - "description": "See the code of conduct for contributing to Coder", - "path": "./about/contributing/CODE_OF_CONDUCT.md", - "icon_path": "./images/icons/circle-dot.svg" - }, - { - "title": "Documentation", - "description": "Our style guide for use when authoring documentation", - "path": "./about/contributing/documentation.md", - "icon_path": "./images/icons/document.svg" - }, - { - "title": "Modules", - "description": "Learn how to contribute modules to Coder", - "path": "./about/contributing/modules.md", - "icon_path": "./images/icons/gear.svg" - }, - { - "title": "Templates", - "description": "Learn how to contribute templates to Coder", - "path": "./about/contributing/templates.md", - "icon_path": "./images/icons/picture.svg" - }, - { - "title": "Backend", - "description": "Our guide for backend development", - "path": "./about/contributing/backend.md", - "icon_path": "./images/icons/gear.svg" - }, - { - "title": "Frontend", - "description": "Our guide for frontend development", - "path": "./about/contributing/frontend.md", - "icon_path": "./images/icons/frontend.svg" - }, - { - "title": "Security", - "description": "Security vulnerability disclosure policy", - "path": "./about/contributing/SECURITY.md", - "icon_path": "./images/icons/lock.svg" - } - ] - } - ] - }, - { - "title": "Install", - "description": "Installing Coder", - "path": "./install/index.md", - "icon_path": "./images/icons/download.svg", - "children": [ - { - "title": "Coder CLI", - "description": "Install the standalone binary", - "path": "./install/cli.md", - "icon_path": "./images/icons/terminal.svg" - }, - { - "title": "Docker", - "description": "Install Coder using Docker", - "path": "./install/docker.md", - "icon_path": "./images/icons/docker.svg" - }, - { - "title": "Kubernetes", - "description": "Install Coder on Kubernetes", - "path": "./install/kubernetes.md", - "icon_path": "./images/icons/kubernetes.svg", - "children": [ - { - "title": "Deploy Coder on Azure with an Application Gateway", - "description": "Deploy Coder on Azure with an Application Gateway", - "path": "./install/kubernetes/kubernetes-azure-app-gateway.md" - } - ] - }, - { - "title": "Rancher", - "description": "Deploy Coder on Rancher", - "path": "./install/rancher.md", - "icon_path": "./images/icons/rancher.svg" - }, - { - "title": "OpenShift", - "description": "Install Coder on OpenShift", - "path": "./install/openshift.md", - "icon_path": "./images/icons/openshift.svg" - }, - { - "title": "Cloud Providers", - "description": "Install Coder on cloud providers", - "path": "./install/cloud/index.md", - "icon_path": "./images/icons/cloud.svg", - "children": [ - { - "title": "AWS EC2", - "description": "Install Coder on AWS EC2", - "path": "./install/cloud/ec2.md" - }, - { - "title": "GCP Compute Engine", - "description": "Install Coder on GCP Compute Engine", - "path": "./install/cloud/compute-engine.md" - }, - { - "title": "Azure VM", - "description": "Install Coder on an Azure VM", - "path": "./install/cloud/azure-vm.md" - } - ] - }, - { - "title": "Offline Deployments", - "description": "Run Coder in offline / air-gapped environments", - "path": "./install/offline.md", - "icon_path": "./images/icons/lan.svg" - }, - { - "title": "Unofficial Install Methods", - "description": "Other installation methods", - "path": "./install/other/index.md", - "icon_path": "./images/icons/generic.svg" - }, - { - "title": "Upgrading", - "description": "Learn how to upgrade Coder", - "path": "./install/upgrade.md", - "icon_path": "./images/icons/upgrade.svg" - }, - { - "title": "Uninstall", - "description": "Learn how to uninstall Coder", - "path": "./install/uninstall.md", - "icon_path": "./images/icons/trash.svg" - }, - { - "title": "Releases", - "description": "Learn about the Coder release channels and schedule", - "path": "./install/releases/index.md", - "icon_path": "./images/icons/star.svg", - "children": [ - { - "title": "Feature stages", - "description": "Information about pre-GA stages.", - "path": "./install/releases/feature-stages.md" - } - ] - } - ] - }, - { - "title": "User Guides", - "description": "Guides for end-users of Coder", - "path": "./user-guides/index.md", - "icon_path": "./images/icons/users.svg", - "children": [ - { - "title": "Access Workspaces", - "description": "Connect to your Coder workspaces", - "path": "./user-guides/workspace-access/index.md", - "icon_path": "./images/icons/access.svg", - "children": [ - { - "title": "Visual Studio Code", - "description": "Use VSCode with Coder in the desktop or browser", - "path": "./user-guides/workspace-access/vscode.md" - }, - { - "title": "JetBrains IDEs", - "description": "Use JetBrains IDEs with Coder", - "path": "./user-guides/workspace-access/jetbrains/index.md", - "children": [ - { - "title": "JetBrains Fleet", - "description": "Connect JetBrains Fleet to a Coder workspace", - "path": "./user-guides/workspace-access/jetbrains/fleet.md" - }, - { - "title": "JetBrains Gateway", - "description": "Use JetBrains Gateway to connect to Coder workspaces", - "path": "./user-guides/workspace-access/jetbrains/gateway.md" - }, - { - "title": "JetBrains Toolbox", - "description": "Access Coder workspaces from JetBrains Toolbox", - "path": "./user-guides/workspace-access/jetbrains/toolbox.md", - "state": [ - "beta" - ] - } - ] - }, - { - "title": "Remote Desktop", - "description": "Use RDP in Coder", - "path": "./user-guides/workspace-access/remote-desktops.md" - }, - { - "title": "Emacs TRAMP", - "description": "Use Emacs TRAMP in Coder", - "path": "./user-guides/workspace-access/emacs-tramp.md" - }, - { - "title": "Port Forwarding", - "description": "Access ports on your workspace", - "path": "./user-guides/workspace-access/port-forwarding.md" - }, - { - "title": "Filebrowser", - "description": "Access your workspace files", - "path": "./user-guides/workspace-access/filebrowser.md" - }, - { - "title": "Web IDEs and Coder Apps", - "description": "Access your workspace with IDEs in the browser", - "path": "./user-guides/workspace-access/web-ides.md" - }, - { - "title": "Zed", - "description": "Access your workspace with Zed", - "path": "./user-guides/workspace-access/zed.md" - }, - { - "title": "Cursor", - "description": "Access your workspace with Cursor", - "path": "./user-guides/workspace-access/cursor.md" - }, - { - "title": "Windsurf", - "description": "Access your workspace with Windsurf", - "path": "./user-guides/workspace-access/windsurf.md" - } - ] - }, - { - "title": "Coder Desktop", - "description": "Transform remote workspaces into seamless local development environments with no port forwarding required", - "path": "./user-guides/desktop/index.md", - "icon_path": "./images/icons/computer-code.svg", - "children": [ - { - "title": "Coder Desktop connect and sync", - "description": "Use Coder Desktop to manage your workspace code and files locally", - "path": "./user-guides/desktop/desktop-connect-sync.md" - } - ] - }, - { - "title": "Workspace Management", - "description": "Manage workspaces", - "path": "./user-guides/workspace-management.md", - "icon_path": "./images/icons/generic.svg" - }, - { - "title": "Workspace Scheduling", - "description": "Cost control with workspace schedules", - "path": "./user-guides/workspace-scheduling.md", - "icon_path": "./images/icons/stopwatch.svg" - }, - { - "title": "Workspace Lifecycle", - "description": "A guide to the workspace lifecycle, from creation and status through stopping and deletion.", - "path": "./user-guides/workspace-lifecycle.md", - "icon_path": "./images/icons/circle-dot.svg" - }, - { - "title": "Dev Containers Integration", - "description": "Run containerized development environments in your Coder workspace using the dev containers specification.", - "path": "./user-guides/devcontainers/index.md", - "icon_path": "./images/icons/container.svg", - "children": [ - { - "title": "Working with dev containers", - "description": "Access dev containers via SSH, your IDE, or web terminal.", - "path": "./user-guides/devcontainers/working-with-dev-containers.md" - }, - { - "title": "Troubleshooting dev containers", - "description": "Diagnose and resolve common issues with dev containers in your Coder workspace.", - "path": "./user-guides/devcontainers/troubleshooting-dev-containers.md" - } - ] - }, - { - "title": "Dotfiles", - "description": "Personalize your environment with dotfiles", - "path": "./user-guides/workspace-dotfiles.md", - "icon_path": "./images/icons/art-pad.svg" - } - ] - }, - { - "title": "Administration", - "description": "Guides for template and deployment administrators", - "path": "./admin/index.md", - "icon_path": "./images/icons/wrench.svg", - "children": [ - { - "title": "Setup", - "description": "Configure user access to your control plane.", - "path": "./admin/setup/index.md", - "icon_path": "./images/icons/toggle_on.svg", - "children": [ - { - "title": "Appearance", - "description": "Learn how to configure the appearance of Coder", - "path": "./admin/setup/appearance.md", - "state": [ - "premium" - ] - }, - { - "title": "Telemetry", - "description": "Learn what usage telemetry Coder collects", - "path": "./admin/setup/telemetry.md" - } - ] - }, - { - "title": "Infrastructure", - "description": "How to integrate Coder with your organization's compute", - "path": "./admin/infrastructure/index.md", - "icon_path": "./images/icons/container.svg", - "children": [ - { - "title": "Architecture", - "description": "Learn about Coder's architecture", - "path": "./admin/infrastructure/architecture.md" - }, - { - "title": "Validated Architectures", - "description": "Architectures for large Coder deployments", - "path": "./admin/infrastructure/validated-architectures/index.md", - "children": [ - { - "title": "Up to 1,000 Users", - "description": "Hardware specifications and architecture guidance for Coder deployments that support up to 1,000 users", - "path": "./admin/infrastructure/validated-architectures/1k-users.md" - }, - { - "title": "Up to 2,000 Users", - "description": "Hardware specifications and architecture guidance for Coder deployments that support up to 2,000 users", - "path": "./admin/infrastructure/validated-architectures/2k-users.md" - }, - { - "title": "Up to 3,000 Users", - "description": "Enterprise-scale architecture recommendations for Coder deployments that support up to 3,000 users", - "path": "./admin/infrastructure/validated-architectures/3k-users.md" - } - ] - }, - { - "title": "Scale Testing", - "description": "Ensure your deployment can handle your organization's needs", - "path": "./admin/infrastructure/scale-testing.md" - }, - { - "title": "Scaling Utilities", - "description": "Tools to help you scale your deployment", - "path": "./admin/infrastructure/scale-utility.md" - }, - { - "title": "Scaling best practices", - "description": "How to prepare a Coder deployment for scale", - "path": "./tutorials/best-practices/scale-coder.md" - } - ] - }, - { - "title": "Users", - "description": "Learn how to manage and audit users", - "path": "./admin/users/index.md", - "icon_path": "./images/icons/users.svg", - "children": [ - { - "title": "OIDC Authentication", - "description": "Configure OpenID Connect authentication with identity providers like Okta or Active Directory", - "path": "./admin/users/oidc-auth/index.md", - "children": [ - { - "title": "Configure OIDC refresh tokens", - "description": "How to configure OIDC refresh tokens", - "path": "./admin/users/oidc-auth/refresh-tokens.md" - } - ] - }, - { - "title": "GitHub Authentication", - "description": "Set up authentication through GitHub OAuth to enable secure user login and sign-up", - "path": "./admin/users/github-auth.md" - }, - { - "title": "Password Authentication", - "description": "Manage username/password authentication settings and user password reset workflows", - "path": "./admin/users/password-auth.md" - }, - { - "title": "Headless Authentication", - "description": "Create and manage headless service accounts for automated systems and API integrations", - "path": "./admin/users/headless-auth.md" - }, - { - "title": "Groups \u0026 Roles", - "description": "Manage access control with user groups and role-based permissions for Coder resources", - "path": "./admin/users/groups-roles.md", - "state": [ - "premium" - ] - }, - { - "title": "IdP Sync", - "description": "Synchronize user groups, roles, and organizations from your identity provider to Coder", - "path": "./admin/users/idp-sync.md", - "state": [ - "premium" - ] - }, - { - "title": "Organizations", - "description": "Segment and isolate resources by creating separate organizations for different teams or projects", - "path": "./admin/users/organizations.md", - "state": [ - "premium" - ] - }, - { - "title": "Quotas", - "description": "Control resource usage by implementing workspace budgets and credit-based cost management", - "path": "./admin/users/quotas.md", - "state": [ - "premium" - ] - }, - { - "title": "Sessions \u0026 API Tokens", - "description": "Manage authentication tokens for API access and configure session duration policies", - "path": "./admin/users/sessions-tokens.md" - } - ] - }, - { - "title": "Templates", - "description": "Learn how to author and maintain Coder templates", - "path": "./admin/templates/index.md", - "icon_path": "./images/icons/picture.svg", - "children": [ - { - "title": "Creating Templates", - "description": "Learn how to create templates with Terraform", - "path": "./admin/templates/creating-templates.md" - }, - { - "title": "Managing Templates", - "description": "Learn how to manage templates and best practices", - "path": "./admin/templates/managing-templates/index.md", - "children": [ - { - "title": "Image Management", - "description": "Learn about template image management", - "path": "./admin/templates/managing-templates/image-management.md" - }, - { - "title": "Change Management", - "description": "Learn about template change management and versioning", - "path": "./admin/templates/managing-templates/change-management.md" - }, - { - "title": "Dev containers", - "description": "Learn about using development containers in templates", - "path": "./admin/templates/managing-templates/devcontainers/index.md", - "children": [ - { - "title": "Add a dev container template", - "description": "How to add a dev container template to Coder", - "path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md" - }, - { - "title": "Dev container security and caching", - "description": "Configure dev container authentication and caching", - "path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md" - }, - { - "title": "Dev container releases and known issues", - "description": "Dev container releases and known issues", - "path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md" - } - ] - }, - { - "title": "Template Dependencies", - "description": "Learn how to manage template dependencies", - "path": "./admin/templates/managing-templates/dependencies.md" - }, - { - "title": "Workspace Scheduling", - "description": "Learn how to control how workspaces are started and stopped", - "path": "./admin/templates/managing-templates/schedule.md" - } - ] - }, - { - "title": "Extending Templates", - "description": "Learn best practices in extending templates", - "path": "./admin/templates/extending-templates/index.md", - "children": [ - { - "title": "Agent Metadata", - "description": "Retrieve real-time stats from the workspace agent", - "path": "./admin/templates/extending-templates/agent-metadata.md" - }, - { - "title": "Build Parameters", - "description": "Use parameters to customize workspaces at build", - "path": "./admin/templates/extending-templates/parameters.md" - }, - { - "title": "Dynamic Parameters", - "description": "Conditional, identity-aware parameter syntax for advanced users.", - "path": "./admin/templates/extending-templates/dynamic-parameters.md", - "state": [ - "beta" - ] - }, - { - "title": "Prebuilt workspaces", - "description": "Pre-provision a ready-to-deploy workspace with a defined set of parameters", - "path": "./admin/templates/extending-templates/prebuilt-workspaces.md", - "state": [ - "premium" - ] - }, - { - "title": "Icons", - "description": "Customize your template with built-in icons", - "path": "./admin/templates/extending-templates/icons.md" - }, - { - "title": "Resource Metadata", - "description": "Display resource state in the workspace dashboard", - "path": "./admin/templates/extending-templates/resource-metadata.md" - }, - { - "title": "Resource Monitoring", - "description": "Monitor resources in the workspace dashboard", - "path": "./admin/templates/extending-templates/resource-monitoring.md" - }, - { - "title": "Resource Ordering", - "description": "Design the UI of workspaces", - "path": "./admin/templates/extending-templates/resource-ordering.md" - }, - { - "title": "Resource Persistence", - "description": "Control resource persistence", - "path": "./admin/templates/extending-templates/resource-persistence.md" - }, - { - "title": "Terraform Variables", - "description": "Use variables to manage template state", - "path": "./admin/templates/extending-templates/variables.md" - }, - { - "title": "Terraform Modules", - "description": "Reuse terraform code across templates", - "path": "./admin/templates/extending-templates/modules.md" - }, - { - "title": "Web IDEs and Coder Apps", - "description": "Add and configure Web IDEs in your templates as coder apps", - "path": "./admin/templates/extending-templates/web-ides.md" - }, - { - "title": "Pre-install JetBrains IDEs", - "description": "Pre-install JetBrains IDEs in a template for faster IDE startup", - "path": "./admin/templates/extending-templates/jetbrains-preinstall.md" - }, - { - "title": "JetBrains IDEs in Air-Gapped Deployments", - "description": "Configure JetBrains IDEs for air-gapped deployments", - "path": "./admin/templates/extending-templates/jetbrains-airgapped.md" - }, - { - "title": "Docker in Workspaces", - "description": "Use Docker in your workspaces", - "path": "./admin/templates/extending-templates/docker-in-workspaces.md" - }, - { - "title": "Workspace Tags", - "description": "Control provisioning using Workspace Tags and Parameters", - "path": "./admin/templates/extending-templates/workspace-tags.md" - }, - { - "title": "Provider Authentication", - "description": "Authenticate with provider APIs to provision workspaces", - "path": "./admin/templates/extending-templates/provider-authentication.md" - }, - { - "title": "Configure a template for dev containers", - "description": "How to use configure your template for dev containers", - "path": "./admin/templates/extending-templates/devcontainers.md" - }, - { - "title": "Process Logging", - "description": "Log workspace processes", - "path": "./admin/templates/extending-templates/process-logging.md", - "state": [ - "premium" - ] - } - ] - }, - { - "title": "Open in Coder", - "description": "Open workspaces in Coder", - "path": "./admin/templates/open-in-coder.md" - }, - { - "title": "Permissions \u0026 Policies", - "description": "Learn how to create templates with Terraform", - "path": "./admin/templates/template-permissions.md", - "state": [ - "premium" - ] - }, - { - "title": "Troubleshooting Templates", - "description": "Learn how to troubleshoot template issues", - "path": "./admin/templates/troubleshooting.md" - } - ] - }, - { - "title": "External Provisioners", - "description": "Learn how to run external provisioners with Coder", - "path": "./admin/provisioners/index.md", - "icon_path": "./images/icons/key.svg", - "state": [ - "premium" - ], - "children": [ - { - "title": "Manage Provisioner Jobs", - "description": "Learn how to run external provisioners with Coder", - "path": "./admin/provisioners/manage-provisioner-jobs.md", - "state": [ - "premium" - ] - } - ] - }, - { - "title": "External Authentication", - "description": "Learn how to configure external authentication", - "path": "./admin/external-auth/index.md", - "icon_path": "./images/icons/plug.svg" - }, - { - "title": "Integrations", - "description": "Use integrations to extend Coder", - "path": "./admin/integrations/index.md", - "icon_path": "./images/icons/puzzle.svg", - "children": [ - { - "title": "Prometheus", - "description": "Collect deployment metrics with Prometheus", - "path": "./admin/integrations/prometheus.md" - }, - { - "title": "Kubernetes Logging", - "description": "Stream K8s event logs on workspace startup", - "path": "./admin/integrations/kubernetes-logs.md" - }, - { - "title": "Additional Kubernetes Clusters", - "description": "Deploy workspaces on additional Kubernetes clusters", - "path": "./admin/integrations/multiple-kube-clusters.md" - }, - { - "title": "JFrog Artifactory", - "description": "Integrate Coder with JFrog Artifactory", - "path": "./admin/integrations/jfrog-artifactory.md" - }, - { - "title": "JFrog Xray", - "description": "Integrate Coder with JFrog Xray", - "path": "./admin/integrations/jfrog-xray.md" - }, - { - "title": "Island Secure Browser", - "description": "Integrate Coder with Island's Secure Browser", - "path": "./admin/integrations/island.md" - }, - { - "title": "DX PlatformX", - "description": "Integrate Coder with DX PlatformX", - "path": "./admin/integrations/platformx.md" - }, - { - "title": "DX", - "description": "Tag Coder Users with DX", - "path": "./admin/integrations/dx-data-cloud.md" - }, - { - "title": "Hashicorp Vault", - "description": "Integrate Coder with Hashicorp Vault", - "path": "./admin/integrations/vault.md" - } - ] - }, - { - "title": "Networking", - "description": "Understand Coder's networking layer", - "path": "./admin/networking/index.md", - "icon_path": "./images/icons/networking.svg", - "children": [ - { - "title": "Port Forwarding", - "description": "Learn how to forward ports in Coder", - "path": "./admin/networking/port-forwarding.md" - }, - { - "title": "STUN and NAT", - "description": "Learn how to forward ports in Coder", - "path": "./admin/networking/stun.md" - }, - { - "title": "Workspace Proxies", - "description": "Run geo distributed workspace proxies", - "path": "./admin/networking/workspace-proxies.md", - "state": [ - "premium" - ] - }, - { - "title": "High Availability", - "description": "Learn how to configure Coder for High Availability", - "path": "./admin/networking/high-availability.md", - "state": [ - "premium" - ] - }, - { - "title": "Troubleshooting", - "description": "Troubleshoot networking issues in Coder", - "path": "./admin/networking/troubleshooting.md" - } - ] - }, - { - "title": "Monitoring", - "description": "Configure security policy and audit your deployment", - "path": "./admin/monitoring/index.md", - "icon_path": "./images/icons/speed.svg", - "children": [ - { - "title": "Logs", - "description": "Learn about Coder's logs", - "path": "./admin/monitoring/logs.md" - }, - { - "title": "Metrics", - "description": "Learn about Coder's logs", - "path": "./admin/monitoring/metrics.md" - }, - { - "title": "Health Check", - "description": "Learn about Coder's automated health checks", - "path": "./admin/monitoring/health-check.md" - }, - { - "title": "Connection Logs", - "description": "Monitor connections to workspaces", - "path": "./admin/monitoring/connection-logs.md", - "state": [ - "premium" - ] - }, - { - "title": "Notifications", - "description": "Configure notifications for your deployment", - "path": "./admin/monitoring/notifications/index.md", - "children": [ - { - "title": "Slack Notifications", - "description": "Learn how to setup Slack notifications", - "path": "./admin/monitoring/notifications/slack.md" - }, - { - "title": "Microsoft Teams Notifications", - "description": "Learn how to setup Microsoft Teams notifications", - "path": "./admin/monitoring/notifications/teams.md" - } - ] - } - ] - }, - { - "title": "Security", - "description": "Configure security policy and audit your deployment", - "path": "./admin/security/index.md", - "icon_path": "./images/icons/lock.svg", - "children": [ - { - "title": "Audit Logs", - "description": "Audit actions taken inside Coder", - "path": "./admin/security/audit-logs.md", - "state": [ - "premium" - ] - }, - { - "title": "Secrets", - "description": "Use sensitive variables in your workspaces", - "path": "./admin/security/secrets.md" - }, - { - "title": "Database Encryption", - "description": "Encrypt the database to prevent unauthorized access", - "path": "./admin/security/database-encryption.md", - "state": [ - "premium" - ] - } - ] - }, - { - "title": "Licensing", - "description": "Configure licensing for your deployment", - "path": "./admin/licensing/index.md", - "icon_path": "./images/icons/licensing.svg" - } - ] - }, - { - "title": "Run AI Coding Agents in Coder", - "description": "Learn how to run and integrate agentic AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", - "path": "./ai-coder/index.md", - "icon_path": "./images/icons/wand.svg", - "children": [ - { - "title": "Best Practices", - "description": "Best Practices running Coding Agents", - "path": "./ai-coder/best-practices.md" - }, - { - "title": "In the IDE", - "description": "Run IDE agents with Coder", - "path": "./ai-coder/ide-agents.md" - }, - { - "title": "Coder Tasks", - "description": "Run Coding Agents on your Own Infrastructure", - "path": "./ai-coder/tasks.md", - "state": [ - "beta" - ], - "children": [ - { - "title": "Custom Agents", - "description": "Run custom agents with Coder Tasks", - "path": "./ai-coder/custom-agents.md", - "state": [ - "beta" - ] - }, - { - "title": "Security \u0026 Boundaries", - "description": "Learn about security and boundaries when running AI coding agents in Coder", - "path": "./ai-coder/security.md" - } - ] - }, - { - "title": "MCP Server", - "description": "Connect to agents Coder with a MCP server", - "path": "./ai-coder/mcp-server.md", - "state": [ - "beta" - ] - } - ] - }, - { - "title": "Tutorials", - "description": "Coder knowledgebase for administrating your deployment", - "path": "./tutorials/index.md", - "icon_path": "./images/icons/generic.svg", - "children": [ - { - "title": "Quickstart", - "description": "Learn how to install and run Coder quickly", - "path": "./tutorials/quickstart.md" - }, - { - "title": "Write a Template from Scratch", - "description": "Learn how to author Coder templates", - "path": "./tutorials/template-from-scratch.md" - }, - { - "title": "Using an External Database", - "description": "Use Coder with an external database", - "path": "./tutorials/external-database.md" - }, - { - "title": "Image Management", - "description": "Learn about image management with Coder", - "path": "./admin/templates/managing-templates/image-management.md" - }, - { - "title": "Configuring Okta", - "description": "Custom claims/scopes with Okta for group/role sync", - "path": "./tutorials/configuring-okta.md" - }, - { - "title": "Google to AWS Federation", - "description": "Federating a Google Cloud service account to AWS", - "path": "./tutorials/gcp-to-aws.md" - }, - { - "title": "JFrog Artifactory Integration", - "description": "Integrate Coder with JFrog Artifactory", - "path": "./admin/integrations/jfrog-artifactory.md" - }, - { - "title": "Istio Integration", - "description": "Integrate Coder with Istio", - "path": "./admin/integrations/istio.md" - }, - { - "title": "Island Secure Browser Integration", - "description": "Integrate Coder with Island's Secure Browser", - "path": "./admin/integrations/island.md" - }, - { - "title": "Template ImagePullSecrets", - "description": "Creating ImagePullSecrets for private registries", - "path": "./tutorials/image-pull-secret.md" - }, - { - "title": "Postgres SSL", - "description": "Configure Coder to connect to Postgres over SSL", - "path": "./tutorials/postgres-ssl.md" - }, - { - "title": "Azure Federation", - "description": "Federating Coder to Azure", - "path": "./tutorials/azure-federation.md" - }, - { - "title": "Deploy Coder on Azure with an Application Gateway", - "description": "Deploy Coder on Azure with an Application Gateway", - "path": "./install/kubernetes/kubernetes-azure-app-gateway.md" - }, - { - "title": "Scanning Workspaces with JFrog Xray", - "description": "Integrate Coder with JFrog Xray", - "path": "./admin/integrations/jfrog-xray.md" - }, - { - "title": "Cloning Git Repositories", - "description": "Learn how to clone Git repositories in Coder", - "path": "./tutorials/cloning-git-repositories.md" - }, - { - "title": "Test Templates Through CI/CD", - "description": "Learn how to test and publish Coder templates in a CI/CD pipeline", - "path": "./tutorials/testing-templates.md" - }, - { - "title": "Use Apache as a Reverse Proxy", - "description": "Learn how to use Apache as a reverse proxy", - "path": "./tutorials/reverse-proxy-apache.md" - }, - { - "title": "Use Caddy as a Reverse Proxy", - "description": "Learn how to use Caddy as a reverse proxy", - "path": "./tutorials/reverse-proxy-caddy.md" - }, - { - "title": "Use NGINX as a Reverse Proxy", - "description": "Learn how to use NGINX as a reverse proxy", - "path": "./tutorials/reverse-proxy-nginx.md" - }, - { - "title": "Pre-install JetBrains IDEs in Workspaces", - "description": "Pre-install JetBrains IDEs in workspaces", - "path": "./admin/templates/extending-templates/jetbrains-preinstall.md" - }, - { - "title": "Use JetBrains IDEs in Air-Gapped Deployments", - "description": "Configure JetBrains IDEs for air-gapped deployments", - "path": "./admin/templates/extending-templates/jetbrains-airgapped.md" - }, - { - "title": "FAQs", - "description": "Miscellaneous FAQs from our community", - "path": "./tutorials/faqs.md" - }, - { - "title": "Best practices", - "description": "Guides to help you make the most of your Coder experience", - "path": "./tutorials/best-practices/index.md", - "children": [ - { - "title": "Organizations - best practices", - "description": "How to make the best use of Coder Organizations", - "path": "./tutorials/best-practices/organizations.md" - }, - { - "title": "Scale Coder", - "description": "How to prepare a Coder deployment for scale", - "path": "./tutorials/best-practices/scale-coder.md" - }, - { - "title": "Security - best practices", - "description": "Make your Coder deployment more secure", - "path": "./tutorials/best-practices/security-best-practices.md" - }, - { - "title": "Speed up your workspaces", - "description": "Speed up your Coder templates and workspaces", - "path": "./tutorials/best-practices/speed-up-templates.md" - } - ] - } - ] - }, - { - "title": "Reference", - "description": "Reference", - "path": "./reference/index.md", - "icon_path": "./images/icons/notes.svg", - "children": [ - { - "title": "REST API", - "description": "Learn how to use Coderd API", - "path": "./reference/api/index.md", - "icon_path": "./images/icons/api.svg", - "children": [ - { - "title": "General", - "path": "./reference/api/general.md" - }, - { - "title": "Agents", - "path": "./reference/api/agents.md" - }, - { - "title": "Applications", - "path": "./reference/api/applications.md" - }, - { - "title": "Audit", - "path": "./reference/api/audit.md" - }, - { - "title": "Authentication", - "path": "./reference/api/authentication.md" - }, - { - "title": "Authorization", - "path": "./reference/api/authorization.md" - }, - { - "title": "Builds", - "path": "./reference/api/builds.md" - }, - { - "title": "Debug", - "path": "./reference/api/debug.md" - }, - { - "title": "Enterprise", - "path": "./reference/api/enterprise.md" - }, - { - "title": "Files", - "path": "./reference/api/files.md" - }, - { - "title": "Git", - "path": "./reference/api/git.md" - }, - { - "title": "Insights", - "path": "./reference/api/insights.md" - }, - { - "title": "Members", - "path": "./reference/api/members.md" - }, - { - "title": "Organizations", - "path": "./reference/api/organizations.md" - }, - { - "title": "PortSharing", - "path": "./reference/api/portsharing.md" - }, - { - "title": "Schemas", - "path": "./reference/api/schemas.md" - }, - { - "title": "Templates", - "path": "./reference/api/templates.md" - }, - { - "title": "Users", - "path": "./reference/api/users.md" - }, - { - "title": "WorkspaceProxies", - "path": "./reference/api/workspaceproxies.md" - }, - { - "title": "Workspaces", - "path": "./reference/api/workspaces.md" - } - ] - }, - { - "title": "Command Line", - "description": "Learn how to use Coder CLI", - "path": "./reference/cli/index.md", - "icon_path": "./images/icons/terminal.svg", - "children": [ - { - "title": "autoupdate", - "description": "Toggle auto-update policy for a workspace", - "path": "reference/cli/autoupdate.md" - }, - { - "title": "coder", - "path": "reference/cli/index.md" - }, - { - "title": "completion", - "description": "Install or update shell completion scripts for the detected or chosen shell.", - "path": "reference/cli/completion.md" - }, - { - "title": "config-ssh", - "description": "Add an SSH Host entry for your workspaces \"ssh workspace.coder\"", - "path": "reference/cli/config-ssh.md" - }, - { - "title": "create", - "description": "Create a workspace", - "path": "reference/cli/create.md" - }, - { - "title": "delete", - "description": "Delete a workspace", - "path": "reference/cli/delete.md" - }, - { - "title": "dotfiles", - "description": "Personalize your workspace by applying a canonical dotfiles repository", - "path": "reference/cli/dotfiles.md" - }, - { - "title": "external-auth", - "description": "Manage external authentication", - "path": "reference/cli/external-auth.md" - }, - { - "title": "external-auth access-token", - "description": "Print auth for an external provider", - "path": "reference/cli/external-auth_access-token.md" - }, - { - "title": "favorite", - "description": "Add a workspace to your favorites", - "path": "reference/cli/favorite.md" - }, - { - "title": "features", - "description": "List Enterprise features", - "path": "reference/cli/features.md" - }, - { - "title": "features list", - "path": "reference/cli/features_list.md" - }, - { - "title": "groups", - "description": "Manage groups", - "path": "reference/cli/groups.md" - }, - { - "title": "groups create", - "description": "Create a user group", - "path": "reference/cli/groups_create.md" - }, - { - "title": "groups delete", - "description": "Delete a user group", - "path": "reference/cli/groups_delete.md" - }, - { - "title": "groups edit", - "description": "Edit a user group", - "path": "reference/cli/groups_edit.md" - }, - { - "title": "groups list", - "description": "List user groups", - "path": "reference/cli/groups_list.md" - }, - { - "title": "licenses", - "description": "Add, delete, and list licenses", - "path": "reference/cli/licenses.md" - }, - { - "title": "licenses add", - "description": "Add license to Coder deployment", - "path": "reference/cli/licenses_add.md" - }, - { - "title": "licenses delete", - "description": "Delete license by ID", - "path": "reference/cli/licenses_delete.md" - }, - { - "title": "licenses list", - "description": "List licenses (including expired)", - "path": "reference/cli/licenses_list.md" - }, - { - "title": "list", - "description": "List workspaces", - "path": "reference/cli/list.md" - }, - { - "title": "login", - "description": "Authenticate with Coder deployment", - "path": "reference/cli/login.md" - }, - { - "title": "logout", - "description": "Unauthenticate your local session", - "path": "reference/cli/logout.md" - }, - { - "title": "netcheck", - "description": "Print network debug information for DERP and STUN", - "path": "reference/cli/netcheck.md" - }, - { - "title": "notifications", - "description": "Manage Coder notifications", - "path": "reference/cli/notifications.md" - }, - { - "title": "notifications pause", - "description": "Pause notifications", - "path": "reference/cli/notifications_pause.md" - }, - { - "title": "notifications resume", - "description": "Resume notifications", - "path": "reference/cli/notifications_resume.md" - }, - { - "title": "notifications test", - "description": "Send a test notification", - "path": "reference/cli/notifications_test.md" - }, - { - "title": "open", - "description": "Open a workspace", - "path": "reference/cli/open.md" - }, - { - "title": "open app", - "description": "Open a workspace application.", - "path": "reference/cli/open_app.md" - }, - { - "title": "open vscode", - "description": "Open a workspace in VS Code Desktop", - "path": "reference/cli/open_vscode.md" - }, - { - "title": "organizations", - "description": "Organization related commands", - "path": "reference/cli/organizations.md" - }, - { - "title": "organizations create", - "description": "Create a new organization.", - "path": "reference/cli/organizations_create.md" - }, - { - "title": "organizations members", - "description": "Manage organization members", - "path": "reference/cli/organizations_members.md" - }, - { - "title": "organizations members add", - "description": "Add a new member to the current organization", - "path": "reference/cli/organizations_members_add.md" - }, - { - "title": "organizations members edit-roles", - "description": "Edit organization member's roles", - "path": "reference/cli/organizations_members_edit-roles.md" - }, - { - "title": "organizations members list", - "description": "List all organization members", - "path": "reference/cli/organizations_members_list.md" - }, - { - "title": "organizations members remove", - "description": "Remove a new member to the current organization", - "path": "reference/cli/organizations_members_remove.md" - }, - { - "title": "organizations roles", - "description": "Manage organization roles.", - "path": "reference/cli/organizations_roles.md" - }, - { - "title": "organizations roles create", - "description": "Create a new organization custom role", - "path": "reference/cli/organizations_roles_create.md" - }, - { - "title": "organizations roles show", - "description": "Show role(s)", - "path": "reference/cli/organizations_roles_show.md" - }, - { - "title": "organizations roles update", - "description": "Update an organization custom role", - "path": "reference/cli/organizations_roles_update.md" - }, - { - "title": "organizations settings", - "description": "Manage organization settings.", - "path": "reference/cli/organizations_settings.md" - }, - { - "title": "organizations settings set", - "description": "Update specified organization setting.", - "path": "reference/cli/organizations_settings_set.md" - }, - { - "title": "organizations settings set group-sync", - "description": "Group sync settings to sync groups from an IdP.", - "path": "reference/cli/organizations_settings_set_group-sync.md" - }, - { - "title": "organizations settings set organization-sync", - "description": "Organization sync settings to sync organization memberships from an IdP.", - "path": "reference/cli/organizations_settings_set_organization-sync.md" - }, - { - "title": "organizations settings set role-sync", - "description": "Role sync settings to sync organization roles from an IdP.", - "path": "reference/cli/organizations_settings_set_role-sync.md" - }, - { - "title": "organizations settings show", - "description": "Outputs specified organization setting.", - "path": "reference/cli/organizations_settings_show.md" - }, - { - "title": "organizations settings show group-sync", - "description": "Group sync settings to sync groups from an IdP.", - "path": "reference/cli/organizations_settings_show_group-sync.md" - }, - { - "title": "organizations settings show organization-sync", - "description": "Organization sync settings to sync organization memberships from an IdP.", - "path": "reference/cli/organizations_settings_show_organization-sync.md" - }, - { - "title": "organizations settings show role-sync", - "description": "Role sync settings to sync organization roles from an IdP.", - "path": "reference/cli/organizations_settings_show_role-sync.md" - }, - { - "title": "organizations show", - "description": "Show the organization. Using \"selected\" will show the selected organization from the \"--org\" flag. Using \"me\" will show all organizations you are a member of.", - "path": "reference/cli/organizations_show.md" - }, - { - "title": "ping", - "description": "Ping a workspace", - "path": "reference/cli/ping.md" - }, - { - "title": "port-forward", - "description": "Forward ports from a workspace to the local machine. For reverse port forwarding, use \"coder ssh -R\".", - "path": "reference/cli/port-forward.md" - }, - { - "title": "prebuilds", - "description": "Manage Coder prebuilds", - "path": "reference/cli/prebuilds.md" - }, - { - "title": "prebuilds pause", - "description": "Pause prebuilds", - "path": "reference/cli/prebuilds_pause.md" - }, - { - "title": "prebuilds resume", - "description": "Resume prebuilds", - "path": "reference/cli/prebuilds_resume.md" - }, - { - "title": "provisioner", - "description": "View and manage provisioner daemons and jobs", - "path": "reference/cli/provisioner.md" - }, - { - "title": "provisioner jobs", - "description": "View and manage provisioner jobs", - "path": "reference/cli/provisioner_jobs.md" - }, - { - "title": "provisioner jobs cancel", - "description": "Cancel a provisioner job", - "path": "reference/cli/provisioner_jobs_cancel.md" - }, - { - "title": "provisioner jobs list", - "description": "List provisioner jobs", - "path": "reference/cli/provisioner_jobs_list.md" - }, - { - "title": "provisioner keys", - "description": "Manage provisioner keys", - "path": "reference/cli/provisioner_keys.md" - }, - { - "title": "provisioner keys create", - "description": "Create a new provisioner key", - "path": "reference/cli/provisioner_keys_create.md" - }, - { - "title": "provisioner keys delete", - "description": "Delete a provisioner key", - "path": "reference/cli/provisioner_keys_delete.md" - }, - { - "title": "provisioner keys list", - "description": "List provisioner keys in an organization", - "path": "reference/cli/provisioner_keys_list.md" - }, - { - "title": "provisioner list", - "description": "List provisioner daemons in an organization", - "path": "reference/cli/provisioner_list.md" - }, - { - "title": "provisioner start", - "description": "Run a provisioner daemon", - "path": "reference/cli/provisioner_start.md" - }, - { - "title": "publickey", - "description": "Output your Coder public key used for Git operations", - "path": "reference/cli/publickey.md" - }, - { - "title": "rename", - "description": "Rename a workspace", - "path": "reference/cli/rename.md" - }, - { - "title": "reset-password", - "description": "Directly connect to the database to reset a user's password", - "path": "reference/cli/reset-password.md" - }, - { - "title": "restart", - "description": "Restart a workspace", - "path": "reference/cli/restart.md" - }, - { - "title": "schedule", - "description": "Schedule automated start and stop times for workspaces", - "path": "reference/cli/schedule.md" - }, - { - "title": "schedule extend", - "description": "Extend the stop time of a currently running workspace instance.", - "path": "reference/cli/schedule_extend.md" - }, - { - "title": "schedule show", - "description": "Show workspace schedules", - "path": "reference/cli/schedule_show.md" - }, - { - "title": "schedule start", - "description": "Edit workspace start schedule", - "path": "reference/cli/schedule_start.md" - }, - { - "title": "schedule stop", - "description": "Edit workspace stop schedule", - "path": "reference/cli/schedule_stop.md" - }, - { - "title": "server", - "description": "Start a Coder server", - "path": "reference/cli/server.md" - }, - { - "title": "server create-admin-user", - "description": "Create a new admin user with the given username, email and password and adds it to every organization.", - "path": "reference/cli/server_create-admin-user.md" - }, - { - "title": "server dbcrypt", - "description": "Manage database encryption.", - "path": "reference/cli/server_dbcrypt.md" - }, - { - "title": "server dbcrypt decrypt", - "description": "Decrypt a previously encrypted database.", - "path": "reference/cli/server_dbcrypt_decrypt.md" - }, - { - "title": "server dbcrypt delete", - "description": "Delete all encrypted data from the database. THIS IS A DESTRUCTIVE OPERATION.", - "path": "reference/cli/server_dbcrypt_delete.md" - }, - { - "title": "server dbcrypt rotate", - "description": "Rotate database encryption keys.", - "path": "reference/cli/server_dbcrypt_rotate.md" - }, - { - "title": "server postgres-builtin-serve", - "description": "Run the built-in PostgreSQL deployment.", - "path": "reference/cli/server_postgres-builtin-serve.md" - }, - { - "title": "server postgres-builtin-url", - "description": "Output the connection URL for the built-in PostgreSQL deployment.", - "path": "reference/cli/server_postgres-builtin-url.md" - }, - { - "title": "show", - "description": "Display details of a workspace's resources and agents", - "path": "reference/cli/show.md" - }, - { - "title": "speedtest", - "description": "Run upload and download tests from your machine to a workspace", - "path": "reference/cli/speedtest.md" - }, - { - "title": "ssh", - "description": "Start a shell into a workspace or run a command", - "path": "reference/cli/ssh.md" - }, - { - "title": "start", - "description": "Start a workspace", - "path": "reference/cli/start.md" - }, - { - "title": "stat", - "description": "Show resource usage for the current workspace.", - "path": "reference/cli/stat.md" - }, - { - "title": "stat cpu", - "description": "Show CPU usage, in cores.", - "path": "reference/cli/stat_cpu.md" - }, - { - "title": "stat disk", - "description": "Show disk usage, in gigabytes.", - "path": "reference/cli/stat_disk.md" - }, - { - "title": "stat mem", - "description": "Show memory usage, in gigabytes.", - "path": "reference/cli/stat_mem.md" - }, - { - "title": "state", - "description": "Manually manage Terraform state to fix broken workspaces", - "path": "reference/cli/state.md" - }, - { - "title": "state pull", - "description": "Pull a Terraform state file from a workspace.", - "path": "reference/cli/state_pull.md" - }, - { - "title": "state push", - "description": "Push a Terraform state file to a workspace.", - "path": "reference/cli/state_push.md" - }, - { - "title": "stop", - "description": "Stop a workspace", - "path": "reference/cli/stop.md" - }, - { - "title": "support", - "description": "Commands for troubleshooting issues with a Coder deployment.", - "path": "reference/cli/support.md" - }, - { - "title": "support bundle", - "description": "Generate a support bundle to troubleshoot issues connecting to a workspace.", - "path": "reference/cli/support_bundle.md" - }, - { - "title": "templates", - "description": "Manage templates", - "path": "reference/cli/templates.md" - }, - { - "title": "templates archive", - "description": "Archive unused or failed template versions from a given template(s)", - "path": "reference/cli/templates_archive.md" - }, - { - "title": "templates create", - "description": "DEPRECATED: Create a template from the current directory or as specified by flag", - "path": "reference/cli/templates_create.md" - }, - { - "title": "templates delete", - "description": "Delete templates", - "path": "reference/cli/templates_delete.md" - }, - { - "title": "templates edit", - "description": "Edit the metadata of a template by name.", - "path": "reference/cli/templates_edit.md" - }, - { - "title": "templates init", - "description": "Get started with a templated template.", - "path": "reference/cli/templates_init.md" - }, - { - "title": "templates list", - "description": "List all the templates available for the organization", - "path": "reference/cli/templates_list.md" - }, - { - "title": "templates presets", - "description": "Manage presets of the specified template", - "path": "reference/cli/templates_presets.md" - }, - { - "title": "templates presets list", - "description": "List all presets of the specified template. Defaults to the active template version.", - "path": "reference/cli/templates_presets_list.md" - }, - { - "title": "templates pull", - "description": "Download the active, latest, or specified version of a template to a path.", - "path": "reference/cli/templates_pull.md" - }, - { - "title": "templates push", - "description": "Create or update a template from the current directory or as specified by flag", - "path": "reference/cli/templates_push.md" - }, - { - "title": "templates versions", - "description": "Manage different versions of the specified template", - "path": "reference/cli/templates_versions.md" - }, - { - "title": "templates versions archive", - "description": "Archive a template version(s).", - "path": "reference/cli/templates_versions_archive.md" - }, - { - "title": "templates versions list", - "description": "List all the versions of the specified template", - "path": "reference/cli/templates_versions_list.md" - }, - { - "title": "templates versions promote", - "description": "Promote a template version to active.", - "path": "reference/cli/templates_versions_promote.md" - }, - { - "title": "templates versions unarchive", - "description": "Unarchive a template version(s).", - "path": "reference/cli/templates_versions_unarchive.md" - }, - { - "title": "tokens", - "description": "Manage personal access tokens", - "path": "reference/cli/tokens.md" - }, - { - "title": "tokens create", - "description": "Create a token", - "path": "reference/cli/tokens_create.md" - }, - { - "title": "tokens list", - "description": "List tokens", - "path": "reference/cli/tokens_list.md" - }, - { - "title": "tokens remove", - "description": "Delete a token", - "path": "reference/cli/tokens_remove.md" - }, - { - "title": "unfavorite", - "description": "Remove a workspace from your favorites", - "path": "reference/cli/unfavorite.md" - }, - { - "title": "update", - "description": "Will update and start a given workspace if it is out of date. If the workspace is already running, it will be stopped first.", - "path": "reference/cli/update.md" - }, - { - "title": "users", - "description": "Manage users", - "path": "reference/cli/users.md" - }, - { - "title": "users activate", - "description": "Update a user's status to 'active'. Active users can fully interact with the platform", - "path": "reference/cli/users_activate.md" - }, - { - "title": "users create", - "description": "Create a new user.", - "path": "reference/cli/users_create.md" - }, - { - "title": "users delete", - "description": "Delete a user by username or user_id.", - "path": "reference/cli/users_delete.md" - }, - { - "title": "users edit-roles", - "description": "Edit a user's roles by username or id", - "path": "reference/cli/users_edit-roles.md" - }, - { - "title": "users list", - "description": "Prints the list of users.", - "path": "reference/cli/users_list.md" - }, - { - "title": "users show", - "description": "Show a single user. Use 'me' to indicate the currently authenticated user.", - "path": "reference/cli/users_show.md" - }, - { - "title": "users suspend", - "description": "Update a user's status to 'suspended'. A suspended user cannot log into the platform", - "path": "reference/cli/users_suspend.md" - }, - { - "title": "version", - "description": "Show coder version", - "path": "reference/cli/version.md" - }, - { - "title": "whoami", - "description": "Fetch authenticated user info for Coder deployment", - "path": "reference/cli/whoami.md" - } - ] - }, - { - "title": "Agent API", - "description": "Learn how to use Coder Agent API", - "path": "./reference/agent-api/index.md", - "icon_path": "./images/icons/api.svg", - "children": [ - { - "title": "Debug", - "path": "./reference/agent-api/debug.md" - }, - { - "title": "Schemas", - "path": "./reference/agent-api/schemas.md" - } - ] - } - ] - } - ] -} \ No newline at end of file + "versions": ["main"], + "routes": [ + { + "title": "About", + "description": "Coder docs", + "path": "./README.md", + "icon_path": "./images/icons/home.svg", + "children": [ + { + "title": "Screenshots", + "description": "View screenshots of the Coder platform", + "path": "./about/screenshots.md" + }, + { + "title": "Quickstart", + "description": "Learn how to install and run Coder quickly", + "path": "./tutorials/quickstart.md" + }, + { + "title": "Support", + "description": "How Coder supports your deployment and you", + "path": "./support/index.md", + "children": [ + { + "title": "Generate a Support Bundle", + "description": "Generate and upload a Support Bundle to Coder Support", + "path": "./support/support-bundle.md" + } + ] + }, + { + "title": "Contributing", + "description": "Learn how to contribute to Coder", + "path": "./about/contributing/CONTRIBUTING.md", + "icon_path": "./images/icons/contributing.svg", + "children": [ + { + "title": "Code of Conduct", + "description": "See the code of conduct for contributing to Coder", + "path": "./about/contributing/CODE_OF_CONDUCT.md", + "icon_path": "./images/icons/circle-dot.svg" + }, + { + "title": "Documentation", + "description": "Our style guide for use when authoring documentation", + "path": "./about/contributing/documentation.md", + "icon_path": "./images/icons/document.svg" + }, + { + "title": "Modules", + "description": "Learn how to contribute modules to Coder", + "path": "./about/contributing/modules.md", + "icon_path": "./images/icons/gear.svg" + }, + { + "title": "Templates", + "description": "Learn how to contribute templates to Coder", + "path": "./about/contributing/templates.md", + "icon_path": "./images/icons/picture.svg" + }, + { + "title": "Backend", + "description": "Our guide for backend development", + "path": "./about/contributing/backend.md", + "icon_path": "./images/icons/gear.svg" + }, + { + "title": "Frontend", + "description": "Our guide for frontend development", + "path": "./about/contributing/frontend.md", + "icon_path": "./images/icons/frontend.svg" + }, + { + "title": "Security", + "description": "Security vulnerability disclosure policy", + "path": "./about/contributing/SECURITY.md", + "icon_path": "./images/icons/lock.svg" + } + ] + } + ] + }, + { + "title": "Install", + "description": "Installing Coder", + "path": "./install/index.md", + "icon_path": "./images/icons/download.svg", + "children": [ + { + "title": "Coder CLI", + "description": "Install the standalone binary", + "path": "./install/cli.md", + "icon_path": "./images/icons/terminal.svg" + }, + { + "title": "Docker", + "description": "Install Coder using Docker", + "path": "./install/docker.md", + "icon_path": "./images/icons/docker.svg" + }, + { + "title": "Kubernetes", + "description": "Install Coder on Kubernetes", + "path": "./install/kubernetes.md", + "icon_path": "./images/icons/kubernetes.svg", + "children": [ + { + "title": "Deploy Coder on Azure with an Application Gateway", + "description": "Deploy Coder on Azure with an Application Gateway", + "path": "./install/kubernetes/kubernetes-azure-app-gateway.md" + } + ] + }, + { + "title": "Rancher", + "description": "Deploy Coder on Rancher", + "path": "./install/rancher.md", + "icon_path": "./images/icons/rancher.svg" + }, + { + "title": "OpenShift", + "description": "Install Coder on OpenShift", + "path": "./install/openshift.md", + "icon_path": "./images/icons/openshift.svg" + }, + { + "title": "Cloud Providers", + "description": "Install Coder on cloud providers", + "path": "./install/cloud/index.md", + "icon_path": "./images/icons/cloud.svg", + "children": [ + { + "title": "AWS EC2", + "description": "Install Coder on AWS EC2", + "path": "./install/cloud/ec2.md" + }, + { + "title": "GCP Compute Engine", + "description": "Install Coder on GCP Compute Engine", + "path": "./install/cloud/compute-engine.md" + }, + { + "title": "Azure VM", + "description": "Install Coder on an Azure VM", + "path": "./install/cloud/azure-vm.md" + } + ] + }, + { + "title": "Offline Deployments", + "description": "Run Coder in offline / air-gapped environments", + "path": "./install/offline.md", + "icon_path": "./images/icons/lan.svg" + }, + { + "title": "Unofficial Install Methods", + "description": "Other installation methods", + "path": "./install/other/index.md", + "icon_path": "./images/icons/generic.svg" + }, + { + "title": "Upgrading", + "description": "Learn how to upgrade Coder", + "path": "./install/upgrade.md", + "icon_path": "./images/icons/upgrade.svg" + }, + { + "title": "Uninstall", + "description": "Learn how to uninstall Coder", + "path": "./install/uninstall.md", + "icon_path": "./images/icons/trash.svg" + }, + { + "title": "Releases", + "description": "Learn about the Coder release channels and schedule", + "path": "./install/releases/index.md", + "icon_path": "./images/icons/star.svg", + "children": [ + { + "title": "Feature stages", + "description": "Information about pre-GA stages.", + "path": "./install/releases/feature-stages.md" + } + ] + } + ] + }, + { + "title": "User Guides", + "description": "Guides for end-users of Coder", + "path": "./user-guides/index.md", + "icon_path": "./images/icons/users.svg", + "children": [ + { + "title": "Access Workspaces", + "description": "Connect to your Coder workspaces", + "path": "./user-guides/workspace-access/index.md", + "icon_path": "./images/icons/access.svg", + "children": [ + { + "title": "Visual Studio Code", + "description": "Use VSCode with Coder in the desktop or browser", + "path": "./user-guides/workspace-access/vscode.md" + }, + { + "title": "JetBrains IDEs", + "description": "Use JetBrains IDEs with Coder", + "path": "./user-guides/workspace-access/jetbrains/index.md", + "children": [ + { + "title": "JetBrains Fleet", + "description": "Connect JetBrains Fleet to a Coder workspace", + "path": "./user-guides/workspace-access/jetbrains/fleet.md" + }, + { + "title": "JetBrains Gateway", + "description": "Use JetBrains Gateway to connect to Coder workspaces", + "path": "./user-guides/workspace-access/jetbrains/gateway.md" + }, + { + "title": "JetBrains Toolbox", + "description": "Access Coder workspaces from JetBrains Toolbox", + "path": "./user-guides/workspace-access/jetbrains/toolbox.md", + "state": ["beta"] + } + ] + }, + { + "title": "Remote Desktop", + "description": "Use RDP in Coder", + "path": "./user-guides/workspace-access/remote-desktops.md" + }, + { + "title": "Emacs TRAMP", + "description": "Use Emacs TRAMP in Coder", + "path": "./user-guides/workspace-access/emacs-tramp.md" + }, + { + "title": "Port Forwarding", + "description": "Access ports on your workspace", + "path": "./user-guides/workspace-access/port-forwarding.md" + }, + { + "title": "Filebrowser", + "description": "Access your workspace files", + "path": "./user-guides/workspace-access/filebrowser.md" + }, + { + "title": "Web IDEs and Coder Apps", + "description": "Access your workspace with IDEs in the browser", + "path": "./user-guides/workspace-access/web-ides.md" + }, + { + "title": "Zed", + "description": "Access your workspace with Zed", + "path": "./user-guides/workspace-access/zed.md" + }, + { + "title": "Cursor", + "description": "Access your workspace with Cursor", + "path": "./user-guides/workspace-access/cursor.md" + }, + { + "title": "Windsurf", + "description": "Access your workspace with Windsurf", + "path": "./user-guides/workspace-access/windsurf.md" + } + ] + }, + { + "title": "Coder Desktop", + "description": "Transform remote workspaces into seamless local development environments with no port forwarding required", + "path": "./user-guides/desktop/index.md", + "icon_path": "./images/icons/computer-code.svg", + "children": [ + { + "title": "Coder Desktop connect and sync", + "description": "Use Coder Desktop to manage your workspace code and files locally", + "path": "./user-guides/desktop/desktop-connect-sync.md" + } + ] + }, + { + "title": "Workspace Management", + "description": "Manage workspaces", + "path": "./user-guides/workspace-management.md", + "icon_path": "./images/icons/generic.svg" + }, + { + "title": "Workspace Scheduling", + "description": "Cost control with workspace schedules", + "path": "./user-guides/workspace-scheduling.md", + "icon_path": "./images/icons/stopwatch.svg" + }, + { + "title": "Workspace Lifecycle", + "description": "A guide to the workspace lifecycle, from creation and status through stopping and deletion.", + "path": "./user-guides/workspace-lifecycle.md", + "icon_path": "./images/icons/circle-dot.svg" + }, + { + "title": "Dev Containers Integration", + "description": "Run containerized development environments in your Coder workspace using the dev containers specification.", + "path": "./user-guides/devcontainers/index.md", + "icon_path": "./images/icons/container.svg", + "children": [ + { + "title": "Working with dev containers", + "description": "Access dev containers via SSH, your IDE, or web terminal.", + "path": "./user-guides/devcontainers/working-with-dev-containers.md" + }, + { + "title": "Troubleshooting dev containers", + "description": "Diagnose and resolve common issues with dev containers in your Coder workspace.", + "path": "./user-guides/devcontainers/troubleshooting-dev-containers.md" + } + ] + }, + { + "title": "Dotfiles", + "description": "Personalize your environment with dotfiles", + "path": "./user-guides/workspace-dotfiles.md", + "icon_path": "./images/icons/art-pad.svg" + } + ] + }, + { + "title": "Administration", + "description": "Guides for template and deployment administrators", + "path": "./admin/index.md", + "icon_path": "./images/icons/wrench.svg", + "children": [ + { + "title": "Setup", + "description": "Configure user access to your control plane.", + "path": "./admin/setup/index.md", + "icon_path": "./images/icons/toggle_on.svg", + "children": [ + { + "title": "Appearance", + "description": "Learn how to configure the appearance of Coder", + "path": "./admin/setup/appearance.md", + "state": ["premium"] + }, + { + "title": "Telemetry", + "description": "Learn what usage telemetry Coder collects", + "path": "./admin/setup/telemetry.md" + } + ] + }, + { + "title": "Infrastructure", + "description": "How to integrate Coder with your organization's compute", + "path": "./admin/infrastructure/index.md", + "icon_path": "./images/icons/container.svg", + "children": [ + { + "title": "Architecture", + "description": "Learn about Coder's architecture", + "path": "./admin/infrastructure/architecture.md" + }, + { + "title": "Validated Architectures", + "description": "Architectures for large Coder deployments", + "path": "./admin/infrastructure/validated-architectures/index.md", + "children": [ + { + "title": "Up to 1,000 Users", + "description": "Hardware specifications and architecture guidance for Coder deployments that support up to 1,000 users", + "path": "./admin/infrastructure/validated-architectures/1k-users.md" + }, + { + "title": "Up to 2,000 Users", + "description": "Hardware specifications and architecture guidance for Coder deployments that support up to 2,000 users", + "path": "./admin/infrastructure/validated-architectures/2k-users.md" + }, + { + "title": "Up to 3,000 Users", + "description": "Enterprise-scale architecture recommendations for Coder deployments that support up to 3,000 users", + "path": "./admin/infrastructure/validated-architectures/3k-users.md" + } + ] + }, + { + "title": "Scale Testing", + "description": "Ensure your deployment can handle your organization's needs", + "path": "./admin/infrastructure/scale-testing.md" + }, + { + "title": "Scaling Utilities", + "description": "Tools to help you scale your deployment", + "path": "./admin/infrastructure/scale-utility.md" + }, + { + "title": "Scaling best practices", + "description": "How to prepare a Coder deployment for scale", + "path": "./tutorials/best-practices/scale-coder.md" + } + ] + }, + { + "title": "Users", + "description": "Learn how to manage and audit users", + "path": "./admin/users/index.md", + "icon_path": "./images/icons/users.svg", + "children": [ + { + "title": "OIDC Authentication", + "description": "Configure OpenID Connect authentication with identity providers like Okta or Active Directory", + "path": "./admin/users/oidc-auth/index.md", + "children": [ + { + "title": "Configure OIDC refresh tokens", + "description": "How to configure OIDC refresh tokens", + "path": "./admin/users/oidc-auth/refresh-tokens.md" + } + ] + }, + { + "title": "GitHub Authentication", + "description": "Set up authentication through GitHub OAuth to enable secure user login and sign-up", + "path": "./admin/users/github-auth.md" + }, + { + "title": "Password Authentication", + "description": "Manage username/password authentication settings and user password reset workflows", + "path": "./admin/users/password-auth.md" + }, + { + "title": "Headless Authentication", + "description": "Create and manage headless service accounts for automated systems and API integrations", + "path": "./admin/users/headless-auth.md" + }, + { + "title": "Groups \u0026 Roles", + "description": "Manage access control with user groups and role-based permissions for Coder resources", + "path": "./admin/users/groups-roles.md", + "state": ["premium"] + }, + { + "title": "IdP Sync", + "description": "Synchronize user groups, roles, and organizations from your identity provider to Coder", + "path": "./admin/users/idp-sync.md", + "state": ["premium"] + }, + { + "title": "Organizations", + "description": "Segment and isolate resources by creating separate organizations for different teams or projects", + "path": "./admin/users/organizations.md", + "state": ["premium"] + }, + { + "title": "Quotas", + "description": "Control resource usage by implementing workspace budgets and credit-based cost management", + "path": "./admin/users/quotas.md", + "state": ["premium"] + }, + { + "title": "Sessions \u0026 API Tokens", + "description": "Manage authentication tokens for API access and configure session duration policies", + "path": "./admin/users/sessions-tokens.md" + } + ] + }, + { + "title": "Templates", + "description": "Learn how to author and maintain Coder templates", + "path": "./admin/templates/index.md", + "icon_path": "./images/icons/picture.svg", + "children": [ + { + "title": "Creating Templates", + "description": "Learn how to create templates with Terraform", + "path": "./admin/templates/creating-templates.md" + }, + { + "title": "Managing Templates", + "description": "Learn how to manage templates and best practices", + "path": "./admin/templates/managing-templates/index.md", + "children": [ + { + "title": "Image Management", + "description": "Learn about template image management", + "path": "./admin/templates/managing-templates/image-management.md" + }, + { + "title": "Change Management", + "description": "Learn about template change management and versioning", + "path": "./admin/templates/managing-templates/change-management.md" + }, + { + "title": "Dev containers", + "description": "Learn about using development containers in templates", + "path": "./admin/templates/managing-templates/devcontainers/index.md", + "children": [ + { + "title": "Add a dev container template", + "description": "How to add a dev container template to Coder", + "path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md" + }, + { + "title": "Dev container security and caching", + "description": "Configure dev container authentication and caching", + "path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md" + }, + { + "title": "Dev container releases and known issues", + "description": "Dev container releases and known issues", + "path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md" + } + ] + }, + { + "title": "Template Dependencies", + "description": "Learn how to manage template dependencies", + "path": "./admin/templates/managing-templates/dependencies.md" + }, + { + "title": "Workspace Scheduling", + "description": "Learn how to control how workspaces are started and stopped", + "path": "./admin/templates/managing-templates/schedule.md" + } + ] + }, + { + "title": "Extending Templates", + "description": "Learn best practices in extending templates", + "path": "./admin/templates/extending-templates/index.md", + "children": [ + { + "title": "Agent Metadata", + "description": "Retrieve real-time stats from the workspace agent", + "path": "./admin/templates/extending-templates/agent-metadata.md" + }, + { + "title": "Build Parameters", + "description": "Use parameters to customize workspaces at build", + "path": "./admin/templates/extending-templates/parameters.md" + }, + { + "title": "Dynamic Parameters", + "description": "Conditional, identity-aware parameter syntax for advanced users.", + "path": "./admin/templates/extending-templates/dynamic-parameters.md", + "state": ["beta"] + }, + { + "title": "Prebuilt workspaces", + "description": "Pre-provision a ready-to-deploy workspace with a defined set of parameters", + "path": "./admin/templates/extending-templates/prebuilt-workspaces.md", + "state": ["premium"] + }, + { + "title": "Icons", + "description": "Customize your template with built-in icons", + "path": "./admin/templates/extending-templates/icons.md" + }, + { + "title": "Resource Metadata", + "description": "Display resource state in the workspace dashboard", + "path": "./admin/templates/extending-templates/resource-metadata.md" + }, + { + "title": "Resource Monitoring", + "description": "Monitor resources in the workspace dashboard", + "path": "./admin/templates/extending-templates/resource-monitoring.md" + }, + { + "title": "Resource Ordering", + "description": "Design the UI of workspaces", + "path": "./admin/templates/extending-templates/resource-ordering.md" + }, + { + "title": "Resource Persistence", + "description": "Control resource persistence", + "path": "./admin/templates/extending-templates/resource-persistence.md" + }, + { + "title": "Terraform Variables", + "description": "Use variables to manage template state", + "path": "./admin/templates/extending-templates/variables.md" + }, + { + "title": "Terraform Modules", + "description": "Reuse terraform code across templates", + "path": "./admin/templates/extending-templates/modules.md" + }, + { + "title": "Web IDEs and Coder Apps", + "description": "Add and configure Web IDEs in your templates as coder apps", + "path": "./admin/templates/extending-templates/web-ides.md" + }, + { + "title": "Pre-install JetBrains IDEs", + "description": "Pre-install JetBrains IDEs in a template for faster IDE startup", + "path": "./admin/templates/extending-templates/jetbrains-preinstall.md" + }, + { + "title": "JetBrains IDEs in Air-Gapped Deployments", + "description": "Configure JetBrains IDEs for air-gapped deployments", + "path": "./admin/templates/extending-templates/jetbrains-airgapped.md" + }, + { + "title": "Docker in Workspaces", + "description": "Use Docker in your workspaces", + "path": "./admin/templates/extending-templates/docker-in-workspaces.md" + }, + { + "title": "Workspace Tags", + "description": "Control provisioning using Workspace Tags and Parameters", + "path": "./admin/templates/extending-templates/workspace-tags.md" + }, + { + "title": "Provider Authentication", + "description": "Authenticate with provider APIs to provision workspaces", + "path": "./admin/templates/extending-templates/provider-authentication.md" + }, + { + "title": "Configure a template for dev containers", + "description": "How to use configure your template for dev containers", + "path": "./admin/templates/extending-templates/devcontainers.md" + }, + { + "title": "Process Logging", + "description": "Log workspace processes", + "path": "./admin/templates/extending-templates/process-logging.md", + "state": ["premium"] + } + ] + }, + { + "title": "Open in Coder", + "description": "Open workspaces in Coder", + "path": "./admin/templates/open-in-coder.md" + }, + { + "title": "Permissions \u0026 Policies", + "description": "Learn how to create templates with Terraform", + "path": "./admin/templates/template-permissions.md", + "state": ["premium"] + }, + { + "title": "Troubleshooting Templates", + "description": "Learn how to troubleshoot template issues", + "path": "./admin/templates/troubleshooting.md" + } + ] + }, + { + "title": "External Provisioners", + "description": "Learn how to run external provisioners with Coder", + "path": "./admin/provisioners/index.md", + "icon_path": "./images/icons/key.svg", + "state": ["premium"], + "children": [ + { + "title": "Manage Provisioner Jobs", + "description": "Learn how to run external provisioners with Coder", + "path": "./admin/provisioners/manage-provisioner-jobs.md", + "state": ["premium"] + } + ] + }, + { + "title": "External Authentication", + "description": "Learn how to configure external authentication", + "path": "./admin/external-auth/index.md", + "icon_path": "./images/icons/plug.svg" + }, + { + "title": "Integrations", + "description": "Use integrations to extend Coder", + "path": "./admin/integrations/index.md", + "icon_path": "./images/icons/puzzle.svg", + "children": [ + { + "title": "Prometheus", + "description": "Collect deployment metrics with Prometheus", + "path": "./admin/integrations/prometheus.md" + }, + { + "title": "Kubernetes Logging", + "description": "Stream K8s event logs on workspace startup", + "path": "./admin/integrations/kubernetes-logs.md" + }, + { + "title": "Additional Kubernetes Clusters", + "description": "Deploy workspaces on additional Kubernetes clusters", + "path": "./admin/integrations/multiple-kube-clusters.md" + }, + { + "title": "JFrog Artifactory", + "description": "Integrate Coder with JFrog Artifactory", + "path": "./admin/integrations/jfrog-artifactory.md" + }, + { + "title": "JFrog Xray", + "description": "Integrate Coder with JFrog Xray", + "path": "./admin/integrations/jfrog-xray.md" + }, + { + "title": "Island Secure Browser", + "description": "Integrate Coder with Island's Secure Browser", + "path": "./admin/integrations/island.md" + }, + { + "title": "DX PlatformX", + "description": "Integrate Coder with DX PlatformX", + "path": "./admin/integrations/platformx.md" + }, + { + "title": "DX", + "description": "Tag Coder Users with DX", + "path": "./admin/integrations/dx-data-cloud.md" + }, + { + "title": "Hashicorp Vault", + "description": "Integrate Coder with Hashicorp Vault", + "path": "./admin/integrations/vault.md" + } + ] + }, + { + "title": "Networking", + "description": "Understand Coder's networking layer", + "path": "./admin/networking/index.md", + "icon_path": "./images/icons/networking.svg", + "children": [ + { + "title": "Port Forwarding", + "description": "Learn how to forward ports in Coder", + "path": "./admin/networking/port-forwarding.md" + }, + { + "title": "STUN and NAT", + "description": "Learn how to forward ports in Coder", + "path": "./admin/networking/stun.md" + }, + { + "title": "Workspace Proxies", + "description": "Run geo distributed workspace proxies", + "path": "./admin/networking/workspace-proxies.md", + "state": ["premium"] + }, + { + "title": "High Availability", + "description": "Learn how to configure Coder for High Availability", + "path": "./admin/networking/high-availability.md", + "state": ["premium"] + }, + { + "title": "Troubleshooting", + "description": "Troubleshoot networking issues in Coder", + "path": "./admin/networking/troubleshooting.md" + } + ] + }, + { + "title": "Monitoring", + "description": "Configure security policy and audit your deployment", + "path": "./admin/monitoring/index.md", + "icon_path": "./images/icons/speed.svg", + "children": [ + { + "title": "Logs", + "description": "Learn about Coder's logs", + "path": "./admin/monitoring/logs.md" + }, + { + "title": "Metrics", + "description": "Learn about Coder's logs", + "path": "./admin/monitoring/metrics.md" + }, + { + "title": "Health Check", + "description": "Learn about Coder's automated health checks", + "path": "./admin/monitoring/health-check.md" + }, + { + "title": "Connection Logs", + "description": "Monitor connections to workspaces", + "path": "./admin/monitoring/connection-logs.md", + "state": ["premium"] + }, + { + "title": "Notifications", + "description": "Configure notifications for your deployment", + "path": "./admin/monitoring/notifications/index.md", + "children": [ + { + "title": "Slack Notifications", + "description": "Learn how to setup Slack notifications", + "path": "./admin/monitoring/notifications/slack.md" + }, + { + "title": "Microsoft Teams Notifications", + "description": "Learn how to setup Microsoft Teams notifications", + "path": "./admin/monitoring/notifications/teams.md" + } + ] + } + ] + }, + { + "title": "Security", + "description": "Configure security policy and audit your deployment", + "path": "./admin/security/index.md", + "icon_path": "./images/icons/lock.svg", + "children": [ + { + "title": "Audit Logs", + "description": "Audit actions taken inside Coder", + "path": "./admin/security/audit-logs.md", + "state": ["premium"] + }, + { + "title": "Secrets", + "description": "Use sensitive variables in your workspaces", + "path": "./admin/security/secrets.md" + }, + { + "title": "Database Encryption", + "description": "Encrypt the database to prevent unauthorized access", + "path": "./admin/security/database-encryption.md", + "state": ["premium"] + } + ] + }, + { + "title": "Licensing", + "description": "Configure licensing for your deployment", + "path": "./admin/licensing/index.md", + "icon_path": "./images/icons/licensing.svg" + } + ] + }, + { + "title": "Run AI Coding Agents in Coder", + "description": "Learn how to run and integrate agentic AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", + "path": "./ai-coder/index.md", + "icon_path": "./images/icons/wand.svg", + "children": [ + { + "title": "Best Practices", + "description": "Best Practices running Coding Agents", + "path": "./ai-coder/best-practices.md" + }, + { + "title": "In the IDE", + "description": "Run IDE agents with Coder", + "path": "./ai-coder/ide-agents.md" + }, + { + "title": "Coder Tasks", + "description": "Run Coding Agents on your Own Infrastructure", + "path": "./ai-coder/tasks.md", + "state": ["beta"], + "children": [ + { + "title": "Custom Agents", + "description": "Run custom agents with Coder Tasks", + "path": "./ai-coder/custom-agents.md", + "state": ["beta"] + }, + { + "title": "Security \u0026 Boundaries", + "description": "Learn about security and boundaries when running AI coding agents in Coder", + "path": "./ai-coder/security.md" + } + ] + }, + { + "title": "MCP Server", + "description": "Connect to agents Coder with a MCP server", + "path": "./ai-coder/mcp-server.md", + "state": ["beta"] + } + ] + }, + { + "title": "Tutorials", + "description": "Coder knowledgebase for administrating your deployment", + "path": "./tutorials/index.md", + "icon_path": "./images/icons/generic.svg", + "children": [ + { + "title": "Quickstart", + "description": "Learn how to install and run Coder quickly", + "path": "./tutorials/quickstart.md" + }, + { + "title": "Write a Template from Scratch", + "description": "Learn how to author Coder templates", + "path": "./tutorials/template-from-scratch.md" + }, + { + "title": "Using an External Database", + "description": "Use Coder with an external database", + "path": "./tutorials/external-database.md" + }, + { + "title": "Image Management", + "description": "Learn about image management with Coder", + "path": "./admin/templates/managing-templates/image-management.md" + }, + { + "title": "Configuring Okta", + "description": "Custom claims/scopes with Okta for group/role sync", + "path": "./tutorials/configuring-okta.md" + }, + { + "title": "Google to AWS Federation", + "description": "Federating a Google Cloud service account to AWS", + "path": "./tutorials/gcp-to-aws.md" + }, + { + "title": "JFrog Artifactory Integration", + "description": "Integrate Coder with JFrog Artifactory", + "path": "./admin/integrations/jfrog-artifactory.md" + }, + { + "title": "Istio Integration", + "description": "Integrate Coder with Istio", + "path": "./admin/integrations/istio.md" + }, + { + "title": "Island Secure Browser Integration", + "description": "Integrate Coder with Island's Secure Browser", + "path": "./admin/integrations/island.md" + }, + { + "title": "Template ImagePullSecrets", + "description": "Creating ImagePullSecrets for private registries", + "path": "./tutorials/image-pull-secret.md" + }, + { + "title": "Postgres SSL", + "description": "Configure Coder to connect to Postgres over SSL", + "path": "./tutorials/postgres-ssl.md" + }, + { + "title": "Azure Federation", + "description": "Federating Coder to Azure", + "path": "./tutorials/azure-federation.md" + }, + { + "title": "Deploy Coder on Azure with an Application Gateway", + "description": "Deploy Coder on Azure with an Application Gateway", + "path": "./install/kubernetes/kubernetes-azure-app-gateway.md" + }, + { + "title": "Scanning Workspaces with JFrog Xray", + "description": "Integrate Coder with JFrog Xray", + "path": "./admin/integrations/jfrog-xray.md" + }, + { + "title": "Cloning Git Repositories", + "description": "Learn how to clone Git repositories in Coder", + "path": "./tutorials/cloning-git-repositories.md" + }, + { + "title": "Test Templates Through CI/CD", + "description": "Learn how to test and publish Coder templates in a CI/CD pipeline", + "path": "./tutorials/testing-templates.md" + }, + { + "title": "Use Apache as a Reverse Proxy", + "description": "Learn how to use Apache as a reverse proxy", + "path": "./tutorials/reverse-proxy-apache.md" + }, + { + "title": "Use Caddy as a Reverse Proxy", + "description": "Learn how to use Caddy as a reverse proxy", + "path": "./tutorials/reverse-proxy-caddy.md" + }, + { + "title": "Use NGINX as a Reverse Proxy", + "description": "Learn how to use NGINX as a reverse proxy", + "path": "./tutorials/reverse-proxy-nginx.md" + }, + { + "title": "Pre-install JetBrains IDEs in Workspaces", + "description": "Pre-install JetBrains IDEs in workspaces", + "path": "./admin/templates/extending-templates/jetbrains-preinstall.md" + }, + { + "title": "Use JetBrains IDEs in Air-Gapped Deployments", + "description": "Configure JetBrains IDEs for air-gapped deployments", + "path": "./admin/templates/extending-templates/jetbrains-airgapped.md" + }, + { + "title": "FAQs", + "description": "Miscellaneous FAQs from our community", + "path": "./tutorials/faqs.md" + }, + { + "title": "Best practices", + "description": "Guides to help you make the most of your Coder experience", + "path": "./tutorials/best-practices/index.md", + "children": [ + { + "title": "Organizations - best practices", + "description": "How to make the best use of Coder Organizations", + "path": "./tutorials/best-practices/organizations.md" + }, + { + "title": "Scale Coder", + "description": "How to prepare a Coder deployment for scale", + "path": "./tutorials/best-practices/scale-coder.md" + }, + { + "title": "Security - best practices", + "description": "Make your Coder deployment more secure", + "path": "./tutorials/best-practices/security-best-practices.md" + }, + { + "title": "Speed up your workspaces", + "description": "Speed up your Coder templates and workspaces", + "path": "./tutorials/best-practices/speed-up-templates.md" + } + ] + } + ] + }, + { + "title": "Reference", + "description": "Reference", + "path": "./reference/index.md", + "icon_path": "./images/icons/notes.svg", + "children": [ + { + "title": "REST API", + "description": "Learn how to use Coderd API", + "path": "./reference/api/index.md", + "icon_path": "./images/icons/api.svg", + "children": [ + { + "title": "General", + "path": "./reference/api/general.md" + }, + { + "title": "Agents", + "path": "./reference/api/agents.md" + }, + { + "title": "Applications", + "path": "./reference/api/applications.md" + }, + { + "title": "Audit", + "path": "./reference/api/audit.md" + }, + { + "title": "Authentication", + "path": "./reference/api/authentication.md" + }, + { + "title": "Authorization", + "path": "./reference/api/authorization.md" + }, + { + "title": "Builds", + "path": "./reference/api/builds.md" + }, + { + "title": "Debug", + "path": "./reference/api/debug.md" + }, + { + "title": "Enterprise", + "path": "./reference/api/enterprise.md" + }, + { + "title": "Files", + "path": "./reference/api/files.md" + }, + { + "title": "Git", + "path": "./reference/api/git.md" + }, + { + "title": "Insights", + "path": "./reference/api/insights.md" + }, + { + "title": "Members", + "path": "./reference/api/members.md" + }, + { + "title": "Organizations", + "path": "./reference/api/organizations.md" + }, + { + "title": "PortSharing", + "path": "./reference/api/portsharing.md" + }, + { + "title": "Schemas", + "path": "./reference/api/schemas.md" + }, + { + "title": "Templates", + "path": "./reference/api/templates.md" + }, + { + "title": "Users", + "path": "./reference/api/users.md" + }, + { + "title": "WorkspaceProxies", + "path": "./reference/api/workspaceproxies.md" + }, + { + "title": "Workspaces", + "path": "./reference/api/workspaces.md" + } + ] + }, + { + "title": "Command Line", + "description": "Learn how to use Coder CLI", + "path": "./reference/cli/index.md", + "icon_path": "./images/icons/terminal.svg", + "children": [ + { + "title": "autoupdate", + "description": "Toggle auto-update policy for a workspace", + "path": "reference/cli/autoupdate.md" + }, + { + "title": "coder", + "path": "reference/cli/index.md" + }, + { + "title": "completion", + "description": "Install or update shell completion scripts for the detected or chosen shell.", + "path": "reference/cli/completion.md" + }, + { + "title": "config-ssh", + "description": "Add an SSH Host entry for your workspaces \"ssh workspace.coder\"", + "path": "reference/cli/config-ssh.md" + }, + { + "title": "create", + "description": "Create a workspace", + "path": "reference/cli/create.md" + }, + { + "title": "delete", + "description": "Delete a workspace", + "path": "reference/cli/delete.md" + }, + { + "title": "dotfiles", + "description": "Personalize your workspace by applying a canonical dotfiles repository", + "path": "reference/cli/dotfiles.md" + }, + { + "title": "external-auth", + "description": "Manage external authentication", + "path": "reference/cli/external-auth.md" + }, + { + "title": "external-auth access-token", + "description": "Print auth for an external provider", + "path": "reference/cli/external-auth_access-token.md" + }, + { + "title": "favorite", + "description": "Add a workspace to your favorites", + "path": "reference/cli/favorite.md" + }, + { + "title": "features", + "description": "List Enterprise features", + "path": "reference/cli/features.md" + }, + { + "title": "features list", + "path": "reference/cli/features_list.md" + }, + { + "title": "groups", + "description": "Manage groups", + "path": "reference/cli/groups.md" + }, + { + "title": "groups create", + "description": "Create a user group", + "path": "reference/cli/groups_create.md" + }, + { + "title": "groups delete", + "description": "Delete a user group", + "path": "reference/cli/groups_delete.md" + }, + { + "title": "groups edit", + "description": "Edit a user group", + "path": "reference/cli/groups_edit.md" + }, + { + "title": "groups list", + "description": "List user groups", + "path": "reference/cli/groups_list.md" + }, + { + "title": "licenses", + "description": "Add, delete, and list licenses", + "path": "reference/cli/licenses.md" + }, + { + "title": "licenses add", + "description": "Add license to Coder deployment", + "path": "reference/cli/licenses_add.md" + }, + { + "title": "licenses delete", + "description": "Delete license by ID", + "path": "reference/cli/licenses_delete.md" + }, + { + "title": "licenses list", + "description": "List licenses (including expired)", + "path": "reference/cli/licenses_list.md" + }, + { + "title": "list", + "description": "List workspaces", + "path": "reference/cli/list.md" + }, + { + "title": "login", + "description": "Authenticate with Coder deployment", + "path": "reference/cli/login.md" + }, + { + "title": "logout", + "description": "Unauthenticate your local session", + "path": "reference/cli/logout.md" + }, + { + "title": "netcheck", + "description": "Print network debug information for DERP and STUN", + "path": "reference/cli/netcheck.md" + }, + { + "title": "notifications", + "description": "Manage Coder notifications", + "path": "reference/cli/notifications.md" + }, + { + "title": "notifications pause", + "description": "Pause notifications", + "path": "reference/cli/notifications_pause.md" + }, + { + "title": "notifications resume", + "description": "Resume notifications", + "path": "reference/cli/notifications_resume.md" + }, + { + "title": "notifications test", + "description": "Send a test notification", + "path": "reference/cli/notifications_test.md" + }, + { + "title": "open", + "description": "Open a workspace", + "path": "reference/cli/open.md" + }, + { + "title": "open app", + "description": "Open a workspace application.", + "path": "reference/cli/open_app.md" + }, + { + "title": "open vscode", + "description": "Open a workspace in VS Code Desktop", + "path": "reference/cli/open_vscode.md" + }, + { + "title": "organizations", + "description": "Organization related commands", + "path": "reference/cli/organizations.md" + }, + { + "title": "organizations create", + "description": "Create a new organization.", + "path": "reference/cli/organizations_create.md" + }, + { + "title": "organizations members", + "description": "Manage organization members", + "path": "reference/cli/organizations_members.md" + }, + { + "title": "organizations members add", + "description": "Add a new member to the current organization", + "path": "reference/cli/organizations_members_add.md" + }, + { + "title": "organizations members edit-roles", + "description": "Edit organization member's roles", + "path": "reference/cli/organizations_members_edit-roles.md" + }, + { + "title": "organizations members list", + "description": "List all organization members", + "path": "reference/cli/organizations_members_list.md" + }, + { + "title": "organizations members remove", + "description": "Remove a new member to the current organization", + "path": "reference/cli/organizations_members_remove.md" + }, + { + "title": "organizations roles", + "description": "Manage organization roles.", + "path": "reference/cli/organizations_roles.md" + }, + { + "title": "organizations roles create", + "description": "Create a new organization custom role", + "path": "reference/cli/organizations_roles_create.md" + }, + { + "title": "organizations roles show", + "description": "Show role(s)", + "path": "reference/cli/organizations_roles_show.md" + }, + { + "title": "organizations roles update", + "description": "Update an organization custom role", + "path": "reference/cli/organizations_roles_update.md" + }, + { + "title": "organizations settings", + "description": "Manage organization settings.", + "path": "reference/cli/organizations_settings.md" + }, + { + "title": "organizations settings set", + "description": "Update specified organization setting.", + "path": "reference/cli/organizations_settings_set.md" + }, + { + "title": "organizations settings set group-sync", + "description": "Group sync settings to sync groups from an IdP.", + "path": "reference/cli/organizations_settings_set_group-sync.md" + }, + { + "title": "organizations settings set organization-sync", + "description": "Organization sync settings to sync organization memberships from an IdP.", + "path": "reference/cli/organizations_settings_set_organization-sync.md" + }, + { + "title": "organizations settings set role-sync", + "description": "Role sync settings to sync organization roles from an IdP.", + "path": "reference/cli/organizations_settings_set_role-sync.md" + }, + { + "title": "organizations settings show", + "description": "Outputs specified organization setting.", + "path": "reference/cli/organizations_settings_show.md" + }, + { + "title": "organizations settings show group-sync", + "description": "Group sync settings to sync groups from an IdP.", + "path": "reference/cli/organizations_settings_show_group-sync.md" + }, + { + "title": "organizations settings show organization-sync", + "description": "Organization sync settings to sync organization memberships from an IdP.", + "path": "reference/cli/organizations_settings_show_organization-sync.md" + }, + { + "title": "organizations settings show role-sync", + "description": "Role sync settings to sync organization roles from an IdP.", + "path": "reference/cli/organizations_settings_show_role-sync.md" + }, + { + "title": "organizations show", + "description": "Show the organization. Using \"selected\" will show the selected organization from the \"--org\" flag. Using \"me\" will show all organizations you are a member of.", + "path": "reference/cli/organizations_show.md" + }, + { + "title": "ping", + "description": "Ping a workspace", + "path": "reference/cli/ping.md" + }, + { + "title": "port-forward", + "description": "Forward ports from a workspace to the local machine. For reverse port forwarding, use \"coder ssh -R\".", + "path": "reference/cli/port-forward.md" + }, + { + "title": "prebuilds", + "description": "Manage Coder prebuilds", + "path": "reference/cli/prebuilds.md" + }, + { + "title": "prebuilds pause", + "description": "Pause prebuilds", + "path": "reference/cli/prebuilds_pause.md" + }, + { + "title": "prebuilds resume", + "description": "Resume prebuilds", + "path": "reference/cli/prebuilds_resume.md" + }, + { + "title": "provisioner", + "description": "View and manage provisioner daemons and jobs", + "path": "reference/cli/provisioner.md" + }, + { + "title": "provisioner jobs", + "description": "View and manage provisioner jobs", + "path": "reference/cli/provisioner_jobs.md" + }, + { + "title": "provisioner jobs cancel", + "description": "Cancel a provisioner job", + "path": "reference/cli/provisioner_jobs_cancel.md" + }, + { + "title": "provisioner jobs list", + "description": "List provisioner jobs", + "path": "reference/cli/provisioner_jobs_list.md" + }, + { + "title": "provisioner keys", + "description": "Manage provisioner keys", + "path": "reference/cli/provisioner_keys.md" + }, + { + "title": "provisioner keys create", + "description": "Create a new provisioner key", + "path": "reference/cli/provisioner_keys_create.md" + }, + { + "title": "provisioner keys delete", + "description": "Delete a provisioner key", + "path": "reference/cli/provisioner_keys_delete.md" + }, + { + "title": "provisioner keys list", + "description": "List provisioner keys in an organization", + "path": "reference/cli/provisioner_keys_list.md" + }, + { + "title": "provisioner list", + "description": "List provisioner daemons in an organization", + "path": "reference/cli/provisioner_list.md" + }, + { + "title": "provisioner start", + "description": "Run a provisioner daemon", + "path": "reference/cli/provisioner_start.md" + }, + { + "title": "publickey", + "description": "Output your Coder public key used for Git operations", + "path": "reference/cli/publickey.md" + }, + { + "title": "rename", + "description": "Rename a workspace", + "path": "reference/cli/rename.md" + }, + { + "title": "reset-password", + "description": "Directly connect to the database to reset a user's password", + "path": "reference/cli/reset-password.md" + }, + { + "title": "restart", + "description": "Restart a workspace", + "path": "reference/cli/restart.md" + }, + { + "title": "schedule", + "description": "Schedule automated start and stop times for workspaces", + "path": "reference/cli/schedule.md" + }, + { + "title": "schedule extend", + "description": "Extend the stop time of a currently running workspace instance.", + "path": "reference/cli/schedule_extend.md" + }, + { + "title": "schedule show", + "description": "Show workspace schedules", + "path": "reference/cli/schedule_show.md" + }, + { + "title": "schedule start", + "description": "Edit workspace start schedule", + "path": "reference/cli/schedule_start.md" + }, + { + "title": "schedule stop", + "description": "Edit workspace stop schedule", + "path": "reference/cli/schedule_stop.md" + }, + { + "title": "server", + "description": "Start a Coder server", + "path": "reference/cli/server.md" + }, + { + "title": "server create-admin-user", + "description": "Create a new admin user with the given username, email and password and adds it to every organization.", + "path": "reference/cli/server_create-admin-user.md" + }, + { + "title": "server dbcrypt", + "description": "Manage database encryption.", + "path": "reference/cli/server_dbcrypt.md" + }, + { + "title": "server dbcrypt decrypt", + "description": "Decrypt a previously encrypted database.", + "path": "reference/cli/server_dbcrypt_decrypt.md" + }, + { + "title": "server dbcrypt delete", + "description": "Delete all encrypted data from the database. THIS IS A DESTRUCTIVE OPERATION.", + "path": "reference/cli/server_dbcrypt_delete.md" + }, + { + "title": "server dbcrypt rotate", + "description": "Rotate database encryption keys.", + "path": "reference/cli/server_dbcrypt_rotate.md" + }, + { + "title": "server postgres-builtin-serve", + "description": "Run the built-in PostgreSQL deployment.", + "path": "reference/cli/server_postgres-builtin-serve.md" + }, + { + "title": "server postgres-builtin-url", + "description": "Output the connection URL for the built-in PostgreSQL deployment.", + "path": "reference/cli/server_postgres-builtin-url.md" + }, + { + "title": "show", + "description": "Display details of a workspace's resources and agents", + "path": "reference/cli/show.md" + }, + { + "title": "speedtest", + "description": "Run upload and download tests from your machine to a workspace", + "path": "reference/cli/speedtest.md" + }, + { + "title": "ssh", + "description": "Start a shell into a workspace or run a command", + "path": "reference/cli/ssh.md" + }, + { + "title": "start", + "description": "Start a workspace", + "path": "reference/cli/start.md" + }, + { + "title": "stat", + "description": "Show resource usage for the current workspace.", + "path": "reference/cli/stat.md" + }, + { + "title": "stat cpu", + "description": "Show CPU usage, in cores.", + "path": "reference/cli/stat_cpu.md" + }, + { + "title": "stat disk", + "description": "Show disk usage, in gigabytes.", + "path": "reference/cli/stat_disk.md" + }, + { + "title": "stat mem", + "description": "Show memory usage, in gigabytes.", + "path": "reference/cli/stat_mem.md" + }, + { + "title": "state", + "description": "Manually manage Terraform state to fix broken workspaces", + "path": "reference/cli/state.md" + }, + { + "title": "state pull", + "description": "Pull a Terraform state file from a workspace.", + "path": "reference/cli/state_pull.md" + }, + { + "title": "state push", + "description": "Push a Terraform state file to a workspace.", + "path": "reference/cli/state_push.md" + }, + { + "title": "stop", + "description": "Stop a workspace", + "path": "reference/cli/stop.md" + }, + { + "title": "support", + "description": "Commands for troubleshooting issues with a Coder deployment.", + "path": "reference/cli/support.md" + }, + { + "title": "support bundle", + "description": "Generate a support bundle to troubleshoot issues connecting to a workspace.", + "path": "reference/cli/support_bundle.md" + }, + { + "title": "templates", + "description": "Manage templates", + "path": "reference/cli/templates.md" + }, + { + "title": "templates archive", + "description": "Archive unused or failed template versions from a given template(s)", + "path": "reference/cli/templates_archive.md" + }, + { + "title": "templates create", + "description": "DEPRECATED: Create a template from the current directory or as specified by flag", + "path": "reference/cli/templates_create.md" + }, + { + "title": "templates delete", + "description": "Delete templates", + "path": "reference/cli/templates_delete.md" + }, + { + "title": "templates edit", + "description": "Edit the metadata of a template by name.", + "path": "reference/cli/templates_edit.md" + }, + { + "title": "templates init", + "description": "Get started with a templated template.", + "path": "reference/cli/templates_init.md" + }, + { + "title": "templates list", + "description": "List all the templates available for the organization", + "path": "reference/cli/templates_list.md" + }, + { + "title": "templates presets", + "description": "Manage presets of the specified template", + "path": "reference/cli/templates_presets.md" + }, + { + "title": "templates presets list", + "description": "List all presets of the specified template. Defaults to the active template version.", + "path": "reference/cli/templates_presets_list.md" + }, + { + "title": "templates pull", + "description": "Download the active, latest, or specified version of a template to a path.", + "path": "reference/cli/templates_pull.md" + }, + { + "title": "templates push", + "description": "Create or update a template from the current directory or as specified by flag", + "path": "reference/cli/templates_push.md" + }, + { + "title": "templates versions", + "description": "Manage different versions of the specified template", + "path": "reference/cli/templates_versions.md" + }, + { + "title": "templates versions archive", + "description": "Archive a template version(s).", + "path": "reference/cli/templates_versions_archive.md" + }, + { + "title": "templates versions list", + "description": "List all the versions of the specified template", + "path": "reference/cli/templates_versions_list.md" + }, + { + "title": "templates versions promote", + "description": "Promote a template version to active.", + "path": "reference/cli/templates_versions_promote.md" + }, + { + "title": "templates versions unarchive", + "description": "Unarchive a template version(s).", + "path": "reference/cli/templates_versions_unarchive.md" + }, + { + "title": "tokens", + "description": "Manage personal access tokens", + "path": "reference/cli/tokens.md" + }, + { + "title": "tokens create", + "description": "Create a token", + "path": "reference/cli/tokens_create.md" + }, + { + "title": "tokens list", + "description": "List tokens", + "path": "reference/cli/tokens_list.md" + }, + { + "title": "tokens remove", + "description": "Delete a token", + "path": "reference/cli/tokens_remove.md" + }, + { + "title": "unfavorite", + "description": "Remove a workspace from your favorites", + "path": "reference/cli/unfavorite.md" + }, + { + "title": "update", + "description": "Will update and start a given workspace if it is out of date. If the workspace is already running, it will be stopped first.", + "path": "reference/cli/update.md" + }, + { + "title": "users", + "description": "Manage users", + "path": "reference/cli/users.md" + }, + { + "title": "users activate", + "description": "Update a user's status to 'active'. Active users can fully interact with the platform", + "path": "reference/cli/users_activate.md" + }, + { + "title": "users create", + "description": "Create a new user.", + "path": "reference/cli/users_create.md" + }, + { + "title": "users delete", + "description": "Delete a user by username or user_id.", + "path": "reference/cli/users_delete.md" + }, + { + "title": "users edit-roles", + "description": "Edit a user's roles by username or id", + "path": "reference/cli/users_edit-roles.md" + }, + { + "title": "users list", + "description": "Prints the list of users.", + "path": "reference/cli/users_list.md" + }, + { + "title": "users show", + "description": "Show a single user. Use 'me' to indicate the currently authenticated user.", + "path": "reference/cli/users_show.md" + }, + { + "title": "users suspend", + "description": "Update a user's status to 'suspended'. A suspended user cannot log into the platform", + "path": "reference/cli/users_suspend.md" + }, + { + "title": "version", + "description": "Show coder version", + "path": "reference/cli/version.md" + }, + { + "title": "whoami", + "description": "Fetch authenticated user info for Coder deployment", + "path": "reference/cli/whoami.md" + } + ] + }, + { + "title": "Agent API", + "description": "Learn how to use Coder Agent API", + "path": "./reference/agent-api/index.md", + "icon_path": "./images/icons/api.svg", + "children": [ + { + "title": "Debug", + "path": "./reference/agent-api/debug.md" + }, + { + "title": "Schemas", + "path": "./reference/agent-api/schemas.md" + } + ] + } + ] + } + ] +}