mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
Merge branch 'main' into jjs/presets
This commit is contained in:
116
coderd/apidoc/docs.go
generated
116
coderd/apidoc/docs.go
generated
@ -7930,6 +7930,49 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaceagents/{workspaceagent}/containers": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Agents"
|
||||
],
|
||||
"summary": "Get running containers for workspace agent",
|
||||
"operationId": "get-running-containers-for-workspace-agent",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace agent ID",
|
||||
"name": "workspaceagent",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "key=value",
|
||||
"description": "Labels",
|
||||
"name": "label",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAgentListContainersResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaceagents/{workspaceagent}/coordinate": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -13330,6 +13373,9 @@ const docTemplate = `{
|
||||
"template_display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@ -15709,6 +15755,57 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceAgentDevcontainer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "CreatedAt is the time the container was created.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID is the unique identifier of the container.",
|
||||
"type": "string"
|
||||
},
|
||||
"image": {
|
||||
"description": "Image is the name of the container image.",
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"description": "Labels is a map of key-value pairs of container labels.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"description": "FriendlyName is the human-readable name of the container.",
|
||||
"type": "string"
|
||||
},
|
||||
"ports": {
|
||||
"description": "Ports includes ports exposed by the container.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort"
|
||||
}
|
||||
},
|
||||
"running": {
|
||||
"description": "Running is true if the container is currently running.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"description": "Status is the current status of the container. This is somewhat\nimplementation-dependent, but should generally be a human-readable\nstring.",
|
||||
"type": "string"
|
||||
},
|
||||
"volumes": {
|
||||
"description": "Volumes is a map of \"things\" mounted into the container. Again, this\nis somewhat implementation-dependent.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceAgentHealth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -15749,6 +15846,25 @@ const docTemplate = `{
|
||||
"WorkspaceAgentLifecycleOff"
|
||||
]
|
||||
},
|
||||
"codersdk.WorkspaceAgentListContainersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"containers": {
|
||||
"description": "Containers is a list of containers visible to the workspace agent.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAgentDevcontainer"
|
||||
}
|
||||
},
|
||||
"warnings": {
|
||||
"description": "Warnings is a list of warnings that may have occurred during the\nprocess of listing containers. This should not include fatal errors.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceAgentListeningPort": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
112
coderd/apidoc/swagger.json
generated
112
coderd/apidoc/swagger.json
generated
@ -6998,6 +6998,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaceagents/{workspaceagent}/containers": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Agents"],
|
||||
"summary": "Get running containers for workspace agent",
|
||||
"operationId": "get-running-containers-for-workspace-agent",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace agent ID",
|
||||
"name": "workspaceagent",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "key=value",
|
||||
"description": "Labels",
|
||||
"name": "label",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAgentListContainersResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaceagents/{workspaceagent}/coordinate": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -12043,6 +12082,9 @@
|
||||
"template_display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@ -14308,6 +14350,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceAgentDevcontainer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "CreatedAt is the time the container was created.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID is the unique identifier of the container.",
|
||||
"type": "string"
|
||||
},
|
||||
"image": {
|
||||
"description": "Image is the name of the container image.",
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"description": "Labels is a map of key-value pairs of container labels.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"description": "FriendlyName is the human-readable name of the container.",
|
||||
"type": "string"
|
||||
},
|
||||
"ports": {
|
||||
"description": "Ports includes ports exposed by the container.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort"
|
||||
}
|
||||
},
|
||||
"running": {
|
||||
"description": "Running is true if the container is currently running.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"description": "Status is the current status of the container. This is somewhat\nimplementation-dependent, but should generally be a human-readable\nstring.",
|
||||
"type": "string"
|
||||
},
|
||||
"volumes": {
|
||||
"description": "Volumes is a map of \"things\" mounted into the container. Again, this\nis somewhat implementation-dependent.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceAgentHealth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -14348,6 +14441,25 @@
|
||||
"WorkspaceAgentLifecycleOff"
|
||||
]
|
||||
},
|
||||
"codersdk.WorkspaceAgentListContainersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"containers": {
|
||||
"description": "Containers is a list of containers visible to the workspace agent.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAgentDevcontainer"
|
||||
}
|
||||
},
|
||||
"warnings": {
|
||||
"description": "Warnings is a list of warnings that may have occurred during the\nprocess of listing containers. This should not include fatal errors.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceAgentListeningPort": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1215,6 +1215,7 @@ func New(options *Options) *API {
|
||||
r.Get("/logs", api.workspaceAgentLogs)
|
||||
r.Get("/listening-ports", api.workspaceAgentListeningPorts)
|
||||
r.Get("/connection", api.workspaceAgentConnection)
|
||||
r.Get("/containers", api.workspaceAgentListContainers)
|
||||
r.Get("/coordinate", api.workspaceAgentClientCoordinate)
|
||||
|
||||
// PTY is part of workspaceAppServer.
|
||||
|
@ -6437,6 +6437,7 @@ SELECT
|
||||
t.id AS template_id,
|
||||
COALESCE(t.name, '') AS template_name,
|
||||
COALESCE(t.display_name, '') AS template_display_name,
|
||||
COALESCE(t.icon, '') AS template_icon,
|
||||
w.id AS workspace_id,
|
||||
COALESCE(w.name, '') AS workspace_name
|
||||
FROM
|
||||
@ -6466,6 +6467,7 @@ GROUP BY
|
||||
t.id,
|
||||
t.name,
|
||||
t.display_name,
|
||||
t.icon,
|
||||
w.id,
|
||||
w.name
|
||||
ORDER BY
|
||||
@ -6490,6 +6492,7 @@ type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow
|
||||
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
|
||||
TemplateName string `db:"template_name" json:"template_name"`
|
||||
TemplateDisplayName string `db:"template_display_name" json:"template_display_name"`
|
||||
TemplateIcon string `db:"template_icon" json:"template_icon"`
|
||||
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
|
||||
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
|
||||
}
|
||||
@ -6535,6 +6538,7 @@ func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionA
|
||||
&i.TemplateID,
|
||||
&i.TemplateName,
|
||||
&i.TemplateDisplayName,
|
||||
&i.TemplateIcon,
|
||||
&i.WorkspaceID,
|
||||
&i.WorkspaceName,
|
||||
); err != nil {
|
||||
|
@ -136,6 +136,7 @@ SELECT
|
||||
t.id AS template_id,
|
||||
COALESCE(t.name, '') AS template_name,
|
||||
COALESCE(t.display_name, '') AS template_display_name,
|
||||
COALESCE(t.icon, '') AS template_icon,
|
||||
w.id AS workspace_id,
|
||||
COALESCE(w.name, '') AS workspace_name
|
||||
FROM
|
||||
@ -165,6 +166,7 @@ GROUP BY
|
||||
t.id,
|
||||
t.name,
|
||||
t.display_name,
|
||||
t.icon,
|
||||
w.id,
|
||||
w.name
|
||||
ORDER BY
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
@ -169,6 +170,7 @@ func TestAzureAKPKIWithCoderd(t *testing.T) {
|
||||
const email = "alice@coder.com"
|
||||
claims := jwt.MapClaims{
|
||||
"email": email,
|
||||
"sub": uuid.NewString(),
|
||||
}
|
||||
helper := oidctest.NewLoginHelper(owner, fake)
|
||||
user, _ := helper.Login(t, claims)
|
||||
|
@ -388,11 +388,12 @@ func convertProvisionerJobWithQueuePosition(pj database.GetProvisionerJobsByOrga
|
||||
QueueSize: pj.QueueSize,
|
||||
})
|
||||
job.AvailableWorkers = pj.AvailableWorkers
|
||||
job.Metadata = &codersdk.ProvisionerJobMetadata{
|
||||
job.Metadata = codersdk.ProvisionerJobMetadata{
|
||||
TemplateVersionName: pj.TemplateVersionName,
|
||||
TemplateID: pj.TemplateID.UUID,
|
||||
TemplateName: pj.TemplateName,
|
||||
TemplateDisplayName: pj.TemplateDisplayName,
|
||||
TemplateIcon: pj.TemplateIcon,
|
||||
WorkspaceName: pj.WorkspaceName,
|
||||
}
|
||||
if pj.WorkspaceID.Valid {
|
||||
|
@ -84,11 +84,12 @@ func TestProvisionerJobs(t *testing.T) {
|
||||
require.Equal(t, job.ID, job2.ID)
|
||||
|
||||
// Verify that job metadata is correct.
|
||||
assert.Equal(t, job2.Metadata, &codersdk.ProvisionerJobMetadata{
|
||||
assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{
|
||||
TemplateVersionName: version.Name,
|
||||
TemplateID: template.ID,
|
||||
TemplateName: template.Name,
|
||||
TemplateDisplayName: template.DisplayName,
|
||||
TemplateIcon: template.Icon,
|
||||
WorkspaceID: &w.ID,
|
||||
WorkspaceName: w.Name,
|
||||
})
|
||||
@ -105,11 +106,12 @@ func TestProvisionerJobs(t *testing.T) {
|
||||
require.Equal(t, version.Job.ID, job2.ID)
|
||||
|
||||
// Verify that job metadata is correct.
|
||||
assert.Equal(t, job2.Metadata, &codersdk.ProvisionerJobMetadata{
|
||||
assert.Equal(t, job2.Metadata, codersdk.ProvisionerJobMetadata{
|
||||
TemplateVersionName: version.Name,
|
||||
TemplateID: template.ID,
|
||||
TemplateName: template.Name,
|
||||
TemplateDisplayName: template.DisplayName,
|
||||
TemplateIcon: template.Icon,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1112,6 +1112,20 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if idToken.Subject == "" {
|
||||
logger.Error(ctx, "oauth2: missing 'sub' claim field in OIDC token",
|
||||
slog.F("source", "id_token"),
|
||||
slog.F("claim_fields", claimFields(idtokenClaims)),
|
||||
slog.F("blank", blankFields(idtokenClaims)),
|
||||
)
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "OIDC token missing 'sub' claim field or 'sub' claim field is empty.",
|
||||
Detail: "'sub' claim field is required to be unique for all users by a given issue, " +
|
||||
"an empty field is invalid and this authentication attempt is rejected.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "got oidc claims",
|
||||
slog.F("source", "id_token"),
|
||||
slog.F("claim_fields", claimFields(idtokenClaims)),
|
||||
|
@ -72,6 +72,7 @@ func TestOIDCOauthLoginWithExisting(t *testing.T) {
|
||||
"email": "alice@coder.com",
|
||||
"email_verified": true,
|
||||
"preferred_username": username,
|
||||
"sub": uuid.NewString(),
|
||||
}
|
||||
|
||||
helper := oidctest.NewLoginHelper(client, fake)
|
||||
@ -899,10 +900,19 @@ func TestUserOIDC(t *testing.T) {
|
||||
IgnoreEmailVerified bool
|
||||
IgnoreUserInfo bool
|
||||
}{
|
||||
{
|
||||
Name: "NoSub",
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
},
|
||||
AllowSignups: true,
|
||||
StatusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
Name: "EmailOnly",
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
StatusCode: http.StatusOK,
|
||||
@ -915,6 +925,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": false,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
StatusCode: http.StatusForbidden,
|
||||
@ -924,6 +935,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": 3.14159,
|
||||
"email_verified": false,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
StatusCode: http.StatusBadRequest,
|
||||
@ -933,6 +945,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": false,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
StatusCode: http.StatusOK,
|
||||
@ -946,6 +959,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
EmailDomain: []string{
|
||||
@ -958,6 +972,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "cian@coder.com",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
EmailDomain: []string{
|
||||
@ -970,6 +985,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
EmailDomain: []string{
|
||||
@ -982,6 +998,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@KWC.io",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
AssertUser: func(t testing.TB, u codersdk.User) {
|
||||
@ -997,6 +1014,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "colin@gmail.com",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
EmailDomain: []string{
|
||||
@ -1015,6 +1033,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
StatusCode: http.StatusForbidden,
|
||||
},
|
||||
@ -1023,6 +1042,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AssertUser: func(t testing.TB, u codersdk.User) {
|
||||
assert.Equal(t, "kyle", u.Username)
|
||||
@ -1036,6 +1056,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": true,
|
||||
"preferred_username": "hotdog",
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AssertUser: func(t testing.TB, u codersdk.User) {
|
||||
assert.Equal(t, "hotdog", u.Username)
|
||||
@ -1049,6 +1070,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": true,
|
||||
"name": "Hot Dog",
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AssertUser: func(t testing.TB, u codersdk.User) {
|
||||
assert.Equal(t, "Hot Dog", u.Name)
|
||||
@ -1065,6 +1087,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
// However, we should not fail to log someone in if their name is too long.
|
||||
// Just truncate it.
|
||||
"name": strings.Repeat("a", 129),
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
StatusCode: http.StatusOK,
|
||||
@ -1080,6 +1103,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
// Full names must not have leading or trailing whitespace, but this is a
|
||||
// daft reason to fail a login.
|
||||
"name": " Bobby Whitespace ",
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
StatusCode: http.StatusOK,
|
||||
@ -1096,6 +1120,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
"email_verified": true,
|
||||
"name": "Kylium Carbonate",
|
||||
"preferred_username": "kyle@kwc.io",
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AssertUser: func(t testing.TB, u codersdk.User) {
|
||||
assert.Equal(t, "kyle", u.Username)
|
||||
@ -1108,6 +1133,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
Name: "UsernameIsEmail",
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"preferred_username": "kyle@kwc.io",
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AssertUser: func(t testing.TB, u codersdk.User) {
|
||||
assert.Equal(t, "kyle", u.Username)
|
||||
@ -1123,6 +1149,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
"email_verified": true,
|
||||
"preferred_username": "kyle",
|
||||
"picture": "/example.png",
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AssertUser: func(t testing.TB, u codersdk.User) {
|
||||
assert.Equal(t, "/example.png", u.AvatarURL)
|
||||
@ -1136,6 +1163,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "kyle@kwc.io",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
UserInfoClaims: jwt.MapClaims{
|
||||
"preferred_username": "potato",
|
||||
@ -1155,6 +1183,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "coolin@coder.com",
|
||||
"groups": []string{"pingpong"},
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
StatusCode: http.StatusOK,
|
||||
@ -1164,6 +1193,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "internaluser@internal.domain",
|
||||
"email_verified": false,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
UserInfoClaims: jwt.MapClaims{
|
||||
"email": "externaluser@external.domain",
|
||||
@ -1182,6 +1212,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "internaluser@internal.domain",
|
||||
"email_verified": false,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
UserInfoClaims: jwt.MapClaims{
|
||||
"email": 1,
|
||||
@ -1197,6 +1228,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
"email_verified": true,
|
||||
"name": "User McName",
|
||||
"preferred_username": "user",
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
UserInfoClaims: jwt.MapClaims{
|
||||
"email": "user.mcname@external.domain",
|
||||
@ -1216,6 +1248,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: inflateClaims(t, jwt.MapClaims{
|
||||
"email": "user@domain.tld",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
}, 65536),
|
||||
AssertUser: func(t testing.TB, u codersdk.User) {
|
||||
assert.Equal(t, "user", u.Username)
|
||||
@ -1228,6 +1261,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
IDTokenClaims: jwt.MapClaims{
|
||||
"email": "user@domain.tld",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
UserInfoClaims: inflateClaims(t, jwt.MapClaims{}, 65536),
|
||||
AssertUser: func(t testing.TB, u codersdk.User) {
|
||||
@ -1242,6 +1276,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
"iss": "https://mismatch.com",
|
||||
"email": "user@domain.tld",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
},
|
||||
AllowSignups: true,
|
||||
StatusCode: http.StatusBadRequest,
|
||||
@ -1331,6 +1366,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
|
||||
client, resp := fake.AttemptLogin(t, owner, jwt.MapClaims{
|
||||
"email": user.Email,
|
||||
"sub": uuid.NewString(),
|
||||
})
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
@ -1369,6 +1405,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"email": userData.Email,
|
||||
"sub": uuid.NewString(),
|
||||
}
|
||||
var err error
|
||||
user.HTTPClient.Jar, err = cookiejar.New(nil)
|
||||
@ -1439,6 +1476,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"email": userData.Email,
|
||||
"sub": uuid.NewString(),
|
||||
}
|
||||
user.HTTPClient.Jar, err = cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
@ -1509,6 +1547,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
numLogs := len(auditor.AuditLogs())
|
||||
claims := jwt.MapClaims{
|
||||
"email": "jon@coder.com",
|
||||
"sub": uuid.NewString(),
|
||||
}
|
||||
|
||||
userClient, _ := fake.Login(t, client, claims)
|
||||
@ -1629,6 +1668,7 @@ func TestUserOIDC(t *testing.T) {
|
||||
claims := jwt.MapClaims{
|
||||
"email": "user@example.com",
|
||||
"email_verified": true,
|
||||
"sub": uuid.NewString(),
|
||||
}
|
||||
|
||||
// Perform the login
|
||||
@ -1794,6 +1834,7 @@ func TestOIDCSkipIssuer(t *testing.T) {
|
||||
userClient, _ := fake.Login(t, owner, jwt.MapClaims{
|
||||
"iss": secondaryURLString,
|
||||
"email": "alice@coder.com",
|
||||
"sub": uuid.NewString(),
|
||||
})
|
||||
found, err := userClient.User(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
|
@ -831,6 +831,7 @@ func TestPostUsers(t *testing.T) {
|
||||
// Try to log in with OIDC.
|
||||
userClient, _ := fake.Login(t, client, jwt.MapClaims{
|
||||
"email": email,
|
||||
"sub": uuid.NewString(),
|
||||
})
|
||||
|
||||
found, err := userClient.User(ctx, "me")
|
||||
|
27
coderd/util/maps/maps.go
Normal file
27
coderd/util/maps/maps.go
Normal file
@ -0,0 +1,27 @@
|
||||
package maps
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// Subset returns true if all the keys of a are present
|
||||
// in b and have the same values.
|
||||
func Subset[T, U comparable](a, b map[T]U) bool {
|
||||
for ka, va := range a {
|
||||
if vb, ok := b[ka]; !ok || va != vb {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SortedKeys returns the keys of m in sorted order.
|
||||
func SortedKeys[T constraints.Ordered](m map[T]any) (keys []T) {
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
|
||||
return keys
|
||||
}
|
64
coderd/util/maps/maps_test.go
Normal file
64
coderd/util/maps/maps_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
package maps_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/util/maps"
|
||||
)
|
||||
|
||||
func TestSubset(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for idx, tc := range []struct {
|
||||
a map[string]string
|
||||
b map[string]string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
a: nil,
|
||||
b: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
a: map[string]string{},
|
||||
b: map[string]string{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
a: map[string]string{"a": "1", "b": "2"},
|
||||
b: map[string]string{"a": "1", "b": "2"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
a: map[string]string{"a": "1", "b": "2"},
|
||||
b: map[string]string{"a": "1"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
a: map[string]string{"a": "1"},
|
||||
b: map[string]string{"a": "1", "b": "2"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
a: map[string]string{"a": "1", "b": "2"},
|
||||
b: map[string]string{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
a: map[string]string{"a": "1", "b": "2"},
|
||||
b: map[string]string{"a": "1", "b": "3"},
|
||||
expected: false,
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run("#"+strconv.Itoa(idx), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := maps.Subset(tc.a, tc.b)
|
||||
if actual != tc.expected {
|
||||
t.Errorf("expected %v, got %v", tc.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -34,6 +34,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/jwtutils"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
maputil "github.com/coder/coder/v2/coderd/util/maps"
|
||||
"github.com/coder/coder/v2/coderd/wspubsub"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
@ -678,6 +679,99 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
|
||||
httpapi.Write(ctx, rw, http.StatusOK, portsResponse)
|
||||
}
|
||||
|
||||
// @Summary Get running containers for workspace agent
|
||||
// @ID get-running-containers-for-workspace-agent
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Agents
|
||||
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
|
||||
// @Param label query string true "Labels" format(key=value)
|
||||
// @Success 200 {object} codersdk.WorkspaceAgentListContainersResponse
|
||||
// @Router /workspaceagents/{workspaceagent}/containers [get]
|
||||
func (api *API) workspaceAgentListContainers(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
workspaceAgent := httpmw.WorkspaceAgentParam(r)
|
||||
|
||||
labelParam, ok := r.URL.Query()["label"]
|
||||
if !ok {
|
||||
labelParam = []string{}
|
||||
}
|
||||
labels := make(map[string]string, len(labelParam)/2)
|
||||
for _, label := range labelParam {
|
||||
kvs := strings.Split(label, "=")
|
||||
if len(kvs) != 2 {
|
||||
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid label format",
|
||||
Detail: "Labels must be in the format key=value",
|
||||
})
|
||||
return
|
||||
}
|
||||
labels[kvs[0]] = kvs[1]
|
||||
}
|
||||
|
||||
// If the agent is unreachable, the request will hang. Assume that if we
|
||||
// don't get a response after 30s that the agent is unreachable.
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
apiAgent, err := db2sdk.WorkspaceAgent(
|
||||
api.DERPMap(),
|
||||
*api.TailnetCoordinator.Load(),
|
||||
workspaceAgent,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
api.AgentInactiveDisconnectTimeout,
|
||||
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error reading workspace agent.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if apiAgent.Status != codersdk.WorkspaceAgentConnected {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Agent state is %q, it must be in the %q state.", apiAgent.Status, codersdk.WorkspaceAgentConnected),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
agentConn, release, err := api.agentProvider.AgentConn(ctx, workspaceAgent.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error dialing workspace agent.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
// Get a list of containers that the agent is able to detect
|
||||
cts, err := agentConn.ListContainers(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
httpapi.Write(ctx, rw, http.StatusRequestTimeout, codersdk.Response{
|
||||
Message: "Failed to fetch containers from agent.",
|
||||
Detail: "Request timed out.",
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching containers.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Filter in-place by labels
|
||||
cts.Containers = slices.DeleteFunc(cts.Containers, func(ct codersdk.WorkspaceAgentDevcontainer) bool {
|
||||
return !maputil.Subset(labels, ct.Labels)
|
||||
})
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, cts)
|
||||
}
|
||||
|
||||
// @Summary Get connection info for workspace agent
|
||||
// @ID get-connection-info-for-workspace-agent
|
||||
// @Security CoderSessionToken
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -15,9 +16,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/xerrors"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"tailscale.com/tailcfg"
|
||||
@ -25,6 +30,9 @@ import (
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/agent"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers/acmock"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/agenttest"
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
@ -1053,6 +1061,187 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceAgentContainers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// This test will not normally run in CI, but is kept here as a semi-manual
|
||||
// test for local development. Run it as follows:
|
||||
// CODER_TEST_USE_DOCKER=1 go test -run TestWorkspaceAgentContainers/Docker ./coderd
|
||||
t.Run("Docker", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" {
|
||||
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
|
||||
}
|
||||
|
||||
pool, err := dockertest.NewPool("")
|
||||
require.NoError(t, err, "Could not connect to docker")
|
||||
testLabels := map[string]string{
|
||||
"com.coder.test": uuid.New().String(),
|
||||
}
|
||||
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "busybox",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"sleep", "infinity"},
|
||||
Labels: testLabels,
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
require.NoError(t, err, "Could not start test docker container")
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name)
|
||||
})
|
||||
|
||||
// Start another container which we will expect to ignore.
|
||||
ct2, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "busybox",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"sleep", "infinity"},
|
||||
Labels: map[string]string{"com.coder.test": "ignoreme"},
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
require.NoError(t, err, "Could not start second test docker container")
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, pool.Purge(ct2), "Could not purge resource %q", ct2.Container.Name)
|
||||
})
|
||||
|
||||
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{})
|
||||
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OrganizationID: user.OrganizationID,
|
||||
OwnerID: user.UserID,
|
||||
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
|
||||
return agents
|
||||
}).Do()
|
||||
_ = agenttest.New(t, client.URL, r.AgentToken, func(opts *agent.Options) {
|
||||
opts.ContainerLister = agentcontainers.NewDocker(agentexec.DefaultExecer)
|
||||
})
|
||||
resources := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait()
|
||||
require.Len(t, resources, 1, "expected one resource")
|
||||
require.Len(t, resources[0].Agents, 1, "expected one agent")
|
||||
agentID := resources[0].Agents[0].ID
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// If we filter by testLabels, we should only get one container back.
|
||||
res, err := client.WorkspaceAgentListContainers(ctx, agentID, testLabels)
|
||||
require.NoError(t, err, "failed to list containers filtered by test label")
|
||||
require.Len(t, res.Containers, 1, "expected exactly one container")
|
||||
assert.Equal(t, ct.Container.ID, res.Containers[0].ID, "expected container ID to match")
|
||||
assert.Equal(t, "busybox:latest", res.Containers[0].Image, "expected container image to match")
|
||||
assert.Equal(t, ct.Container.Config.Labels, res.Containers[0].Labels, "expected container labels to match")
|
||||
assert.Equal(t, strings.TrimPrefix(ct.Container.Name, "/"), res.Containers[0].FriendlyName, "expected container name to match")
|
||||
assert.True(t, res.Containers[0].Running, "expected container to be running")
|
||||
assert.Equal(t, "running", res.Containers[0].Status, "expected container status to be running")
|
||||
|
||||
// List all containers and ensure we get at least both (there may be more).
|
||||
res, err = client.WorkspaceAgentListContainers(ctx, agentID, nil)
|
||||
require.NoError(t, err, "failed to list all containers")
|
||||
require.NotEmpty(t, res.Containers, "expected to find containers")
|
||||
var found []string
|
||||
for _, c := range res.Containers {
|
||||
found = append(found, c.ID)
|
||||
}
|
||||
require.Contains(t, found, ct.Container.ID, "expected to find first container without label filter")
|
||||
require.Contains(t, found, ct2.Container.ID, "expected to find first container without label filter")
|
||||
})
|
||||
|
||||
// This test will normally run in CI. It uses a mock implementation of
|
||||
// agentcontainers.Lister instead of introducing a hard dependency on Docker.
|
||||
t.Run("Mock", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// begin test fixtures
|
||||
testLabels := map[string]string{
|
||||
"com.coder.test": uuid.New().String(),
|
||||
}
|
||||
testResponse := codersdk.WorkspaceAgentListContainersResponse{
|
||||
Containers: []codersdk.WorkspaceAgentDevcontainer{
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
CreatedAt: dbtime.Now(),
|
||||
FriendlyName: testutil.GetRandomName(t),
|
||||
Image: "busybox:latest",
|
||||
Labels: testLabels,
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentListeningPort{
|
||||
{
|
||||
Network: "tcp",
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
Volumes: map[string]string{
|
||||
"/host": "/container",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
// end test fixtures
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
setupMock func(*acmock.MockLister) (codersdk.WorkspaceAgentListContainersResponse, error)
|
||||
}{
|
||||
{
|
||||
name: "test response",
|
||||
setupMock: func(mcl *acmock.MockLister) (codersdk.WorkspaceAgentListContainersResponse, error) {
|
||||
mcl.EXPECT().List(gomock.Any()).Return(testResponse, nil).Times(1)
|
||||
return testResponse, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error response",
|
||||
setupMock: func(mcl *acmock.MockLister) (codersdk.WorkspaceAgentListContainersResponse, error) {
|
||||
mcl.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{}, assert.AnError).Times(1)
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, assert.AnError
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mcl := acmock.NewMockLister(ctrl)
|
||||
expected, expectedErr := tc.setupMock(mcl)
|
||||
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OrganizationID: user.OrganizationID,
|
||||
OwnerID: user.UserID,
|
||||
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
|
||||
return agents
|
||||
}).Do()
|
||||
_ = agenttest.New(t, client.URL, r.AgentToken, func(opts *agent.Options) {
|
||||
opts.ContainerLister = mcl
|
||||
})
|
||||
resources := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait()
|
||||
require.Len(t, resources, 1, "expected one resource")
|
||||
require.Len(t, resources[0].Agents, 1, "expected one agent")
|
||||
agentID := resources[0].Agents[0].ID
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// List containers and ensure we get the expected mocked response.
|
||||
res, err := client.WorkspaceAgentListContainers(ctx, agentID, nil)
|
||||
if expectedErr != nil {
|
||||
require.Contains(t, err.Error(), expectedErr.Error(), "unexpected error")
|
||||
require.Empty(t, res, "expected empty response")
|
||||
} else {
|
||||
require.NoError(t, err, "failed to list all containers")
|
||||
if diff := cmp.Diff(expected, res); diff != "" {
|
||||
t.Fatalf("unexpected response (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceAgentAppHealth(t *testing.T) {
|
||||
t.Parallel()
|
||||
client, db := coderdtest.NewWithDatabase(t, nil)
|
||||
|
Reference in New Issue
Block a user