Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16361af95a | |||
| e8eef04afb |
@@ -402,7 +402,6 @@ func WorkspaceAgentDevcontainer(t testing.TB, db database.Store, orig database.W
|
||||
Name: []string{takeFirst(orig.Name, testutil.GetRandomName(t))},
|
||||
WorkspaceFolder: []string{takeFirst(orig.WorkspaceFolder, "/workspace")},
|
||||
ConfigPath: []string{takeFirst(orig.ConfigPath, "")},
|
||||
SubagentID: []uuid.UUID{takeFirst(orig.SubagentID, uuid.NullUUID{}).UUID},
|
||||
})
|
||||
require.NoError(t, err, "insert workspace agent devcontainer")
|
||||
return devcontainers[0]
|
||||
|
||||
Generated
+1
-2
@@ -2505,8 +2505,7 @@ CREATE TABLE workspace_agent_devcontainers (
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
workspace_folder text NOT NULL,
|
||||
config_path text NOT NULL,
|
||||
name text NOT NULL,
|
||||
subagent_id uuid
|
||||
name text NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON TABLE workspace_agent_devcontainers IS 'Workspace agent devcontainer configuration';
|
||||
|
||||
@@ -1 +1 @@
|
||||
DROP INDEX IF EXISTS workspace_agents_auth_instance_id_deleted_idx;
|
||||
DROP INDEX IF EXISTS public.workspace_agents_auth_instance_id_deleted_idx;
|
||||
|
||||
@@ -1 +1 @@
|
||||
CREATE INDEX IF NOT EXISTS workspace_agents_auth_instance_id_deleted_idx ON workspace_agents (auth_instance_id, deleted);
|
||||
CREATE INDEX IF NOT EXISTS workspace_agents_auth_instance_id_deleted_idx ON public.workspace_agents (auth_instance_id, deleted);
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE workspace_agent_devcontainers
|
||||
DROP COLUMN subagent_id;
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE workspace_agent_devcontainers
|
||||
ADD COLUMN subagent_id UUID;
|
||||
@@ -4743,8 +4743,7 @@ type WorkspaceAgentDevcontainer struct {
|
||||
// Path to devcontainer.json.
|
||||
ConfigPath string `db:"config_path" json:"config_path"`
|
||||
// The name of the Dev Container.
|
||||
Name string `db:"name" json:"name"`
|
||||
SubagentID uuid.NullUUID `db:"subagent_id" json:"subagent_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
type WorkspaceAgentLog struct {
|
||||
|
||||
@@ -7989,99 +7989,3 @@ func TestDeleteExpiredAPIKeys(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, remaining, len(unexpiredTimes))
|
||||
}
|
||||
|
||||
func TestWorkspaceAgentDevcontainersSubagentID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
|
||||
// Setup: create workspace agent
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
tpl := dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
OrganizationID: org.ID,
|
||||
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
ws := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OrganizationID: org.ID,
|
||||
OwnerID: user.ID,
|
||||
TemplateID: tpl.ID,
|
||||
})
|
||||
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
WorkspaceID: ws.ID,
|
||||
JobID: job.ID,
|
||||
TemplateVersionID: tv.ID,
|
||||
})
|
||||
res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: build.JobID,
|
||||
})
|
||||
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: res.ID,
|
||||
})
|
||||
|
||||
// Create a subagent that will be referenced
|
||||
subagent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: res.ID,
|
||||
ParentID: uuid.NullUUID{UUID: agent.ID, Valid: true},
|
||||
})
|
||||
|
||||
t.Run("InsertWithSubagentID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
devcontainers, err := db.InsertWorkspaceAgentDevcontainers(ctx, database.InsertWorkspaceAgentDevcontainersParams{
|
||||
WorkspaceAgentID: agent.ID,
|
||||
CreatedAt: dbtime.Now(),
|
||||
ID: []uuid.UUID{uuid.New()},
|
||||
Name: []string{"test-devcontainer"},
|
||||
WorkspaceFolder: []string{"/workspace"},
|
||||
ConfigPath: []string{"/workspace/.devcontainer/devcontainer.json"},
|
||||
SubagentID: []uuid.UUID{subagent.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, devcontainers, 1)
|
||||
require.True(t, devcontainers[0].SubagentID.Valid)
|
||||
require.Equal(t, subagent.ID, devcontainers[0].SubagentID.UUID)
|
||||
|
||||
// Verify retrieval
|
||||
retrieved, err := db.GetWorkspaceAgentDevcontainersByAgentID(ctx, agent.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, retrieved, 1)
|
||||
require.True(t, retrieved[0].SubagentID.Valid)
|
||||
require.Equal(t, subagent.ID, retrieved[0].SubagentID.UUID)
|
||||
})
|
||||
|
||||
t.Run("InsertWithNilSubagentID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
// Create a separate agent for this subtest
|
||||
agent2 := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: res.ID,
|
||||
})
|
||||
|
||||
// When uuid.Nil is passed, it stores the zero UUID (not NULL).
|
||||
// This matches the provisionerdserver behavior.
|
||||
devcontainers, err := db.InsertWorkspaceAgentDevcontainers(ctx, database.InsertWorkspaceAgentDevcontainersParams{
|
||||
WorkspaceAgentID: agent2.ID,
|
||||
CreatedAt: dbtime.Now(),
|
||||
ID: []uuid.UUID{uuid.New()},
|
||||
Name: []string{"no-subagent"},
|
||||
WorkspaceFolder: []string{"/workspace"},
|
||||
ConfigPath: []string{""},
|
||||
SubagentID: []uuid.UUID{uuid.Nil},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, devcontainers, 1)
|
||||
// uuid.Nil is stored as a zero UUID, not NULL.
|
||||
require.Equal(t, uuid.Nil, devcontainers[0].SubagentID.UUID)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17336,7 +17336,7 @@ func (q *sqlQuerier) ValidateUserIDs(ctx context.Context, userIds []uuid.UUID) (
|
||||
|
||||
const getWorkspaceAgentDevcontainersByAgentID = `-- name: GetWorkspaceAgentDevcontainersByAgentID :many
|
||||
SELECT
|
||||
id, workspace_agent_id, created_at, workspace_folder, config_path, name, subagent_id
|
||||
id, workspace_agent_id, created_at, workspace_folder, config_path, name
|
||||
FROM
|
||||
workspace_agent_devcontainers
|
||||
WHERE
|
||||
@@ -17361,7 +17361,6 @@ func (q *sqlQuerier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context
|
||||
&i.WorkspaceFolder,
|
||||
&i.ConfigPath,
|
||||
&i.Name,
|
||||
&i.SubagentID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -17378,16 +17377,15 @@ func (q *sqlQuerier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context
|
||||
|
||||
const insertWorkspaceAgentDevcontainers = `-- name: InsertWorkspaceAgentDevcontainers :many
|
||||
INSERT INTO
|
||||
workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, workspace_folder, config_path, subagent_id)
|
||||
workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, 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 name,
|
||||
unnest($5::text[]) AS workspace_folder,
|
||||
unnest($6::text[]) AS config_path,
|
||||
unnest($7::uuid[]) AS subagent_id
|
||||
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, workspace_agent_devcontainers.name, workspace_agent_devcontainers.subagent_id
|
||||
unnest($6::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, workspace_agent_devcontainers.name
|
||||
`
|
||||
|
||||
type InsertWorkspaceAgentDevcontainersParams struct {
|
||||
@@ -17397,7 +17395,6 @@ type InsertWorkspaceAgentDevcontainersParams struct {
|
||||
Name []string `db:"name" json:"name"`
|
||||
WorkspaceFolder []string `db:"workspace_folder" json:"workspace_folder"`
|
||||
ConfigPath []string `db:"config_path" json:"config_path"`
|
||||
SubagentID []uuid.UUID `db:"subagent_id" json:"subagent_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg InsertWorkspaceAgentDevcontainersParams) ([]WorkspaceAgentDevcontainer, error) {
|
||||
@@ -17408,7 +17405,6 @@ func (q *sqlQuerier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg
|
||||
pq.Array(arg.Name),
|
||||
pq.Array(arg.WorkspaceFolder),
|
||||
pq.Array(arg.ConfigPath),
|
||||
pq.Array(arg.SubagentID),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -17424,7 +17420,6 @@ func (q *sqlQuerier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg
|
||||
&i.WorkspaceFolder,
|
||||
&i.ConfigPath,
|
||||
&i.Name,
|
||||
&i.SubagentID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
-- name: InsertWorkspaceAgentDevcontainers :many
|
||||
INSERT INTO
|
||||
workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, workspace_folder, config_path, subagent_id)
|
||||
workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, 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(@name::text[]) AS name,
|
||||
unnest(@workspace_folder::text[]) AS workspace_folder,
|
||||
unnest(@config_path::text[]) AS config_path,
|
||||
unnest(@subagent_id::uuid[]) AS subagent_id
|
||||
unnest(@config_path::text[]) AS config_path
|
||||
RETURNING workspace_agent_devcontainers.*;
|
||||
|
||||
-- name: GetWorkspaceAgentDevcontainersByAgentID :many
|
||||
|
||||
@@ -2897,7 +2897,6 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
||||
devcontainerNames = make([]string, 0, len(devcontainers))
|
||||
devcontainerWorkspaceFolders = make([]string, 0, len(devcontainers))
|
||||
devcontainerConfigPaths = make([]string, 0, len(devcontainers))
|
||||
devcontainerSubagentIDs = make([]uuid.UUID, 0, len(devcontainers))
|
||||
)
|
||||
for _, dc := range devcontainers {
|
||||
id := uuid.New()
|
||||
@@ -2905,9 +2904,6 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
||||
devcontainerNames = append(devcontainerNames, dc.Name)
|
||||
devcontainerWorkspaceFolders = append(devcontainerWorkspaceFolders, dc.WorkspaceFolder)
|
||||
devcontainerConfigPaths = append(devcontainerConfigPaths, dc.ConfigPath)
|
||||
var subagentID uuid.UUID
|
||||
copy(subagentID[:], dc.GetSubagentId())
|
||||
devcontainerSubagentIDs = append(devcontainerSubagentIDs, subagentID)
|
||||
|
||||
// Add a log source and script for each devcontainer so we can
|
||||
// track logs and timings for each devcontainer.
|
||||
@@ -2936,7 +2932,6 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
||||
Name: devcontainerNames,
|
||||
WorkspaceFolder: devcontainerWorkspaceFolders,
|
||||
ConfigPath: devcontainerConfigPaths,
|
||||
SubagentID: devcontainerSubagentIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert agent devcontainer: %w", err)
|
||||
|
||||
@@ -3706,7 +3706,6 @@ func TestInsertWorkspaceResource(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{})
|
||||
subagentID := uuid.New()
|
||||
err := insert(db, job.ID, &sdkproto.Resource{
|
||||
Name: "something",
|
||||
Type: "aws_instance",
|
||||
@@ -3715,7 +3714,6 @@ func TestInsertWorkspaceResource(t *testing.T) {
|
||||
Devcontainers: []*sdkproto.Devcontainer{
|
||||
{Name: "foo", WorkspaceFolder: "/workspace1"},
|
||||
{Name: "bar", WorkspaceFolder: "/workspace2", ConfigPath: "/workspace2/.devcontainer/devcontainer.json"},
|
||||
{Name: "baz", WorkspaceFolder: "/workspace3", SubagentId: subagentID[:]},
|
||||
},
|
||||
}},
|
||||
})
|
||||
@@ -3732,17 +3730,13 @@ func TestInsertWorkspaceResource(t *testing.T) {
|
||||
return devcontainers[i].Name > devcontainers[j].Name
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, devcontainers, 3)
|
||||
require.Len(t, devcontainers, 2)
|
||||
require.Equal(t, "foo", devcontainers[0].Name)
|
||||
require.Equal(t, "/workspace1", devcontainers[0].WorkspaceFolder)
|
||||
require.Equal(t, "", devcontainers[0].ConfigPath)
|
||||
require.Equal(t, uuid.Nil, devcontainers[0].SubagentID.UUID)
|
||||
require.Equal(t, "baz", devcontainers[1].Name)
|
||||
require.Equal(t, "/workspace3", devcontainers[1].WorkspaceFolder)
|
||||
require.Equal(t, subagentID, devcontainers[1].SubagentID.UUID)
|
||||
require.Equal(t, "bar", devcontainers[2].Name)
|
||||
require.Equal(t, "/workspace2", devcontainers[2].WorkspaceFolder)
|
||||
require.Equal(t, "/workspace2/.devcontainer/devcontainer.json", devcontainers[2].ConfigPath)
|
||||
require.Equal(t, "bar", devcontainers[1].Name)
|
||||
require.Equal(t, "/workspace2", devcontainers[1].WorkspaceFolder)
|
||||
require.Equal(t, "/workspace2/.devcontainer/devcontainer.json", devcontainers[1].ConfigPath)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,77 @@ property in your `devcontainer.json`:
|
||||
This maps container ports to the parent workspace, which can then be forwarded
|
||||
using the main workspace agent.
|
||||
|
||||
## Docker Compose dev containers
|
||||
|
||||
Dev containers support Docker Compose for multi-container environments. When you
|
||||
define a dev container with `dockerComposeFile` and `service` properties, the
|
||||
devcontainer CLI orchestrates all services defined in your Compose file.
|
||||
|
||||
### Configuration
|
||||
|
||||
To use Docker Compose, your `devcontainer.json` must specify:
|
||||
|
||||
- `dockerComposeFile`: Path to your Docker Compose file(s)
|
||||
- `service`: The container Coder connects to (becomes the dev container sub-agent)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Project",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace"
|
||||
}
|
||||
```
|
||||
|
||||
With a corresponding `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: mcr.microsoft.com/devcontainers/base:ubuntu
|
||||
volumes:
|
||||
- .:/workspace
|
||||
command: sleep infinity
|
||||
database:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
```
|
||||
|
||||
The `app` service becomes your dev container with full Coder integration (SSH,
|
||||
web terminal, VS Code). The `database` service runs as a sidecar container.
|
||||
|
||||
### Container communication
|
||||
|
||||
Containers in a Compose setup communicate via Docker's internal DNS. From the
|
||||
primary container, reach sidecar services by their service name:
|
||||
|
||||
```console
|
||||
psql -h database -U postgres
|
||||
```
|
||||
|
||||
### Accessing sidecar services
|
||||
|
||||
Since only the primary service container runs as a Coder sub-agent, you cannot
|
||||
SSH or port-forward directly to sidecar containers. Instead:
|
||||
|
||||
1. **From the dev container**: Connect to sidecars using their service name
|
||||
(e.g., `psql -h database`).
|
||||
|
||||
1. **From your local machine**: Access sidecar services through the primary
|
||||
container. For example, run a proxy command in the dev container, then
|
||||
port-forward that port.
|
||||
|
||||
### Limitations
|
||||
|
||||
- The `forwardPorts` property with `host:port` syntax (e.g., `"database:5432"`)
|
||||
for forwarding ports from sidecar containers to your local machine is not yet
|
||||
supported.
|
||||
- Only the primary service container has Coder agent integration.
|
||||
|
||||
For more details on Docker Compose dev containers, see the
|
||||
[Dev Container specification](https://containers.dev/implementors/spec/#docker-compose-based).
|
||||
|
||||
## Dev container features
|
||||
|
||||
You can use standard [dev container features](https://containers.dev/features)
|
||||
|
||||
Generated
+536
-593
File diff suppressed because it is too large
Load Diff
@@ -244,11 +244,6 @@ message Devcontainer {
|
||||
string workspace_folder = 1;
|
||||
string config_path = 2;
|
||||
string name = 3;
|
||||
optional bytes id = 4;
|
||||
optional bytes subagent_id = 5;
|
||||
repeated App apps = 6;
|
||||
repeated Script scripts = 7;
|
||||
repeated Env envs = 8;
|
||||
}
|
||||
|
||||
enum AppOpenIn {
|
||||
|
||||
Generated
-20
@@ -306,11 +306,6 @@ export interface Devcontainer {
|
||||
workspaceFolder: string;
|
||||
configPath: string;
|
||||
name: string;
|
||||
id?: Uint8Array | undefined;
|
||||
subagentId?: Uint8Array | undefined;
|
||||
apps: App[];
|
||||
scripts: Script[];
|
||||
envs: Env[];
|
||||
}
|
||||
|
||||
/** App represents a dev-accessible application on the workspace. */
|
||||
@@ -1100,21 +1095,6 @@ export const Devcontainer = {
|
||||
if (message.name !== "") {
|
||||
writer.uint32(26).string(message.name);
|
||||
}
|
||||
if (message.id !== undefined) {
|
||||
writer.uint32(34).bytes(message.id);
|
||||
}
|
||||
if (message.subagentId !== undefined) {
|
||||
writer.uint32(42).bytes(message.subagentId);
|
||||
}
|
||||
for (const v of message.apps) {
|
||||
App.encode(v!, writer.uint32(50).fork()).ldelim();
|
||||
}
|
||||
for (const v of message.scripts) {
|
||||
Script.encode(v!, writer.uint32(58).fork()).ldelim();
|
||||
}
|
||||
for (const v of message.envs) {
|
||||
Env.encode(v!, writer.uint32(66).fork()).ldelim();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user