feat: allow specifying devcontainer on agent in terraform (#16997)

This change allows specifying devcontainers in terraform and plumbs it
through to the agent via agent manifest.

This will be used for autostarting devcontainers in a workspace.

Depends on coder/terraform-provider-coder#368
Updates #16423
This commit is contained in:
Mathias Fredriksson
2025-03-20 19:09:39 +02:00
committed by GitHub
parent 287e3198d8
commit 69ba27e347
49 changed files with 2615 additions and 1253 deletions
+814 -716
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -95,6 +95,13 @@ message Manifest {
repeated WorkspaceAgentScript scripts = 10;
repeated WorkspaceApp apps = 11;
repeated WorkspaceAgentMetadata.Description metadata = 12;
repeated WorkspaceAgentDevcontainer devcontainers = 17;
}
message WorkspaceAgentDevcontainer {
bytes id = 1;
string workspace_folder = 2;
string config_path = 3;
}
message GetManifestRequest {}
+1 -1
View File
@@ -7,7 +7,7 @@
"last_seen_at": "====[timestamp]=====",
"name": "test",
"version": "v0.0.0-devel",
"api_version": "1.3",
"api_version": "1.4",
"provisioners": [
"echo"
],
+31 -9
View File
@@ -3,6 +3,7 @@ package agentapi
import (
"context"
"database/sql"
"errors"
"net/url"
"strings"
"time"
@@ -42,11 +43,12 @@ func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifest
return nil, err
}
var (
dbApps []database.WorkspaceApp
scripts []database.WorkspaceAgentScript
metadata []database.WorkspaceAgentMetadatum
workspace database.Workspace
owner database.User
dbApps []database.WorkspaceApp
scripts []database.WorkspaceAgentScript
metadata []database.WorkspaceAgentMetadatum
workspace database.Workspace
owner database.User
devcontainers []database.WorkspaceAgentDevcontainer
)
var eg errgroup.Group
@@ -80,6 +82,13 @@ func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifest
}
return err
})
eg.Go(func() (err error) {
devcontainers, err = a.Database.GetWorkspaceAgentDevcontainersByAgentID(ctx, workspaceAgent.ID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
return nil
})
err = eg.Wait()
if err != nil {
return nil, xerrors.Errorf("fetching workspace agent data: %w", err)
@@ -125,10 +134,11 @@ func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifest
DisableDirectConnections: a.DisableDirectConnections,
DerpForceWebsockets: a.DerpForceWebSockets,
DerpMap: tailnet.DERPMapToProto(a.DerpMapFn()),
Scripts: dbAgentScriptsToProto(scripts),
Apps: apps,
Metadata: dbAgentMetadataToProtoDescription(metadata),
DerpMap: tailnet.DERPMapToProto(a.DerpMapFn()),
Scripts: dbAgentScriptsToProto(scripts),
Apps: apps,
Metadata: dbAgentMetadataToProtoDescription(metadata),
Devcontainers: dbAgentDevcontainersToProto(devcontainers),
}, nil
}
@@ -228,3 +238,15 @@ func dbAppToProto(dbApp database.WorkspaceApp, agent database.WorkspaceAgent, ow
Hidden: dbApp.Hidden,
}, nil
}
func dbAgentDevcontainersToProto(devcontainers []database.WorkspaceAgentDevcontainer) []*agentproto.WorkspaceAgentDevcontainer {
ret := make([]*agentproto.WorkspaceAgentDevcontainer, len(devcontainers))
for i, dc := range devcontainers {
ret[i] = &agentproto.WorkspaceAgentDevcontainer{
Id: dc.ID[:],
WorkspaceFolder: dc.WorkspaceFolder,
ConfigPath: dc.ConfigPath,
}
}
return ret
}
+36 -8
View File
@@ -156,6 +156,19 @@ func TestGetManifest(t *testing.T) {
CollectedAt: someTime.Add(time.Hour),
},
}
devcontainers = []database.WorkspaceAgentDevcontainer{
{
ID: uuid.New(),
WorkspaceAgentID: agent.ID,
WorkspaceFolder: "/cool/folder",
},
{
ID: uuid.New(),
WorkspaceAgentID: agent.ID,
WorkspaceFolder: "/another/cool/folder",
ConfigPath: "/another/cool/folder/.devcontainer/devcontainer.json",
},
}
derpMapFn = func() *tailcfg.DERPMap {
return &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
@@ -267,6 +280,17 @@ func TestGetManifest(t *testing.T) {
Timeout: durationpb.New(time.Duration(metadata[1].Timeout)),
},
}
protoDevcontainers = []*agentproto.WorkspaceAgentDevcontainer{
{
Id: devcontainers[0].ID[:],
WorkspaceFolder: devcontainers[0].WorkspaceFolder,
},
{
Id: devcontainers[1].ID[:],
WorkspaceFolder: devcontainers[1].WorkspaceFolder,
ConfigPath: devcontainers[1].ConfigPath,
},
}
)
t.Run("OK", func(t *testing.T) {
@@ -299,6 +323,7 @@ func TestGetManifest(t *testing.T) {
WorkspaceAgentID: agent.ID,
Keys: nil, // all
}).Return(metadata, nil)
mDB.EXPECT().GetWorkspaceAgentDevcontainersByAgentID(gomock.Any(), agent.ID).Return(devcontainers, nil)
mDB.EXPECT().GetWorkspaceByID(gomock.Any(), workspace.ID).Return(workspace, nil)
mDB.EXPECT().GetUserByID(gomock.Any(), workspace.OwnerID).Return(owner, nil)
@@ -321,10 +346,11 @@ func TestGetManifest(t *testing.T) {
// tailnet.DERPMapToProto() is extensively tested elsewhere, so it's
// not necessary to manually recreate a big DERP map here like we
// did for apps and metadata.
DerpMap: tailnet.DERPMapToProto(derpMapFn()),
Scripts: protoScripts,
Apps: protoApps,
Metadata: protoMetadata,
DerpMap: tailnet.DERPMapToProto(derpMapFn()),
Scripts: protoScripts,
Apps: protoApps,
Metadata: protoMetadata,
Devcontainers: protoDevcontainers,
}
// Log got and expected with spew.
@@ -364,6 +390,7 @@ func TestGetManifest(t *testing.T) {
WorkspaceAgentID: agent.ID,
Keys: nil, // all
}).Return(metadata, nil)
mDB.EXPECT().GetWorkspaceAgentDevcontainersByAgentID(gomock.Any(), agent.ID).Return(devcontainers, nil)
mDB.EXPECT().GetWorkspaceByID(gomock.Any(), workspace.ID).Return(workspace, nil)
mDB.EXPECT().GetUserByID(gomock.Any(), workspace.OwnerID).Return(owner, nil)
@@ -386,10 +413,11 @@ func TestGetManifest(t *testing.T) {
// tailnet.DERPMapToProto() is extensively tested elsewhere, so it's
// not necessary to manually recreate a big DERP map here like we
// did for apps and metadata.
DerpMap: tailnet.DERPMapToProto(derpMapFn()),
Scripts: protoScripts,
Apps: protoApps,
Metadata: protoMetadata,
DerpMap: tailnet.DERPMapToProto(derpMapFn()),
Scripts: protoScripts,
Apps: protoApps,
Metadata: protoMetadata,
Devcontainers: protoDevcontainers,
}
// Log got and expected with spew.
+2
View File
@@ -14079,6 +14079,7 @@ const docTemplate = `{
"template",
"user",
"workspace",
"workspace_agent_devcontainers",
"workspace_agent_resource_monitor",
"workspace_dormant",
"workspace_proxy"
@@ -14115,6 +14116,7 @@ const docTemplate = `{
"ResourceTemplate",
"ResourceUser",
"ResourceWorkspace",
"ResourceWorkspaceAgentDevcontainers",
"ResourceWorkspaceAgentResourceMonitor",
"ResourceWorkspaceDormant",
"ResourceWorkspaceProxy"
+2
View File
@@ -12746,6 +12746,7 @@
"template",
"user",
"workspace",
"workspace_agent_devcontainers",
"workspace_agent_resource_monitor",
"workspace_dormant",
"workspace_proxy"
@@ -12782,6 +12783,7 @@
"ResourceTemplate",
"ResourceUser",
"ResourceWorkspace",
"ResourceWorkspaceAgentDevcontainers",
"ResourceWorkspaceAgentResourceMonitor",
"ResourceWorkspaceDormant",
"ResourceWorkspaceProxy"
+16
View File
@@ -186,6 +186,7 @@ var (
rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead},
// Provisionerd creates workspaces resources monitor
rbac.ResourceWorkspaceAgentResourceMonitor.Type: {policy.ActionCreate},
rbac.ResourceWorkspaceAgentDevcontainers.Type: {policy.ActionCreate},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
@@ -2660,6 +2661,14 @@ func (q *querier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanc
return agent, nil
}
func (q *querier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentDevcontainer, error) {
_, err := q.GetWorkspaceAgentByID(ctx, workspaceAgentID)
if err != nil {
return nil, err
}
return q.db.GetWorkspaceAgentDevcontainersByAgentID(ctx, workspaceAgentID)
}
func (q *querier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceAgentLifecycleStateByIDRow, error) {
_, err := q.GetWorkspaceAgentByID(ctx, id)
if err != nil {
@@ -3390,6 +3399,13 @@ func (q *querier) InsertWorkspaceAgent(ctx context.Context, arg database.InsertW
return q.db.InsertWorkspaceAgent(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg database.InsertWorkspaceAgentDevcontainersParams) ([]database.WorkspaceAgentDevcontainer, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWorkspaceAgentDevcontainers); err != nil {
return nil, err
}
return q.db.InsertWorkspaceAgentDevcontainers(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentLogSources(ctx context.Context, arg database.InsertWorkspaceAgentLogSourcesParams) ([]database.WorkspaceAgentLogSource, error) {
// TODO: This is used by the agent, should we have an rbac check here?
return q.db.InsertWorkspaceAgentLogSources(ctx, arg)
+72
View File
@@ -3074,6 +3074,36 @@ func (s *MethodTestSuite) TestWorkspace() {
})
check.Args(w.ID).Asserts(w, policy.ActionUpdate).Returns()
}))
s.Run("GetWorkspaceAgentDevcontainersByAgentID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
o := dbgen.Organization(s.T(), db, database.Organization{})
tpl := dbgen.Template(s.T(), db, database.Template{
OrganizationID: o.ID,
CreatedBy: u.ID,
})
tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
OrganizationID: o.ID,
CreatedBy: u.ID,
})
w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{
TemplateID: tpl.ID,
OrganizationID: o.ID,
OwnerID: u.ID,
})
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{
JobID: j.ID,
WorkspaceID: w.ID,
TemplateVersionID: tv.ID,
})
res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: b.JobID})
agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID})
d := dbgen.WorkspaceAgentDevcontainer(s.T(), db, database.WorkspaceAgentDevcontainer{WorkspaceAgentID: agt.ID})
check.Args(agt.ID).Asserts(w, policy.ActionRead).Returns([]database.WorkspaceAgentDevcontainer{d})
}))
}
func (s *MethodTestSuite) TestWorkspacePortSharing() {
@@ -5021,3 +5051,45 @@ func (s *MethodTestSuite) TestResourcesMonitor() {
check.Args(agt.ID).Asserts(w, policy.ActionRead).Returns(monitors)
}))
}
func (s *MethodTestSuite) TestResourcesProvisionerdserver() {
createAgent := func(t *testing.T, db database.Store) (database.WorkspaceAgent, database.WorkspaceTable) {
t.Helper()
u := dbgen.User(t, db, database.User{})
o := dbgen.Organization(t, db, database.Organization{})
tpl := dbgen.Template(t, db, database.Template{
OrganizationID: o.ID,
CreatedBy: u.ID,
})
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
OrganizationID: o.ID,
CreatedBy: u.ID,
})
w := dbgen.Workspace(t, db, database.WorkspaceTable{
TemplateID: tpl.ID,
OrganizationID: o.ID,
OwnerID: u.ID,
})
j := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
b := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
JobID: j.ID,
WorkspaceID: w.ID,
TemplateVersionID: tv.ID,
})
res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: b.JobID})
agt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res.ID})
return agt, w
}
s.Run("InsertWorkspaceAgentDevcontainers", s.Subtest(func(db database.Store, check *expects) {
agt, _ := createAgent(s.T(), db)
check.Args(database.InsertWorkspaceAgentDevcontainersParams{
WorkspaceAgentID: agt.ID,
}).Asserts(rbac.ResourceWorkspaceAgentDevcontainers, policy.ActionCreate)
}))
}
+12
View File
@@ -255,6 +255,18 @@ func WorkspaceAgentScriptTiming(t testing.TB, db database.Store, orig database.W
panic("failed to insert workspace agent script timing")
}
func WorkspaceAgentDevcontainer(t testing.TB, db database.Store, orig database.WorkspaceAgentDevcontainer) database.WorkspaceAgentDevcontainer {
devcontainers, err := db.InsertWorkspaceAgentDevcontainers(genCtx, database.InsertWorkspaceAgentDevcontainersParams{
WorkspaceAgentID: takeFirst(orig.WorkspaceAgentID, uuid.New()),
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
ID: []uuid.UUID{takeFirst(orig.ID, uuid.New())},
WorkspaceFolder: []string{takeFirst(orig.WorkspaceFolder, "/workspace")},
ConfigPath: []string{takeFirst(orig.ConfigPath, "")},
})
require.NoError(t, err, "insert workspace agent devcontainer")
return devcontainers[0]
}
func Workspace(t testing.TB, db database.Store, orig database.WorkspaceTable) database.WorkspaceTable {
t.Helper()
+46
View File
@@ -237,6 +237,7 @@ type data struct {
workspaceAgentStats []database.WorkspaceAgentStat
workspaceAgentMemoryResourceMonitors []database.WorkspaceAgentMemoryResourceMonitor
workspaceAgentVolumeResourceMonitors []database.WorkspaceAgentVolumeResourceMonitor
workspaceAgentDevcontainers []database.WorkspaceAgentDevcontainer
workspaceApps []database.WorkspaceApp
workspaceAppAuditSessions []database.WorkspaceAppAuditSession
workspaceAppStatsLastInsertID int64
@@ -6696,6 +6697,22 @@ func (q *FakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceI
return database.WorkspaceAgent{}, sql.ErrNoRows
}
func (q *FakeQuerier) GetWorkspaceAgentDevcontainersByAgentID(_ context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentDevcontainer, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
devcontainers := make([]database.WorkspaceAgentDevcontainer, 0)
for _, dc := range q.workspaceAgentDevcontainers {
if dc.WorkspaceAgentID == workspaceAgentID {
devcontainers = append(devcontainers, dc)
}
}
if len(devcontainers) == 0 {
return nil, sql.ErrNoRows
}
return devcontainers, nil
}
func (q *FakeQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceAgentLifecycleStateByIDRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -9051,6 +9068,35 @@ func (q *FakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser
return agent, nil
}
func (q *FakeQuerier) InsertWorkspaceAgentDevcontainers(_ context.Context, arg database.InsertWorkspaceAgentDevcontainersParams) ([]database.WorkspaceAgentDevcontainer, error) {
err := validateDatabaseType(arg)
if err != nil {
return nil, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for _, agent := range q.workspaceAgents {
if agent.ID == arg.WorkspaceAgentID {
var devcontainers []database.WorkspaceAgentDevcontainer
for i, id := range arg.ID {
devcontainers = append(devcontainers, database.WorkspaceAgentDevcontainer{
WorkspaceAgentID: arg.WorkspaceAgentID,
CreatedAt: arg.CreatedAt,
ID: id,
WorkspaceFolder: arg.WorkspaceFolder[i],
ConfigPath: arg.ConfigPath[i],
})
}
q.workspaceAgentDevcontainers = append(q.workspaceAgentDevcontainers, devcontainers...)
return devcontainers, nil
}
}
return nil, errForeignKeyConstraint
}
func (q *FakeQuerier) InsertWorkspaceAgentLogSources(_ context.Context, arg database.InsertWorkspaceAgentLogSourcesParams) ([]database.WorkspaceAgentLogSource, error) {
err := validateDatabaseType(arg)
if err != nil {
+14
View File
@@ -1515,6 +1515,13 @@ func (m queryMetricsStore) GetWorkspaceAgentByInstanceID(ctx context.Context, au
return agent, err
}
func (m queryMetricsStore) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentDevcontainer, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceAgentDevcontainersByAgentID(ctx, workspaceAgentID)
m.queryLatencies.WithLabelValues("GetWorkspaceAgentDevcontainersByAgentID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceAgentLifecycleStateByIDRow, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceAgentLifecycleStateByID(ctx, id)
@@ -2138,6 +2145,13 @@ func (m queryMetricsStore) InsertWorkspaceAgent(ctx context.Context, arg databas
return agent, err
}
func (m queryMetricsStore) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg database.InsertWorkspaceAgentDevcontainersParams) ([]database.WorkspaceAgentDevcontainer, error) {
start := time.Now()
r0, r1 := m.s.InsertWorkspaceAgentDevcontainers(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWorkspaceAgentDevcontainers").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) InsertWorkspaceAgentLogSources(ctx context.Context, arg database.InsertWorkspaceAgentLogSourcesParams) ([]database.WorkspaceAgentLogSource, error) {
start := time.Now()
r0, r1 := m.s.InsertWorkspaceAgentLogSources(ctx, arg)
+30
View File
@@ -3172,6 +3172,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentByInstanceID(ctx, authInstance
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentByInstanceID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentByInstanceID), ctx, authInstanceID)
}
// GetWorkspaceAgentDevcontainersByAgentID mocks base method.
func (m *MockStore) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentDevcontainer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceAgentDevcontainersByAgentID", ctx, workspaceAgentID)
ret0, _ := ret[0].([]database.WorkspaceAgentDevcontainer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceAgentDevcontainersByAgentID indicates an expected call of GetWorkspaceAgentDevcontainersByAgentID.
func (mr *MockStoreMockRecorder) GetWorkspaceAgentDevcontainersByAgentID(ctx, workspaceAgentID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentDevcontainersByAgentID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentDevcontainersByAgentID), ctx, workspaceAgentID)
}
// GetWorkspaceAgentLifecycleStateByID mocks base method.
func (m *MockStore) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (database.GetWorkspaceAgentLifecycleStateByIDRow, error) {
m.ctrl.T.Helper()
@@ -4513,6 +4528,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAgent(ctx, arg any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgent", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgent), ctx, arg)
}
// InsertWorkspaceAgentDevcontainers mocks base method.
func (m *MockStore) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg database.InsertWorkspaceAgentDevcontainersParams) ([]database.WorkspaceAgentDevcontainer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWorkspaceAgentDevcontainers", ctx, arg)
ret0, _ := ret[0].([]database.WorkspaceAgentDevcontainer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertWorkspaceAgentDevcontainers indicates an expected call of InsertWorkspaceAgentDevcontainers.
func (mr *MockStoreMockRecorder) InsertWorkspaceAgentDevcontainers(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentDevcontainers", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentDevcontainers), ctx, arg)
}
// InsertWorkspaceAgentLogSources mocks base method.
func (m *MockStore) InsertWorkspaceAgentLogSources(ctx context.Context, arg database.InsertWorkspaceAgentLogSourcesParams) ([]database.WorkspaceAgentLogSource, error) {
m.ctrl.T.Helper()
+30
View File
@@ -1585,6 +1585,26 @@ CREATE TABLE user_status_changes (
COMMENT ON TABLE user_status_changes IS 'Tracks the history of user status changes';
CREATE TABLE workspace_agent_devcontainers (
id uuid NOT NULL,
workspace_agent_id uuid NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
workspace_folder text NOT NULL,
config_path text NOT NULL
);
COMMENT ON TABLE workspace_agent_devcontainers IS 'Workspace agent devcontainer configuration';
COMMENT ON COLUMN workspace_agent_devcontainers.id IS 'Unique identifier';
COMMENT ON COLUMN workspace_agent_devcontainers.workspace_agent_id IS 'Workspace agent foreign key';
COMMENT ON COLUMN workspace_agent_devcontainers.created_at IS 'Creation timestamp';
COMMENT ON COLUMN workspace_agent_devcontainers.workspace_folder IS 'Workspace folder';
COMMENT ON COLUMN workspace_agent_devcontainers.config_path IS 'Path to devcontainer.json.';
CREATE TABLE workspace_agent_log_sources (
workspace_agent_id uuid NOT NULL,
id uuid NOT NULL,
@@ -2250,6 +2270,9 @@ ALTER TABLE ONLY user_status_changes
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_agent_devcontainers
ADD CONSTRAINT workspace_agent_devcontainers_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_agent_log_sources
ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id);
@@ -2407,6 +2430,10 @@ CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WH
CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
CREATE INDEX workspace_agent_devcontainers_workspace_agent_id ON workspace_agent_devcontainers USING btree (workspace_agent_id);
COMMENT ON INDEX workspace_agent_devcontainers_workspace_agent_id IS 'Workspace agent foreign key and query index';
CREATE INDEX workspace_agent_scripts_workspace_agent_id_idx ON workspace_agent_scripts USING btree (workspace_agent_id);
COMMENT ON INDEX workspace_agent_scripts_workspace_agent_id_idx IS 'Foreign key support index for faster lookups';
@@ -2680,6 +2707,9 @@ ALTER TABLE ONLY user_links
ALTER TABLE ONLY user_status_changes
ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE ONLY workspace_agent_devcontainers
ADD CONSTRAINT workspace_agent_devcontainers_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agent_log_sources
ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
@@ -57,6 +57,7 @@ const (
ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
ForeignKeyWorkspaceAgentDevcontainersWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_devcontainers_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentMemoryResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_memory_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
@@ -0,0 +1 @@
DROP TABLE workspace_agent_devcontainers;
@@ -0,0 +1,19 @@
CREATE TABLE workspace_agent_devcontainers (
id UUID PRIMARY KEY,
workspace_agent_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
workspace_folder TEXT NOT NULL,
config_path TEXT NOT NULL,
FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE
);
COMMENT ON TABLE workspace_agent_devcontainers IS 'Workspace agent devcontainer configuration';
COMMENT ON COLUMN workspace_agent_devcontainers.id IS 'Unique identifier';
COMMENT ON COLUMN workspace_agent_devcontainers.workspace_agent_id IS 'Workspace agent foreign key';
COMMENT ON COLUMN workspace_agent_devcontainers.created_at IS 'Creation timestamp';
COMMENT ON COLUMN workspace_agent_devcontainers.workspace_folder IS 'Workspace folder';
COMMENT ON COLUMN workspace_agent_devcontainers.config_path IS 'Path to devcontainer.json.';
CREATE INDEX workspace_agent_devcontainers_workspace_agent_id ON workspace_agent_devcontainers (workspace_agent_id);
COMMENT ON INDEX workspace_agent_devcontainers_workspace_agent_id IS 'Workspace agent foreign key and query index';
@@ -0,0 +1,15 @@
INSERT INTO
workspace_agent_devcontainers (
workspace_agent_id,
created_at,
id,
workspace_folder,
config_path
)
VALUES (
'45e89705-e09d-4850-bcec-f9a937f5d78d',
'2021-09-01 00:00:00',
'489c0a1d-387d-41f0-be55-63aa7c5d7b14',
'/workspace',
'/workspace/.devcontainer/devcontainer.json'
)
+14
View File
@@ -3306,6 +3306,20 @@ type WorkspaceAgent struct {
DisplayOrder int32 `db:"display_order" json:"display_order"`
}
// Workspace agent devcontainer configuration
type WorkspaceAgentDevcontainer struct {
// Unique identifier
ID uuid.UUID `db:"id" json:"id"`
// Workspace agent foreign key
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
// Creation timestamp
CreatedAt time.Time `db:"created_at" json:"created_at"`
// Workspace folder
WorkspaceFolder string `db:"workspace_folder" json:"workspace_folder"`
// Path to devcontainer.json.
ConfigPath string `db:"config_path" json:"config_path"`
}
type WorkspaceAgentLog struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
+2
View File
@@ -342,6 +342,7 @@ type sqlcQuerier interface {
GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (GetWorkspaceAgentAndLatestBuildByAuthTokenRow, error)
GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentDevcontainer, error)
GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error)
GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error)
GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error)
@@ -452,6 +453,7 @@ type sqlcQuerier interface {
InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error)
InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (WorkspaceTable, error)
InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error)
InsertWorkspaceAgentDevcontainers(ctx context.Context, arg InsertWorkspaceAgentDevcontainersParams) ([]WorkspaceAgentDevcontainer, error)
InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error)
InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error)
InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error
+95
View File
@@ -12269,6 +12269,101 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
return i, err
}
const getWorkspaceAgentDevcontainersByAgentID = `-- name: GetWorkspaceAgentDevcontainersByAgentID :many
SELECT
id, workspace_agent_id, created_at, workspace_folder, config_path
FROM
workspace_agent_devcontainers
WHERE
workspace_agent_id = $1
ORDER BY
created_at, id
`
func (q *sqlQuerier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentDevcontainer, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentDevcontainersByAgentID, workspaceAgentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentDevcontainer
for rows.Next() {
var i WorkspaceAgentDevcontainer
if err := rows.Scan(
&i.ID,
&i.WorkspaceAgentID,
&i.CreatedAt,
&i.WorkspaceFolder,
&i.ConfigPath,
); 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 insertWorkspaceAgentDevcontainers = `-- name: InsertWorkspaceAgentDevcontainers :many
INSERT INTO
workspace_agent_devcontainers (workspace_agent_id, created_at, id, workspace_folder, config_path)
SELECT
$1::uuid AS workspace_agent_id,
$2::timestamptz AS created_at,
unnest($3::uuid[]) AS id,
unnest($4::text[]) AS workspace_folder,
unnest($5::text[]) AS config_path
RETURNING workspace_agent_devcontainers.id, workspace_agent_devcontainers.workspace_agent_id, workspace_agent_devcontainers.created_at, workspace_agent_devcontainers.workspace_folder, workspace_agent_devcontainers.config_path
`
type InsertWorkspaceAgentDevcontainersParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ID []uuid.UUID `db:"id" json:"id"`
WorkspaceFolder []string `db:"workspace_folder" json:"workspace_folder"`
ConfigPath []string `db:"config_path" json:"config_path"`
}
func (q *sqlQuerier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg InsertWorkspaceAgentDevcontainersParams) ([]WorkspaceAgentDevcontainer, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentDevcontainers,
arg.WorkspaceAgentID,
arg.CreatedAt,
pq.Array(arg.ID),
pq.Array(arg.WorkspaceFolder),
pq.Array(arg.ConfigPath),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentDevcontainer
for rows.Next() {
var i WorkspaceAgentDevcontainer
if err := rows.Scan(
&i.ID,
&i.WorkspaceAgentID,
&i.CreatedAt,
&i.WorkspaceFolder,
&i.ConfigPath,
); 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 deleteWorkspaceAgentPortShare = `-- name: DeleteWorkspaceAgentPortShare :exec
DELETE FROM
workspace_agent_port_share
@@ -0,0 +1,20 @@
-- name: InsertWorkspaceAgentDevcontainers :many
INSERT INTO
workspace_agent_devcontainers (workspace_agent_id, created_at, id, workspace_folder, config_path)
SELECT
@workspace_agent_id::uuid AS workspace_agent_id,
@created_at::timestamptz AS created_at,
unnest(@id::uuid[]) AS id,
unnest(@workspace_folder::text[]) AS workspace_folder,
unnest(@config_path::text[]) AS config_path
RETURNING workspace_agent_devcontainers.*;
-- name: GetWorkspaceAgentDevcontainersByAgentID :many
SELECT
*
FROM
workspace_agent_devcontainers
WHERE
workspace_agent_id = $1
ORDER BY
created_at, id;
+1
View File
@@ -70,6 +70,7 @@ const (
UniqueUserLinksPkey UniqueConstraint = "user_links_pkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_pkey PRIMARY KEY (user_id, login_type);
UniqueUserStatusChangesPkey UniqueConstraint = "user_status_changes_pkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id);
UniqueUsersPkey UniqueConstraint = "users_pkey" // ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
UniqueWorkspaceAgentDevcontainersPkey UniqueConstraint = "workspace_agent_devcontainers_pkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_pkey PRIMARY KEY (id);
UniqueWorkspaceAgentLogSourcesPkey UniqueConstraint = "workspace_agent_log_sources_pkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id);
UniqueWorkspaceAgentMemoryResourceMonitorsPkey UniqueConstraint = "workspace_agent_memory_resource_monitors_pkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_pkey PRIMARY KEY (agent_id);
UniqueWorkspaceAgentMetadataPkey UniqueConstraint = "workspace_agent_metadata_pkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key);
@@ -2096,6 +2096,30 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
return xerrors.Errorf("insert agent scripts: %w", err)
}
if devcontainers := prAgent.GetDevcontainers(); len(devcontainers) > 0 {
var (
devContainerIDs = make([]uuid.UUID, 0, len(devcontainers))
devContainerWorkspaceFolders = make([]string, 0, len(devcontainers))
devContainerConfigPaths = make([]string, 0, len(devcontainers))
)
for _, dc := range devcontainers {
devContainerIDs = append(devContainerIDs, uuid.New())
devContainerWorkspaceFolders = append(devContainerWorkspaceFolders, dc.WorkspaceFolder)
devContainerConfigPaths = append(devContainerConfigPaths, dc.ConfigPath)
}
_, err = db.InsertWorkspaceAgentDevcontainers(ctx, database.InsertWorkspaceAgentDevcontainersParams{
WorkspaceAgentID: agentID,
CreatedAt: dbtime.Now(),
ID: devContainerIDs,
WorkspaceFolder: devContainerWorkspaceFolders,
ConfigPath: devContainerConfigPaths,
})
if err != nil {
return xerrors.Errorf("insert agent devcontainer: %w", err)
}
}
for _, app := range prAgent.Apps {
// Similar logic is duplicated in terraform/resources.go.
slug := app.Slug
@@ -2190,6 +2190,37 @@ func TestInsertWorkspaceResource(t *testing.T) {
require.Equal(t, int32(50), volMonitors[1].Threshold)
require.Equal(t, "/volume2", volMonitors[1].Path)
})
t.Run("Devcontainers", func(t *testing.T) {
t.Parallel()
db := dbmem.New()
job := uuid.New()
err := insert(db, job, &sdkproto.Resource{
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev",
Devcontainers: []*sdkproto.Devcontainer{
{WorkspaceFolder: "/workspace1"},
{WorkspaceFolder: "/workspace2", ConfigPath: "/workspace2/.devcontainer/devcontainer.json"},
},
}},
})
require.NoError(t, err)
resources, err := db.GetWorkspaceResourcesByJobID(ctx, job)
require.NoError(t, err)
require.Len(t, resources, 1)
agents, err := db.GetWorkspaceAgentsByResourceIDs(ctx, []uuid.UUID{resources[0].ID})
require.NoError(t, err)
require.Len(t, agents, 1)
agent := agents[0]
devcontainers, err := db.GetWorkspaceAgentDevcontainersByAgentID(ctx, agent.ID)
require.NoError(t, err)
require.Len(t, devcontainers, 2)
require.Equal(t, "/workspace1", devcontainers[0].WorkspaceFolder)
require.Equal(t, "/workspace2", devcontainers[1].WorkspaceFolder)
require.Equal(t, "/workspace2/.devcontainer/devcontainer.json", devcontainers[1].ConfigPath)
})
}
func TestNotifications(t *testing.T) {
+8
View File
@@ -294,6 +294,13 @@ var (
Type: "workspace",
}
// ResourceWorkspaceAgentDevcontainers
// Valid Actions
// - "ActionCreate" :: create workspace agent devcontainers
ResourceWorkspaceAgentDevcontainers = Object{
Type: "workspace_agent_devcontainers",
}
// ResourceWorkspaceAgentResourceMonitor
// Valid Actions
// - "ActionCreate" :: create workspace agent resource monitor
@@ -361,6 +368,7 @@ func AllResources() []Objecter {
ResourceTemplate,
ResourceUser,
ResourceWorkspace,
ResourceWorkspaceAgentDevcontainers,
ResourceWorkspaceAgentResourceMonitor,
ResourceWorkspaceDormant,
ResourceWorkspaceProxy,
+5
View File
@@ -309,4 +309,9 @@ var RBACPermissions = map[string]PermissionDefinition{
ActionUpdate: actDef("update workspace agent resource monitor"),
},
},
"workspace_agent_devcontainers": {
Actions: map[Action]ActionDefinition{
ActionCreate: actDef("create workspace agent devcontainers"),
},
},
}
+15
View File
@@ -806,6 +806,21 @@ func TestRolePermissions(t *testing.T) {
},
},
},
{
Name: "WorkspaceAgentDevcontainers",
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceWorkspaceAgentDevcontainers,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {
memberMe, orgMemberMe, otherOrgMember,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
}
// We expect every permission to be tested above.
+1
View File
@@ -121,6 +121,7 @@ type Manifest struct {
DisableDirectConnections bool `json:"disable_direct_connections"`
Metadata []codersdk.WorkspaceAgentMetadataDescription `json:"metadata"`
Scripts []codersdk.WorkspaceAgentScript `json:"scripts"`
Devcontainers []codersdk.WorkspaceAgentDevcontainer `json:"devcontainers"`
}
type LogSource struct {
+46
View File
@@ -31,6 +31,10 @@ func ManifestFromProto(manifest *proto.Manifest) (Manifest, error) {
if err != nil {
return Manifest{}, xerrors.Errorf("error converting workspace ID: %w", err)
}
devcontainers, err := DevcontainersFromProto(manifest.Devcontainers)
if err != nil {
return Manifest{}, xerrors.Errorf("error converting workspace agent devcontainers: %w", err)
}
return Manifest{
AgentID: agentID,
AgentName: manifest.AgentName,
@@ -48,6 +52,7 @@ func ManifestFromProto(manifest *proto.Manifest) (Manifest, error) {
MOTDFile: manifest.MotdPath,
DisableDirectConnections: manifest.DisableDirectConnections,
Metadata: MetadataDescriptionsFromProto(manifest.Metadata),
Devcontainers: devcontainers,
}, nil
}
@@ -73,6 +78,7 @@ func ProtoFromManifest(manifest Manifest) (*proto.Manifest, error) {
Scripts: ProtoFromScripts(manifest.Scripts),
Apps: apps,
Metadata: ProtoFromMetadataDescriptions(manifest.Metadata),
Devcontainers: ProtoFromDevcontainers(manifest.Devcontainers),
}, nil
}
@@ -424,3 +430,43 @@ func ProtoFromConnectionType(typ ConnectionType) (proto.Connection_Type, error)
return 0, xerrors.Errorf("unknown connection type %q", typ)
}
}
func DevcontainersFromProto(pdcs []*proto.WorkspaceAgentDevcontainer) ([]codersdk.WorkspaceAgentDevcontainer, error) {
ret := make([]codersdk.WorkspaceAgentDevcontainer, len(pdcs))
for i, pdc := range pdcs {
dc, err := DevcontainerFromProto(pdc)
if err != nil {
return nil, xerrors.Errorf("parse devcontainer %v: %w", i, err)
}
ret[i] = dc
}
return ret, nil
}
func DevcontainerFromProto(pdc *proto.WorkspaceAgentDevcontainer) (codersdk.WorkspaceAgentDevcontainer, error) {
id, err := uuid.FromBytes(pdc.Id)
if err != nil {
return codersdk.WorkspaceAgentDevcontainer{}, xerrors.Errorf("parse id: %w", err)
}
return codersdk.WorkspaceAgentDevcontainer{
ID: id,
WorkspaceFolder: pdc.WorkspaceFolder,
ConfigPath: pdc.ConfigPath,
}, nil
}
func ProtoFromDevcontainers(dcs []codersdk.WorkspaceAgentDevcontainer) []*proto.WorkspaceAgentDevcontainer {
ret := make([]*proto.WorkspaceAgentDevcontainer, len(dcs))
for i, dc := range dcs {
ret[i] = ProtoFromDevcontainer(dc)
}
return ret
}
func ProtoFromDevcontainer(dc codersdk.WorkspaceAgentDevcontainer) *proto.WorkspaceAgentDevcontainer {
return &proto.WorkspaceAgentDevcontainer{
Id: dc.ID[:],
WorkspaceFolder: dc.WorkspaceFolder,
ConfigPath: dc.ConfigPath,
}
}
+8
View File
@@ -130,6 +130,13 @@ func TestManifest(t *testing.T) {
DisplayName: "bar",
},
},
Devcontainers: []codersdk.WorkspaceAgentDevcontainer{
{
ID: uuid.New(),
WorkspaceFolder: "/home/coder/coder",
ConfigPath: "/home/coder/coder/.devcontainer/devcontainer.json",
},
},
}
p, err := agentsdk.ProtoFromManifest(manifest)
require.NoError(t, err)
@@ -152,6 +159,7 @@ func TestManifest(t *testing.T) {
require.Equal(t, manifest.DisableDirectConnections, back.DisableDirectConnections)
require.Equal(t, manifest.Metadata, back.Metadata)
require.Equal(t, manifest.Scripts, back.Scripts)
require.Equal(t, manifest.Devcontainers, back.Devcontainers)
}
func TestSubsystems(t *testing.T) {
+2
View File
@@ -35,6 +35,7 @@ const (
ResourceTemplate RBACResource = "template"
ResourceUser RBACResource = "user"
ResourceWorkspace RBACResource = "workspace"
ResourceWorkspaceAgentDevcontainers RBACResource = "workspace_agent_devcontainers"
ResourceWorkspaceAgentResourceMonitor RBACResource = "workspace_agent_resource_monitor"
ResourceWorkspaceDormant RBACResource = "workspace_dormant"
ResourceWorkspaceProxy RBACResource = "workspace_proxy"
@@ -93,6 +94,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
ResourceTemplate: {ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionUse, ActionViewInsights},
ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal},
ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
ResourceWorkspaceAgentDevcontainers: {ActionCreate},
ResourceWorkspaceAgentResourceMonitor: {ActionCreate, ActionRead, ActionUpdate},
ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
ResourceWorkspaceProxy: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
+8
View File
@@ -392,6 +392,14 @@ func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid.
return listeningPorts, json.NewDecoder(res.Body).Decode(&listeningPorts)
}
// WorkspaceAgentDevcontainer defines the location of a devcontainer
// configuration in a workspace that is visible to the workspace agent.
type WorkspaceAgentDevcontainer struct {
ID uuid.UUID `json:"id" format:"uuid"`
WorkspaceFolder string `json:"workspace_folder"`
ConfigPath string `json:"config_path,omitempty"`
}
// WorkspaceAgentContainer describes a devcontainer of some sort
// that is visible to the workspace agent. This struct is an abstraction
// of potentially multiple implementations, and the fields will be
+5
View File
@@ -211,6 +211,7 @@ Status Code **200**
| `resource_type` | `template` |
| `resource_type` | `user` |
| `resource_type` | `workspace` |
| `resource_type` | `workspace_agent_devcontainers` |
| `resource_type` | `workspace_agent_resource_monitor` |
| `resource_type` | `workspace_dormant` |
| `resource_type` | `workspace_proxy` |
@@ -375,6 +376,7 @@ Status Code **200**
| `resource_type` | `template` |
| `resource_type` | `user` |
| `resource_type` | `workspace` |
| `resource_type` | `workspace_agent_devcontainers` |
| `resource_type` | `workspace_agent_resource_monitor` |
| `resource_type` | `workspace_dormant` |
| `resource_type` | `workspace_proxy` |
@@ -539,6 +541,7 @@ Status Code **200**
| `resource_type` | `template` |
| `resource_type` | `user` |
| `resource_type` | `workspace` |
| `resource_type` | `workspace_agent_devcontainers` |
| `resource_type` | `workspace_agent_resource_monitor` |
| `resource_type` | `workspace_dormant` |
| `resource_type` | `workspace_proxy` |
@@ -672,6 +675,7 @@ Status Code **200**
| `resource_type` | `template` |
| `resource_type` | `user` |
| `resource_type` | `workspace` |
| `resource_type` | `workspace_agent_devcontainers` |
| `resource_type` | `workspace_agent_resource_monitor` |
| `resource_type` | `workspace_dormant` |
| `resource_type` | `workspace_proxy` |
@@ -1027,6 +1031,7 @@ Status Code **200**
| `resource_type` | `template` |
| `resource_type` | `user` |
| `resource_type` | `workspace` |
| `resource_type` | `workspace_agent_devcontainers` |
| `resource_type` | `workspace_agent_resource_monitor` |
| `resource_type` | `workspace_dormant` |
| `resource_type` | `workspace_proxy` |
+1
View File
@@ -5321,6 +5321,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
| `template` |
| `user` |
| `workspace` |
| `workspace_agent_devcontainers` |
| `workspace_agent_resource_monitor` |
| `workspace_dormant` |
| `workspace_proxy` |
+32
View File
@@ -59,6 +59,12 @@ type agentAttributes struct {
ResourcesMonitoring []agentResourcesMonitoring `mapstructure:"resources_monitoring"`
}
type agentDevcontainerAttributes struct {
AgentID string `mapstructure:"agent_id"`
WorkspaceFolder string `mapstructure:"workspace_folder"`
ConfigPath string `mapstructure:"config_path"`
}
type agentResourcesMonitoring struct {
Memory []agentMemoryResourceMonitor `mapstructure:"memory"`
Volumes []agentVolumeResourceMonitor `mapstructure:"volume"`
@@ -590,6 +596,32 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
}
}
// Associate Dev Containers with agents.
for _, resources := range tfResourcesByLabel {
for _, resource := range resources {
if resource.Type != "coder_devcontainer" {
continue
}
var attrs agentDevcontainerAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return nil, xerrors.Errorf("decode script attributes: %w", err)
}
for _, agents := range resourceAgents {
for _, agent := range agents {
// Find agents with the matching ID and associate them!
if !dependsOnAgent(graph, agent, attrs.AgentID, resource) {
continue
}
agent.Devcontainers = append(agent.Devcontainers, &proto.Devcontainer{
WorkspaceFolder: attrs.WorkspaceFolder,
ConfigPath: attrs.ConfigPath,
})
}
}
}
}
// Associate metadata blocks with resources.
resourceMetadata := map[string][]*proto.Resource_Metadata{}
resourceHidden := map[string]bool{}
+31
View File
@@ -830,6 +830,34 @@ func TestConvertResources(t *testing.T) {
}},
}},
},
"devcontainer": {
resources: []*proto.Resource{
{
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
ResourcesMonitoring: &proto.ResourcesMonitoring{},
Devcontainers: []*proto.Devcontainer{
{
WorkspaceFolder: "/workspace1",
},
{
WorkspaceFolder: "/workspace2",
ConfigPath: "/workspace2/.devcontainer/devcontainer.json",
},
},
}},
},
{Name: "dev1", Type: "coder_devcontainer"},
{Name: "dev2", Type: "coder_devcontainer"},
},
},
} {
folderName := folderName
expected := expected
@@ -1375,6 +1403,9 @@ func sortResources(resources []*proto.Resource) {
sort.Slice(agent.Scripts, func(i, j int) bool {
return agent.Scripts[i].DisplayName < agent.Scripts[j].DisplayName
})
sort.Slice(agent.Devcontainers, func(i, j int) bool {
return agent.Devcontainers[i].WorkspaceFolder < agent.Devcontainers[j].WorkspaceFolder
})
}
sort.Slice(resource.Agents, func(i, j int) bool {
return resource.Agents[i].Name < resource.Agents[j].Name
@@ -0,0 +1,30 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">=2.0.0"
}
}
}
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
}
resource "coder_devcontainer" "dev1" {
agent_id = coder_agent.main.id
workspace_folder = "/workspace1"
}
resource "coder_devcontainer" "dev2" {
agent_id = coder_agent.main.id
workspace_folder = "/workspace2"
config_path = "/workspace2/.devcontainer/devcontainer.json"
}
resource "null_resource" "dev" {
depends_on = [
coder_agent.main
]
}
+22
View File
@@ -0,0 +1,22 @@
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] coder_agent.main (expand)" [label = "coder_agent.main", shape = "box"]
"[root] coder_devcontainer.dev1 (expand)" [label = "coder_devcontainer.dev1", shape = "box"]
"[root] coder_devcontainer.dev2 (expand)" [label = "coder_devcontainer.dev2", shape = "box"]
"[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"]
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
"[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"]
"[root] coder_agent.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] coder_devcontainer.dev1 (expand)" -> "[root] coder_agent.main (expand)"
"[root] coder_devcontainer.dev2 (expand)" -> "[root] coder_agent.main (expand)"
"[root] null_resource.dev (expand)" -> "[root] coder_agent.main (expand)"
"[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_devcontainer.dev1 (expand)"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_devcontainer.dev2 (expand)"
"[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)"
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
"[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)"
}
}
+288
View File
@@ -0,0 +1,288 @@
{
"format_version": "1.2",
"terraform_version": "1.11.0",
"planned_values": {
"root_module": {
"resources": [
{
"address": "coder_agent.main",
"mode": "managed",
"type": "coder_agent",
"name": "main",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 1,
"values": {
"arch": "amd64",
"auth": "token",
"connection_timeout": 120,
"dir": null,
"env": null,
"metadata": [],
"motd_file": null,
"order": null,
"os": "linux",
"resources_monitoring": [],
"shutdown_script": null,
"startup_script": null,
"startup_script_behavior": "non-blocking",
"troubleshooting_url": null
},
"sensitive_values": {
"display_apps": [],
"metadata": [],
"resources_monitoring": [],
"token": true
}
},
{
"address": "coder_devcontainer.dev1",
"mode": "managed",
"type": "coder_devcontainer",
"name": "dev1",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 1,
"values": {
"config_path": null,
"workspace_folder": "/workspace1"
},
"sensitive_values": {}
},
{
"address": "coder_devcontainer.dev2",
"mode": "managed",
"type": "coder_devcontainer",
"name": "dev2",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 1,
"values": {
"config_path": "/workspace2/.devcontainer/devcontainer.json",
"workspace_folder": "/workspace2"
},
"sensitive_values": {}
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"triggers": null
},
"sensitive_values": {}
}
]
}
},
"resource_changes": [
{
"address": "coder_agent.main",
"mode": "managed",
"type": "coder_agent",
"name": "main",
"provider_name": "registry.terraform.io/coder/coder",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"arch": "amd64",
"auth": "token",
"connection_timeout": 120,
"dir": null,
"env": null,
"metadata": [],
"motd_file": null,
"order": null,
"os": "linux",
"resources_monitoring": [],
"shutdown_script": null,
"startup_script": null,
"startup_script_behavior": "non-blocking",
"troubleshooting_url": null
},
"after_unknown": {
"display_apps": true,
"id": true,
"init_script": true,
"metadata": [],
"resources_monitoring": [],
"token": true
},
"before_sensitive": false,
"after_sensitive": {
"display_apps": [],
"metadata": [],
"resources_monitoring": [],
"token": true
}
}
},
{
"address": "coder_devcontainer.dev1",
"mode": "managed",
"type": "coder_devcontainer",
"name": "dev1",
"provider_name": "registry.terraform.io/coder/coder",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"config_path": null,
"workspace_folder": "/workspace1"
},
"after_unknown": {
"agent_id": true,
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "coder_devcontainer.dev2",
"mode": "managed",
"type": "coder_devcontainer",
"name": "dev2",
"provider_name": "registry.terraform.io/coder/coder",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"config_path": "/workspace2/.devcontainer/devcontainer.json",
"workspace_folder": "/workspace2"
},
"after_unknown": {
"agent_id": true,
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_name": "registry.terraform.io/hashicorp/null",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"triggers": null
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
}
],
"configuration": {
"provider_config": {
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": ">= 2.0.0"
},
"null": {
"name": "null",
"full_name": "registry.terraform.io/hashicorp/null"
}
},
"root_module": {
"resources": [
{
"address": "coder_agent.main",
"mode": "managed",
"type": "coder_agent",
"name": "main",
"provider_config_key": "coder",
"expressions": {
"arch": {
"constant_value": "amd64"
},
"os": {
"constant_value": "linux"
}
},
"schema_version": 1
},
{
"address": "coder_devcontainer.dev1",
"mode": "managed",
"type": "coder_devcontainer",
"name": "dev1",
"provider_config_key": "coder",
"expressions": {
"agent_id": {
"references": [
"coder_agent.main.id",
"coder_agent.main"
]
},
"workspace_folder": {
"constant_value": "/workspace1"
}
},
"schema_version": 1
},
{
"address": "coder_devcontainer.dev2",
"mode": "managed",
"type": "coder_devcontainer",
"name": "dev2",
"provider_config_key": "coder",
"expressions": {
"agent_id": {
"references": [
"coder_agent.main.id",
"coder_agent.main"
]
},
"config_path": {
"constant_value": "/workspace2/.devcontainer/devcontainer.json"
},
"workspace_folder": {
"constant_value": "/workspace2"
}
},
"schema_version": 1
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_config_key": "null",
"schema_version": 0,
"depends_on": [
"coder_agent.main"
]
}
]
}
},
"relevant_attributes": [
{
"resource": "coder_agent.main",
"attribute": [
"id"
]
}
],
"timestamp": "2025-03-19T12:53:34Z",
"applyable": true,
"complete": true,
"errored": false
}
+22
View File
@@ -0,0 +1,22 @@
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] coder_agent.main (expand)" [label = "coder_agent.main", shape = "box"]
"[root] coder_devcontainer.dev1 (expand)" [label = "coder_devcontainer.dev1", shape = "box"]
"[root] coder_devcontainer.dev2 (expand)" [label = "coder_devcontainer.dev2", shape = "box"]
"[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"]
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
"[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"]
"[root] coder_agent.main (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] coder_devcontainer.dev1 (expand)" -> "[root] coder_agent.main (expand)"
"[root] coder_devcontainer.dev2 (expand)" -> "[root] coder_agent.main (expand)"
"[root] null_resource.dev (expand)" -> "[root] coder_agent.main (expand)"
"[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_devcontainer.dev1 (expand)"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_devcontainer.dev2 (expand)"
"[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)"
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
"[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)"
}
}
+106
View File
@@ -0,0 +1,106 @@
{
"format_version": "1.0",
"terraform_version": "1.11.0",
"values": {
"root_module": {
"resources": [
{
"address": "coder_agent.main",
"mode": "managed",
"type": "coder_agent",
"name": "main",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 1,
"values": {
"arch": "amd64",
"auth": "token",
"connection_timeout": 120,
"dir": null,
"display_apps": [
{
"port_forwarding_helper": true,
"ssh_helper": true,
"vscode": true,
"vscode_insiders": false,
"web_terminal": true
}
],
"env": null,
"id": "eb1fa705-34c6-405b-a2ec-70e4efd1614e",
"init_script": "",
"metadata": [],
"motd_file": null,
"order": null,
"os": "linux",
"resources_monitoring": [],
"shutdown_script": null,
"startup_script": null,
"startup_script_behavior": "non-blocking",
"token": "e8663cf8-6991-40ca-b534-b9d48575cc4e",
"troubleshooting_url": null
},
"sensitive_values": {
"display_apps": [
{}
],
"metadata": [],
"resources_monitoring": [],
"token": true
}
},
{
"address": "coder_devcontainer.dev1",
"mode": "managed",
"type": "coder_devcontainer",
"name": "dev1",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 1,
"values": {
"agent_id": "eb1fa705-34c6-405b-a2ec-70e4efd1614e",
"config_path": null,
"id": "eb9b7f18-c277-48af-af7c-2a8e5fb42bab",
"workspace_folder": "/workspace1"
},
"sensitive_values": {},
"depends_on": [
"coder_agent.main"
]
},
{
"address": "coder_devcontainer.dev2",
"mode": "managed",
"type": "coder_devcontainer",
"name": "dev2",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 1,
"values": {
"agent_id": "eb1fa705-34c6-405b-a2ec-70e4efd1614e",
"config_path": "/workspace2/.devcontainer/devcontainer.json",
"id": "964430ff-f0d9-4fcb-b645-6333cf6ba9f2",
"workspace_folder": "/workspace2"
},
"sensitive_values": {},
"depends_on": [
"coder_agent.main"
]
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "4099703416178965439",
"triggers": null
},
"sensitive_values": {},
"depends_on": [
"coder_agent.main"
]
}
]
}
}
}
+5 -2
View File
@@ -8,10 +8,13 @@ import "github.com/coder/coder/v2/apiversion"
// - Add support for `open_in` parameters in the workspace apps.
//
// API v1.3:
// - Add new field named `resources_monitoring` in the Agent with resources monitoring..
// - Add new field named `resources_monitoring` in the Agent with resources monitoring.
//
// API v1.4:
// - Add new field named `devcontainers` in the Agent.
const (
CurrentMajor = 1
CurrentMinor = 3
CurrentMinor = 4
)
// CurrentVersion is the current provisionerd API version.
+604 -517
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -141,6 +141,7 @@ message Agent {
repeated Env extra_envs = 22;
int64 order = 23;
ResourcesMonitoring resources_monitoring = 24;
repeated Devcontainer devcontainers = 25;
}
enum AppSharingLevel {
@@ -191,6 +192,11 @@ message Script {
string log_path = 9;
}
message Devcontainer {
string workspace_folder = 1;
string config_path = 2;
}
enum AppOpenIn {
WINDOW = 0 [deprecated = true];
SLIM_WINDOW = 1;
+1
View File
@@ -640,6 +640,7 @@ const createTemplateVersionTar = async (
startupScriptTimeoutSeconds: 300,
troubleshootingUrl: "",
token: randomUUID(),
devcontainers: [],
...agent,
} as Agent;
+21
View File
@@ -158,6 +158,7 @@ export interface Agent {
extraEnvs: Env[];
order: number;
resourcesMonitoring: ResourcesMonitoring | undefined;
devcontainers: Devcontainer[];
}
export interface Agent_Metadata {
@@ -216,6 +217,11 @@ export interface Script {
logPath: string;
}
export interface Devcontainer {
workspaceFolder: string;
configPath: string;
}
/** App represents a dev-accessible application on the workspace. */
export interface App {
/**
@@ -643,6 +649,9 @@ export const Agent = {
if (message.resourcesMonitoring !== undefined) {
ResourcesMonitoring.encode(message.resourcesMonitoring, writer.uint32(194).fork()).ldelim();
}
for (const v of message.devcontainers) {
Devcontainer.encode(v!, writer.uint32(202).fork()).ldelim();
}
return writer;
},
};
@@ -788,6 +797,18 @@ export const Script = {
},
};
export const Devcontainer = {
encode(message: Devcontainer, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.workspaceFolder !== "") {
writer.uint32(10).string(message.workspaceFolder);
}
if (message.configPath !== "") {
writer.uint32(18).string(message.configPath);
}
return writer;
},
};
export const App = {
encode(message: App, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.slug !== "") {
+3
View File
@@ -167,6 +167,9 @@ export const RBACResourceActions: Partial<
stop: "allows stopping a workspace",
update: "edit workspace settings (scheduling, permissions, parameters)",
},
workspace_agent_devcontainers: {
create: "create workspace agent devcontainers",
},
workspace_agent_resource_monitor: {
create: "create workspace agent resource monitor",
read: "read workspace agent resource monitor",
+9
View File
@@ -1966,6 +1966,7 @@ export type RBACResource =
| "user"
| "*"
| "workspace"
| "workspace_agent_devcontainers"
| "workspace_agent_resource_monitor"
| "workspace_dormant"
| "workspace_proxy";
@@ -2002,6 +2003,7 @@ export const RBACResources: RBACResource[] = [
"user",
"*",
"workspace",
"workspace_agent_devcontainers",
"workspace_agent_resource_monitor",
"workspace_dormant",
"workspace_proxy",
@@ -3078,6 +3080,13 @@ export interface WorkspaceAgentContainerPort {
readonly host_port?: number;
}
// From codersdk/workspaceagents.go
export interface WorkspaceAgentDevcontainer {
readonly id: string;
readonly workspace_folder: string;
readonly config_path?: string;
}
// From codersdk/workspaceagents.go
export interface WorkspaceAgentHealth {
readonly healthy: boolean;