feat: add agent metadata (#6614)

This commit is contained in:
Ammar Bandukwala
2023-03-31 15:26:19 -05:00
committed by GitHub
parent c191692751
commit ca4fa81570
62 changed files with 3139 additions and 727 deletions

131
coderd/apidoc/docs.go generated
View File

@ -4263,7 +4263,7 @@ const docTemplate = `{
}
}
},
"/workspaceagents/me/metadata": {
"/workspaceagents/me/manifest": {
"get": {
"security": [
{
@ -4276,18 +4276,62 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Get authorized workspace agent metadata",
"operationId": "get-authorized-workspace-agent-metadata",
"summary": "Get authorized workspace agent manifest",
"operationId": "get-authorized-workspace-agent-manifest",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/agentsdk.Metadata"
"$ref": "#/definitions/agentsdk.Manifest"
}
}
}
}
},
"/workspaceagents/me/metadata/{key}": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Submit workspace agent metadata",
"operationId": "submit-workspace-agent-metadata",
"parameters": [
{
"description": "Workspace agent metadata request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/agentsdk.PostMetadataRequest"
}
},
{
"type": "string",
"format": "string",
"description": "metadata key",
"name": "key",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "Success"
}
},
"x-apidocgen": {
"skip": true
}
}
},
"/workspaceagents/me/report-lifecycle": {
"post": {
"security": [
@ -4663,6 +4707,38 @@ const docTemplate = `{
}
}
},
"/workspaceagents/{workspaceagent}/watch-metadata": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": [
"Agents"
],
"summary": "Watch for workspace agent metadata updates",
"operationId": "watch-for-workspace-agent-metadata-updates",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace agent ID",
"name": "workspaceagent",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success"
}
},
"x-apidocgen": {
"skip": true
}
}
},
"/workspacebuilds/{workspacebuild}": {
"get": {
"security": [
@ -5397,7 +5473,7 @@ const docTemplate = `{
}
}
},
"agentsdk.Metadata": {
"agentsdk.Manifest": {
"type": "object",
"properties": {
"apps": {
@ -5422,6 +5498,12 @@ const docTemplate = `{
"description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.",
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentMetadataDescription"
}
},
"motd_file": {
"type": "string"
},
@ -5473,6 +5555,25 @@ const docTemplate = `{
}
}
},
"agentsdk.PostMetadataRequest": {
"type": "object",
"properties": {
"age": {
"description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.",
"type": "integer"
},
"collected_at": {
"type": "string",
"format": "date-time"
},
"error": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"agentsdk.PostStartupRequest": {
"type": "object",
"properties": {
@ -8915,6 +9016,26 @@ const docTemplate = `{
}
}
},
"codersdk.WorkspaceAgentMetadataDescription": {
"type": "object",
"properties": {
"display_name": {
"type": "string"
},
"interval": {
"type": "integer"
},
"key": {
"type": "string"
},
"script": {
"type": "string"
},
"timeout": {
"type": "integer"
}
}
},
"codersdk.WorkspaceAgentStartupLog": {
"type": "object",
"properties": {

View File

@ -3747,7 +3747,7 @@
}
}
},
"/workspaceagents/me/metadata": {
"/workspaceagents/me/manifest": {
"get": {
"security": [
{
@ -3756,18 +3756,58 @@
],
"produces": ["application/json"],
"tags": ["Agents"],
"summary": "Get authorized workspace agent metadata",
"operationId": "get-authorized-workspace-agent-metadata",
"summary": "Get authorized workspace agent manifest",
"operationId": "get-authorized-workspace-agent-manifest",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/agentsdk.Metadata"
"$ref": "#/definitions/agentsdk.Manifest"
}
}
}
}
},
"/workspaceagents/me/metadata/{key}": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"tags": ["Agents"],
"summary": "Submit workspace agent metadata",
"operationId": "submit-workspace-agent-metadata",
"parameters": [
{
"description": "Workspace agent metadata request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/agentsdk.PostMetadataRequest"
}
},
{
"type": "string",
"format": "string",
"description": "metadata key",
"name": "key",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "Success"
}
},
"x-apidocgen": {
"skip": true
}
}
},
"/workspaceagents/me/report-lifecycle": {
"post": {
"security": [
@ -4101,6 +4141,36 @@
}
}
},
"/workspaceagents/{workspaceagent}/watch-metadata": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": ["Agents"],
"summary": "Watch for workspace agent metadata updates",
"operationId": "watch-for-workspace-agent-metadata-updates",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace agent ID",
"name": "workspaceagent",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success"
}
},
"x-apidocgen": {
"skip": true
}
}
},
"/workspacebuilds/{workspacebuild}": {
"get": {
"security": [
@ -4758,7 +4828,7 @@
}
}
},
"agentsdk.Metadata": {
"agentsdk.Manifest": {
"type": "object",
"properties": {
"apps": {
@ -4783,6 +4853,12 @@
"description": "GitAuthConfigs stores the number of Git configurations\nthe Coder deployment has. If this number is \u003e0, we\nset up special configuration in the workspace.",
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentMetadataDescription"
}
},
"motd_file": {
"type": "string"
},
@ -4834,6 +4910,25 @@
}
}
},
"agentsdk.PostMetadataRequest": {
"type": "object",
"properties": {
"age": {
"description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.",
"type": "integer"
},
"collected_at": {
"type": "string",
"format": "date-time"
},
"error": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"agentsdk.PostStartupRequest": {
"type": "object",
"properties": {
@ -8043,6 +8138,26 @@
}
}
},
"codersdk.WorkspaceAgentMetadataDescription": {
"type": "object",
"properties": {
"display_name": {
"type": "string"
},
"interval": {
"type": "integer"
},
"key": {
"type": "string"
},
"script": {
"type": "string"
},
"timeout": {
"type": "integer"
}
}
},
"codersdk.WorkspaceAgentStartupLog": {
"type": "object",
"properties": {

View File

@ -608,7 +608,10 @@ func New(options *Options) *API {
r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity)
r.Route("/me", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
r.Get("/metadata", api.workspaceAgentMetadata)
r.Get("/manifest", api.workspaceAgentManifest)
// This route is deprecated and will be removed in a future release.
// New agents will use /me/manifest instead.
r.Get("/metadata", api.workspaceAgentManifest)
r.Post("/startup", api.postWorkspaceAgentStartup)
r.Patch("/startup-logs", api.patchWorkspaceAgentStartupLogs)
r.Post("/app-health", api.postWorkspaceAppHealth)
@ -617,6 +620,7 @@ func New(options *Options) *API {
r.Get("/coordinate", api.workspaceAgentCoordinate)
r.Post("/report-stats", api.workspaceAgentReportStats)
r.Post("/report-lifecycle", api.workspaceAgentReportLifecycle)
r.Post("/metadata/{key}", api.workspaceAgentPostMetadata)
})
// No middleware on the PTY endpoint since it uses workspace
// application auth and tickets.
@ -628,6 +632,8 @@ func New(options *Options) *API {
httpmw.ExtractWorkspaceParam(options.Database),
)
r.Get("/", api.workspaceAgent)
r.Get("/watch-metadata", api.watchWorkspaceAgentMetadata)
r.Get("/pty", api.workspaceAgentPTY)
r.Get("/startup-logs", api.workspaceAgentStartupLogs)
r.Get("/listening-ports", api.workspaceAgentListeningPorts)
r.Get("/connection", api.workspaceAgentConnection)

View File

@ -160,6 +160,11 @@ func VerifySwaggerDefinitions(t *testing.T, router chi.Router, swaggerComments [
t.Run(method+" "+route, func(t *testing.T) {
t.Parallel()
// This route is for compatibility purposes and is not documented.
if route == "/workspaceagents/me/metadata" {
return
}
c := findSwaggerCommentByMethodAndRoute(swaggerComments, method, route)
assert.NotNil(t, c, "Missing @Router annotation")
if c == nil {

View File

@ -1564,6 +1564,44 @@ func (q *querier) InsertWorkspaceAgentStat(ctx context.Context, arg database.Ins
return q.db.InsertWorkspaceAgentStat(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentMetadata(ctx context.Context, arg database.InsertWorkspaceAgentMetadataParams) error {
// We don't check for workspace ownership here since the agent metadata may
// be associated with an orphaned agent used by a dry run build.
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
return err
}
return q.db.InsertWorkspaceAgentMetadata(ctx, arg)
}
func (q *querier) UpdateWorkspaceAgentMetadata(ctx context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error {
workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.WorkspaceAgentID)
if err != nil {
return err
}
err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace)
if err != nil {
return err
}
return q.db.UpdateWorkspaceAgentMetadata(ctx, arg)
}
func (q *querier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) {
workspace, err := q.db.GetWorkspaceByAgentID(ctx, workspaceAgentID)
if err != nil {
return nil, err
}
err = q.authorizeContext(ctx, rbac.ActionRead, workspace)
if err != nil {
return nil, err
}
return q.db.GetWorkspaceAgentMetadata(ctx, workspaceAgentID)
}
func (q *querier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg database.UpdateWorkspaceAppHealthByIDParams) error {
// TODO: This is a workspace agent operation. Should users be able to query this?
workspace, err := q.db.GetWorkspaceByWorkspaceAppID(ctx, arg.ID)

View File

@ -124,6 +124,7 @@ type data struct {
templateVersionVariables []database.TemplateVersionVariable
templates []database.Template
workspaceAgents []database.WorkspaceAgent
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
workspaceAgentLogs []database.WorkspaceAgentStartupLog
workspaceApps []database.WorkspaceApp
workspaceBuilds []database.WorkspaceBuild
@ -2741,6 +2742,60 @@ func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP
return key, nil
}
func (q *fakeQuerier) UpdateWorkspaceAgentMetadata(_ context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
//nolint:gosimple
updated := database.WorkspaceAgentMetadatum{
WorkspaceAgentID: arg.WorkspaceAgentID,
Key: arg.Key,
Value: arg.Value,
Error: arg.Error,
CollectedAt: arg.CollectedAt,
}
for i, m := range q.workspaceAgentMetadata {
if m.WorkspaceAgentID == arg.WorkspaceAgentID && m.Key == arg.Key {
q.workspaceAgentMetadata[i] = updated
return nil
}
}
return nil
}
func (q *fakeQuerier) InsertWorkspaceAgentMetadata(_ context.Context, arg database.InsertWorkspaceAgentMetadataParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
//nolint:gosimple
metadatum := database.WorkspaceAgentMetadatum{
WorkspaceAgentID: arg.WorkspaceAgentID,
Script: arg.Script,
DisplayName: arg.DisplayName,
Key: arg.Key,
Timeout: arg.Timeout,
Interval: arg.Interval,
}
q.workspaceAgentMetadata = append(q.workspaceAgentMetadata, metadatum)
return nil
}
func (q *fakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
metadata := make([]database.WorkspaceAgentMetadatum, 0)
for _, m := range q.workspaceAgentMetadata {
if m.WorkspaceAgentID == workspaceAgentID {
metadata = append(metadata, m)
}
}
return metadata, nil
}
func (q *fakeQuerier) InsertFile(_ context.Context, arg database.InsertFileParams) (database.File, error) {
if err := validateDatabaseType(arg); err != nil {
return database.File{}, err

View File

@ -475,6 +475,18 @@ CREATE TABLE users (
last_seen_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL
);
CREATE UNLOGGED TABLE workspace_agent_metadata (
workspace_agent_id uuid NOT NULL,
display_name character varying(127) NOT NULL,
key character varying(127) NOT NULL,
script character varying(65535) NOT NULL,
value character varying(65535) DEFAULT ''::character varying NOT NULL,
error character varying(65535) DEFAULT ''::character varying NOT NULL,
timeout bigint NOT NULL,
"interval" bigint NOT NULL,
collected_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL
);
CREATE TABLE workspace_agent_startup_logs (
agent_id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -756,6 +768,9 @@ ALTER TABLE ONLY user_links
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_agent_metadata
ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key);
ALTER TABLE ONLY workspace_agent_startup_logs
ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id);
@ -894,6 +909,9 @@ ALTER TABLE ONLY templates
ALTER TABLE ONLY user_links
ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
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;
ALTER TABLE ONLY workspace_agent_startup_logs
ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;

View File

@ -0,0 +1 @@
DROP TABLE workspace_agent_metadata;

View File

@ -0,0 +1,16 @@
-- This table is UNLOGGED because it is very update-heavy and the the data
-- is not valuable enough to justify the overhead of WAL logging. This should
-- give us a ~70% improvement in write throughput.
CREATE UNLOGGED TABLE workspace_agent_metadata (
workspace_agent_id uuid NOT NULL,
display_name varchar(127) NOT NULL,
key varchar(127) NOT NULL,
script varchar(65535) NOT NULL,
value varchar(65535) NOT NULL DEFAULT '',
error varchar(65535) NOT NULL DEFAULT '',
timeout bigint NOT NULL,
interval bigint NOT NULL,
collected_at timestamp with time zone NOT NULL DEFAULT '0001-01-01 00:00:00+00',
PRIMARY KEY (workspace_agent_id, key),
FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,18 @@
INSERT INTO
workspace_agent_metadata (
workspace_agent_id,
display_name,
key,
script,
timeout,
interval
)
VALUES
(
'45e89705-e09d-4850-bcec-f9a937f5d78d',
'a h e m',
'ahem',
'rm -rf',
3,
1
);

View File

@ -1575,6 +1575,18 @@ type WorkspaceAgent struct {
StartupLogsOverflowed bool `db:"startup_logs_overflowed" json:"startup_logs_overflowed"`
}
type WorkspaceAgentMetadatum struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
DisplayName string `db:"display_name" json:"display_name"`
Key string `db:"key" json:"key"`
Script string `db:"script" json:"script"`
Value string `db:"value" json:"value"`
Error string `db:"error" json:"error"`
Timeout int64 `db:"timeout" json:"timeout"`
Interval int64 `db:"interval" json:"interval"`
CollectedAt time.Time `db:"collected_at" json:"collected_at"`
}
type WorkspaceAgentStartupLog struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`

View File

@ -126,6 +126,7 @@ type sqlcQuerier interface {
GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentMetadatum, error)
GetWorkspaceAgentStartupLogsAfter(ctx context.Context, arg GetWorkspaceAgentStartupLogsAfterParams) ([]WorkspaceAgentStartupLog, error)
GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error)
GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error)
@ -185,6 +186,7 @@ type sqlcQuerier interface {
InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error)
InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error)
InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error)
InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error
InsertWorkspaceAgentStartupLogs(ctx context.Context, arg InsertWorkspaceAgentStartupLogsParams) ([]WorkspaceAgentStartupLog, error)
InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error)
InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error)
@ -229,6 +231,7 @@ type sqlcQuerier interface {
UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error)
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error
UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error
UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error
UpdateWorkspaceAgentStartupLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentStartupLogOverflowByIDParams) error
UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error

View File

@ -5297,6 +5297,48 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst
return i, err
}
const getWorkspaceAgentMetadata = `-- name: GetWorkspaceAgentMetadata :many
SELECT
workspace_agent_id, display_name, key, script, value, error, timeout, interval, collected_at
FROM
workspace_agent_metadata
WHERE
workspace_agent_id = $1
`
func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentMetadatum, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentMetadata, workspaceAgentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentMetadatum
for rows.Next() {
var i WorkspaceAgentMetadatum
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.DisplayName,
&i.Key,
&i.Script,
&i.Value,
&i.Error,
&i.Timeout,
&i.Interval,
&i.CollectedAt,
); 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 getWorkspaceAgentStartupLogsAfter = `-- name: GetWorkspaceAgentStartupLogsAfter :many
SELECT
agent_id, created_at, output, id
@ -5651,6 +5693,41 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
return i, err
}
const insertWorkspaceAgentMetadata = `-- name: InsertWorkspaceAgentMetadata :exec
INSERT INTO
workspace_agent_metadata (
workspace_agent_id,
display_name,
key,
script,
timeout,
interval
)
VALUES
($1, $2, $3, $4, $5, $6)
`
type InsertWorkspaceAgentMetadataParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
DisplayName string `db:"display_name" json:"display_name"`
Key string `db:"key" json:"key"`
Script string `db:"script" json:"script"`
Timeout int64 `db:"timeout" json:"timeout"`
Interval int64 `db:"interval" json:"interval"`
}
func (q *sqlQuerier) InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error {
_, err := q.db.ExecContext(ctx, insertWorkspaceAgentMetadata,
arg.WorkspaceAgentID,
arg.DisplayName,
arg.Key,
arg.Script,
arg.Timeout,
arg.Interval,
)
return err
}
const insertWorkspaceAgentStartupLogs = `-- name: InsertWorkspaceAgentStartupLogs :many
WITH new_length AS (
UPDATE workspace_agents SET
@ -5758,6 +5835,37 @@ func (q *sqlQuerier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context,
return err
}
const updateWorkspaceAgentMetadata = `-- name: UpdateWorkspaceAgentMetadata :exec
UPDATE
workspace_agent_metadata
SET
value = $3,
error = $4,
collected_at = $5
WHERE
workspace_agent_id = $1
AND key = $2
`
type UpdateWorkspaceAgentMetadataParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
Error string `db:"error" json:"error"`
CollectedAt time.Time `db:"collected_at" json:"collected_at"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentMetadata,
arg.WorkspaceAgentID,
arg.Key,
arg.Value,
arg.Error,
arg.CollectedAt,
)
return err
}
const updateWorkspaceAgentStartupByID = `-- name: UpdateWorkspaceAgentStartupByID :exec
UPDATE
workspace_agents

View File

@ -94,6 +94,38 @@ SET
WHERE
id = $1;
-- name: InsertWorkspaceAgentMetadata :exec
INSERT INTO
workspace_agent_metadata (
workspace_agent_id,
display_name,
key,
script,
timeout,
interval
)
VALUES
($1, $2, $3, $4, $5, $6);
-- name: UpdateWorkspaceAgentMetadata :exec
UPDATE
workspace_agent_metadata
SET
value = $3,
error = $4,
collected_at = $5
WHERE
workspace_agent_id = $1
AND key = $2;
-- name: GetWorkspaceAgentMetadata :many
SELECT
*
FROM
workspace_agent_metadata
WHERE
workspace_agent_id = $1;
-- name: UpdateWorkspaceAgentStartupLogOverflowByID :exec
UPDATE
workspace_agents

View File

@ -1277,6 +1277,21 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
}
snapshot.WorkspaceAgents = append(snapshot.WorkspaceAgents, telemetry.ConvertWorkspaceAgent(dbAgent))
for _, md := range prAgent.Metadata {
p := database.InsertWorkspaceAgentMetadataParams{
WorkspaceAgentID: agentID,
DisplayName: md.DisplayName,
Script: md.Script,
Key: md.Key,
Timeout: md.Timeout,
Interval: md.Interval,
}
err := db.InsertWorkspaceAgentMetadata(ctx, p)
if err != nil {
return xerrors.Errorf("insert agent metadata: %w, params: %+v", err, p)
}
}
for _, app := range prAgent.Apps {
slug := app.Slug
if slug == "" {

View File

@ -21,6 +21,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"go.opentelemetry.io/otel/trace"
"golang.org/x/exp/slices"
"golang.org/x/mod/semver"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
@ -76,14 +77,14 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, apiAgent)
}
// @Summary Get authorized workspace agent metadata
// @ID get-authorized-workspace-agent-metadata
// @Summary Get authorized workspace agent manifest
// @ID get-authorized-workspace-agent-manifest
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Success 200 {object} agentsdk.Metadata
// @Router /workspaceagents/me/metadata [get]
func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) {
// @Success 200 {object} agentsdk.Manifest
// @Router /workspaceagents/me/manifest [get]
func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
apiAgent, err := convertWorkspaceAgent(
@ -105,6 +106,16 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
})
return
}
metadata, err := api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace agent metadata.",
Detail: err.Error(),
})
return
}
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@ -149,7 +160,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
vscodeProxyURI += fmt.Sprintf(":%s", api.AccessURL.Port())
}
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Metadata{
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Manifest{
Apps: convertApps(dbApps),
DERPMap: api.DERPMap,
GitAuthConfigs: len(api.GitAuthConfigs),
@ -161,6 +172,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
StartupScriptTimeout: time.Duration(apiAgent.StartupScriptTimeoutSeconds) * time.Second,
ShutdownScript: apiAgent.ShutdownScript,
ShutdownScriptTimeout: time.Duration(apiAgent.ShutdownScriptTimeoutSeconds) * time.Second,
Metadata: convertWorkspaceAgentMetadataDesc(metadata),
})
}
@ -1133,6 +1145,20 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp {
return apps
}
func convertWorkspaceAgentMetadataDesc(mds []database.WorkspaceAgentMetadatum) []codersdk.WorkspaceAgentMetadataDescription {
metadata := make([]codersdk.WorkspaceAgentMetadataDescription, 0)
for _, datum := range mds {
metadata = append(metadata, codersdk.WorkspaceAgentMetadataDescription{
DisplayName: datum.DisplayName,
Key: datum.Key,
Script: datum.Script,
Interval: datum.Interval,
Timeout: datum.Timeout,
})
}
return metadata
}
func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordinator, dbAgent database.WorkspaceAgent, apps []codersdk.WorkspaceApp, agentInactiveDisconnectTimeout time.Duration, agentFallbackTroubleshootingURL string) (codersdk.WorkspaceAgent, error) {
var envs map[string]string
if dbAgent.EnvironmentVariables.Valid {
@ -1298,6 +1324,219 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
})
}
// @Summary Submit workspace agent metadata
// @ID submit-workspace-agent-metadata
// @Security CoderSessionToken
// @Accept json
// @Tags Agents
// @Param request body agentsdk.PostMetadataRequest true "Workspace agent metadata request"
// @Param key path string true "metadata key" format(string)
// @Success 204 "Success"
// @Router /workspaceagents/me/metadata/{key} [post]
// @x-apidocgen {"skip": true}
func (api *API) workspaceAgentPostMetadata(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req agentsdk.PostMetadataRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
workspaceAgent := httpmw.WorkspaceAgent(r)
workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to get workspace.",
Detail: err.Error(),
})
return
}
key := chi.URLParam(r, "key")
const (
maxValueLen = 32 << 10
maxErrorLen = maxValueLen
)
metadataError := req.Error
// We overwrite the error if the provided payload is too long.
if len(req.Value) > maxValueLen {
metadataError = fmt.Sprintf("value of %d bytes exceeded %d bytes", len(req.Value), maxValueLen)
req.Value = req.Value[:maxValueLen]
}
if len(req.Error) > maxErrorLen {
metadataError = fmt.Sprintf("error of %d bytes exceeded %d bytes", len(req.Error), maxErrorLen)
req.Error = req.Error[:maxErrorLen]
}
datum := database.UpdateWorkspaceAgentMetadataParams{
WorkspaceAgentID: workspaceAgent.ID,
// We don't want a misconfigured agent to fill the database.
Key: key,
Value: req.Value,
Error: metadataError,
// We ignore the CollectedAt from the agent to avoid bugs caused by
// clock skew.
CollectedAt: time.Now(),
}
err = api.Database.UpdateWorkspaceAgentMetadata(ctx, datum)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
api.Logger.Debug(
ctx, "accepted metadata report",
slog.F("agent", workspaceAgent.ID),
slog.F("workspace", workspace.ID),
slog.F("collected_at", datum.CollectedAt),
slog.F("key", datum.Key),
)
err = api.Pubsub.Publish(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), []byte(datum.Key))
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
}
// @Summary Watch for workspace agent metadata updates
// @ID watch-for-workspace-agent-metadata-updates
// @Security CoderSessionToken
// @Tags Agents
// @Success 200 "Success"
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Router /workspaceagents/{workspaceagent}/watch-metadata [get]
// @x-apidocgen {"skip": true}
func (api *API) watchWorkspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
workspaceAgent = httpmw.WorkspaceAgentParam(r)
)
sendEvent, senderClosed, err := httpapi.ServerSentEventSender(rw, r)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error setting up server-sent events.",
Detail: err.Error(),
})
return
}
// Prevent handler from returning until the sender is closed.
defer func() {
<-senderClosed
}()
// We don't want this intentionally long request to skew our tracing
// reports.
ctx = trace.ContextWithSpan(ctx, tracing.NoopSpan)
const refreshInterval = time.Second * 5
refreshTicker := time.NewTicker(refreshInterval)
defer refreshTicker.Stop()
var (
lastDBMetaMu sync.Mutex
lastDBMeta []database.WorkspaceAgentMetadatum
)
sendMetadata := func(pull bool) {
lastDBMetaMu.Lock()
defer lastDBMetaMu.Unlock()
var err error
if pull {
// We always use the original Request context because it contains
// the RBAC actor.
lastDBMeta, err = api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID)
if err != nil {
_ = sendEvent(ctx, codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError,
Data: codersdk.Response{
Message: "Internal error getting metadata.",
Detail: err.Error(),
},
})
return
}
slices.SortFunc(lastDBMeta, func(i, j database.WorkspaceAgentMetadatum) bool {
return i.Key < j.Key
})
// Avoid sending refresh if the client is about to get a
// fresh update.
refreshTicker.Reset(refreshInterval)
}
_ = sendEvent(ctx, codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeData,
Data: convertWorkspaceAgentMetadata(lastDBMeta),
})
}
// Send initial metadata.
sendMetadata(true)
// Send metadata on updates.
cancelSub, err := api.Pubsub.Subscribe(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), func(_ context.Context, _ []byte) {
sendMetadata(true)
})
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
defer cancelSub()
for {
select {
case <-senderClosed:
return
case <-refreshTicker.C:
break
}
// Avoid spamming the DB with reads we know there are no updates. We want
// to continue sending updates to the frontend so that "Result.Age"
// is always accurate. This way, the frontend doesn't need to
// sync its own clock with the backend.
sendMetadata(false)
}
}
func convertWorkspaceAgentMetadata(db []database.WorkspaceAgentMetadatum) []codersdk.WorkspaceAgentMetadata {
// An empty array is easier for clients to handle than a null.
result := []codersdk.WorkspaceAgentMetadata{}
for _, datum := range db {
result = append(result, codersdk.WorkspaceAgentMetadata{
Result: codersdk.WorkspaceAgentMetadataResult{
Value: datum.Value,
Error: datum.Error,
CollectedAt: datum.CollectedAt,
Age: int64(time.Since(datum.CollectedAt).Seconds()),
},
Description: codersdk.WorkspaceAgentMetadataDescription{
DisplayName: datum.DisplayName,
Key: datum.Key,
Script: datum.Script,
Interval: datum.Interval,
Timeout: datum.Timeout,
},
})
}
return result
}
func watchWorkspaceAgentMetadataChannel(id uuid.UUID) string {
return "workspace_agent_metadata:" + id.String()
}
// @Summary Submit workspace agent lifecycle state
// @ID submit-workspace-agent-lifecycle-state
// @Security CoderSessionToken

View File

@ -831,10 +831,10 @@ func TestWorkspaceAgentAppHealth(t *testing.T) {
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
metadata, err := agentClient.Metadata(ctx)
manifest, err := agentClient.Manifest(ctx)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, metadata.Apps[0].Health)
require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, metadata.Apps[1].Health)
require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, manifest.Apps[0].Health)
require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, manifest.Apps[1].Health)
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{})
require.Error(t, err)
// empty
@ -843,37 +843,37 @@ func TestWorkspaceAgentAppHealth(t *testing.T) {
// healthcheck disabled
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
metadata.Apps[0].ID: codersdk.WorkspaceAppHealthInitializing,
manifest.Apps[0].ID: codersdk.WorkspaceAppHealthInitializing,
},
})
require.Error(t, err)
// invalid value
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
metadata.Apps[1].ID: codersdk.WorkspaceAppHealth("bad-value"),
manifest.Apps[1].ID: codersdk.WorkspaceAppHealth("bad-value"),
},
})
require.Error(t, err)
// update to healthy
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
metadata.Apps[1].ID: codersdk.WorkspaceAppHealthHealthy,
manifest.Apps[1].ID: codersdk.WorkspaceAppHealthHealthy,
},
})
require.NoError(t, err)
metadata, err = agentClient.Metadata(ctx)
manifest, err = agentClient.Manifest(ctx)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAppHealthHealthy, metadata.Apps[1].Health)
require.EqualValues(t, codersdk.WorkspaceAppHealthHealthy, manifest.Apps[1].Health)
// update to unhealthy
err = agentClient.PostAppHealth(ctx, agentsdk.PostAppHealthsRequest{
Healths: map[uuid.UUID]codersdk.WorkspaceAppHealth{
metadata.Apps[1].ID: codersdk.WorkspaceAppHealthUnhealthy,
manifest.Apps[1].ID: codersdk.WorkspaceAppHealthUnhealthy,
},
})
require.NoError(t, err)
metadata, err = agentClient.Metadata(ctx)
manifest, err = agentClient.Manifest(ctx)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAppHealthUnhealthy, metadata.Apps[1].Health)
require.EqualValues(t, codersdk.WorkspaceAppHealthUnhealthy, manifest.Apps[1].Health)
}
// nolint:bodyclose
@ -1262,3 +1262,155 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) {
}
})
}
func TestWorkspaceAgent_Metadata(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
})
user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Metadata: []*proto.Agent_Metadata{
{
DisplayName: "First Meta",
Key: "foo1",
Script: "echo hi",
Interval: 10,
Timeout: 3,
},
{
DisplayName: "Second Meta",
Key: "foo2",
Script: "echo howdy",
Interval: 10,
Timeout: 3,
},
{
DisplayName: "TooLong",
Key: "foo3",
Script: "echo howdy",
Interval: 10,
Timeout: 3,
},
},
Id: uuid.NewString(),
Auth: &proto.Agent_Token{
Token: authToken,
},
}},
}},
},
},
}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
for _, res := range workspace.LatestBuild.Resources {
for _, a := range res.Agents {
require.Equal(t, codersdk.WorkspaceAgentLifecycleCreated, a.LifecycleState)
}
}
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
ctx := testutil.Context(t, testutil.WaitMedium)
manifest, err := agentClient.Manifest(ctx)
require.NoError(t, err)
// Verify manifest API response.
require.Equal(t, "First Meta", manifest.Metadata[0].DisplayName)
require.Equal(t, "foo1", manifest.Metadata[0].Key)
require.Equal(t, "echo hi", manifest.Metadata[0].Script)
require.EqualValues(t, 10, manifest.Metadata[0].Interval)
require.EqualValues(t, 3, manifest.Metadata[0].Timeout)
post := func(key string, mr codersdk.WorkspaceAgentMetadataResult) {
err := agentClient.PostMetadata(ctx, key, mr)
require.NoError(t, err, "post metadata", t)
}
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "get workspace")
agentID := workspace.LatestBuild.Resources[0].Agents[0].ID
var update []codersdk.WorkspaceAgentMetadata
check := func(want codersdk.WorkspaceAgentMetadataResult, got codersdk.WorkspaceAgentMetadata) {
require.WithinDuration(t, want.CollectedAt, got.Result.CollectedAt, time.Second)
require.WithinDuration(
t, time.Now(), got.Result.CollectedAt.Add(time.Duration(got.Result.Age)*time.Second), time.Millisecond*500,
)
require.Equal(t, want.Value, got.Result.Value)
require.Equal(t, want.Error, got.Result.Error)
}
wantMetadata1 := codersdk.WorkspaceAgentMetadataResult{
CollectedAt: time.Now(),
Value: "bar",
}
// Initial post must come before the Watch is established.
post("foo1", wantMetadata1)
updates, errors := client.WatchWorkspaceAgentMetadata(ctx, agentID)
recvUpdate := func() []codersdk.WorkspaceAgentMetadata {
select {
case err := <-errors:
t.Fatalf("error watching metadata: %v", err)
return nil
case update := <-updates:
return update
}
}
update = recvUpdate()
require.Len(t, update, 3)
check(wantMetadata1, update[0])
// The second metadata result is not yet posted.
require.Zero(t, update[1].Result.CollectedAt)
wantMetadata2 := wantMetadata1
post("foo2", wantMetadata2)
update = recvUpdate()
require.Len(t, update, 3)
check(wantMetadata1, update[0])
check(wantMetadata2, update[1])
wantMetadata1.Error = "error"
post("foo1", wantMetadata1)
update = recvUpdate()
require.Len(t, update, 3)
check(wantMetadata1, update[0])
const maxValueLen = 32 << 10
tooLongValueMetadata := wantMetadata1
tooLongValueMetadata.Value = strings.Repeat("a", maxValueLen*2)
tooLongValueMetadata.Error = ""
tooLongValueMetadata.CollectedAt = time.Now()
post("foo3", tooLongValueMetadata)
got := recvUpdate()[2]
require.Len(t, got.Result.Value, maxValueLen)
require.NotEmpty(t, got.Result.Error)
unknownKeyMetadata := wantMetadata1
err = agentClient.PostMetadata(ctx, "unknown", unknownKeyMetadata)
require.NoError(t, err)
}

View File

@ -273,7 +273,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
if appHost != "" {
metadata, err := agentClient.Metadata(context.Background())
manifest, err := agentClient.Manifest(context.Background())
require.NoError(t, err)
proxyURL := fmt.Sprintf(
"http://{{port}}--%s--%s--%s%s",
@ -285,7 +285,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
if client.URL.Port() != "" {
proxyURL += fmt.Sprintf(":%s", client.URL.Port())
}
require.Equal(t, proxyURL, metadata.VSCodePortProxyURI)
require.Equal(t, proxyURL, manifest.VSCodePortProxyURI)
}
agentCloser := agent.New(agent.Options{
Client: agentClient,

View File

@ -41,7 +41,7 @@ func TestCache(t *testing.T) {
t.Run("Same", func(t *testing.T) {
t.Parallel()
cache := wsconncache.New(func(id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) {
return setupAgent(t, agentsdk.Metadata{}, 0), nil
return setupAgent(t, agentsdk.Manifest{}, 0), nil
}, 0)
defer func() {
_ = cache.Close()
@ -57,7 +57,7 @@ func TestCache(t *testing.T) {
called := atomic.NewInt32(0)
cache := wsconncache.New(func(id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) {
called.Add(1)
return setupAgent(t, agentsdk.Metadata{}, 0), nil
return setupAgent(t, agentsdk.Manifest{}, 0), nil
}, time.Microsecond)
defer func() {
_ = cache.Close()
@ -75,7 +75,7 @@ func TestCache(t *testing.T) {
t.Run("NoExpireWhenLocked", func(t *testing.T) {
t.Parallel()
cache := wsconncache.New(func(id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) {
return setupAgent(t, agentsdk.Metadata{}, 0), nil
return setupAgent(t, agentsdk.Manifest{}, 0), nil
}, time.Microsecond)
defer func() {
_ = cache.Close()
@ -108,7 +108,7 @@ func TestCache(t *testing.T) {
go server.Serve(random)
cache := wsconncache.New(func(id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) {
return setupAgent(t, agentsdk.Metadata{}, 0), nil
return setupAgent(t, agentsdk.Manifest{}, 0), nil
}, time.Microsecond)
defer func() {
_ = cache.Close()
@ -154,10 +154,10 @@ func TestCache(t *testing.T) {
})
}
func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Duration) *codersdk.WorkspaceAgentConn {
func setupAgent(t *testing.T, manifest agentsdk.Manifest, ptyTimeout time.Duration) *codersdk.WorkspaceAgentConn {
t.Helper()
metadata.DERPMap = tailnettest.RunDERPAndSTUN(t)
manifest.DERPMap = tailnettest.RunDERPAndSTUN(t)
coordinator := tailnet.NewCoordinator()
t.Cleanup(func() {
@ -168,7 +168,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Durati
Client: &client{
t: t,
agentID: agentID,
metadata: metadata,
manifest: manifest,
coordinator: coordinator,
},
Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelInfo),
@ -179,7 +179,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Durati
})
conn, err := tailnet.NewConn(&tailnet.Options{
Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
DERPMap: metadata.DERPMap,
DERPMap: manifest.DERPMap,
Logger: slogtest.Make(t, nil).Named("tailnet").Leveled(slog.LevelDebug),
})
require.NoError(t, err)
@ -211,12 +211,12 @@ func setupAgent(t *testing.T, metadata agentsdk.Metadata, ptyTimeout time.Durati
type client struct {
t *testing.T
agentID uuid.UUID
metadata agentsdk.Metadata
manifest agentsdk.Manifest
coordinator tailnet.Coordinator
}
func (c *client) Metadata(_ context.Context) (agentsdk.Metadata, error) {
return c.metadata, nil
func (c *client) Manifest(_ context.Context) (agentsdk.Manifest, error) {
return c.manifest, nil
}
func (c *client) Listen(_ context.Context) (net.Conn, error) {
@ -246,6 +246,10 @@ func (*client) PostAppHealth(_ context.Context, _ agentsdk.PostAppHealthsRequest
return nil
}
func (*client) PostMetadata(_ context.Context, _ string, _ agentsdk.PostMetadataRequest) error {
return nil
}
func (*client) PostStartup(_ context.Context, _ agentsdk.PostStartupRequest) error {
return nil
}