feat: Add workspace agent lifecycle state reporting (#5785)

This commit is contained in:
Mathias Fredriksson
2023-01-24 14:24:27 +02:00
committed by GitHub
parent dbfeb5630c
commit 138887de7e
34 changed files with 1596 additions and 634 deletions

87
coderd/apidoc/docs.go generated
View File

@ -3973,6 +3973,42 @@ const docTemplate = `{
}
}
},
"/workspaceagents/me/report-lifecycle": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Submit workspace agent lifecycle state",
"operationId": "submit-workspace-agent-lifecycle-state",
"parameters": [
{
"description": "Workspace agent lifecycle request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.PostWorkspaceAgentLifecycleRequest"
}
}
],
"responses": {
"204": {
"description": "Success"
}
},
"x-apidocgen": {
"skip": true
}
}
},
"/workspaceagents/me/report-stats": {
"post": {
"security": [
@ -6723,6 +6759,14 @@ const docTemplate = `{
"ParameterSourceSchemeData"
]
},
"codersdk.PostWorkspaceAgentLifecycleRequest": {
"type": "object",
"properties": {
"state": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle"
}
}
},
"codersdk.PostWorkspaceAgentVersionRequest": {
"description": "x-apidocgen:skip",
"type": "object",
@ -7651,6 +7695,10 @@ const docTemplate = `{
"type": "string",
"format": "date-time"
},
"delay_login_until_ready": {
"description": "DelayLoginUntilReady if true, the agent will delay logins until it is ready (e.g. executing startup script has ended).",
"type": "boolean"
},
"directory": {
"type": "string"
},
@ -7686,6 +7734,9 @@ const docTemplate = `{
"$ref": "#/definitions/codersdk.DERPRegion"
}
},
"lifecycle_state": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle"
},
"name": {
"type": "string"
},
@ -7699,18 +7750,12 @@ const docTemplate = `{
"startup_script": {
"type": "string"
},
"startup_script_timeout_seconds": {
"description": "StartupScriptTimeoutSeconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout.",
"type": "integer"
},
"status": {
"enum": [
"connecting",
"connected",
"disconnected",
"timeout"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentStatus"
}
]
"$ref": "#/definitions/codersdk.WorkspaceAgentStatus"
},
"troubleshooting_url": {
"type": "string"
@ -7754,6 +7799,23 @@ const docTemplate = `{
}
}
},
"codersdk.WorkspaceAgentLifecycle": {
"type": "string",
"enum": [
"created",
"starting",
"start_timeout",
"start_error",
"ready"
],
"x-enum-varnames": [
"WorkspaceAgentLifecycleCreated",
"WorkspaceAgentLifecycleStarting",
"WorkspaceAgentLifecycleStartTimeout",
"WorkspaceAgentLifecycleStartError",
"WorkspaceAgentLifecycleReady"
]
},
"codersdk.WorkspaceAgentMetadata": {
"type": "object",
"properties": {
@ -7785,6 +7847,9 @@ const docTemplate = `{
"startup_script": {
"type": "string"
},
"startup_script_timeout": {
"type": "integer"
},
"vscode_port_proxy_uri": {
"type": "string"
}

View File

@ -3493,6 +3493,38 @@
}
}
},
"/workspaceagents/me/report-lifecycle": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"tags": ["Agents"],
"summary": "Submit workspace agent lifecycle state",
"operationId": "submit-workspace-agent-lifecycle-state",
"parameters": [
{
"description": "Workspace agent lifecycle request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.PostWorkspaceAgentLifecycleRequest"
}
}
],
"responses": {
"204": {
"description": "Success"
}
},
"x-apidocgen": {
"skip": true
}
}
},
"/workspaceagents/me/report-stats": {
"post": {
"security": [
@ -6006,6 +6038,14 @@
"ParameterSourceSchemeData"
]
},
"codersdk.PostWorkspaceAgentLifecycleRequest": {
"type": "object",
"properties": {
"state": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle"
}
}
},
"codersdk.PostWorkspaceAgentVersionRequest": {
"description": "x-apidocgen:skip",
"type": "object",
@ -6886,6 +6926,10 @@
"type": "string",
"format": "date-time"
},
"delay_login_until_ready": {
"description": "DelayLoginUntilReady if true, the agent will delay logins until it is ready (e.g. executing startup script has ended).",
"type": "boolean"
},
"directory": {
"type": "string"
},
@ -6921,6 +6965,9 @@
"$ref": "#/definitions/codersdk.DERPRegion"
}
},
"lifecycle_state": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle"
},
"name": {
"type": "string"
},
@ -6934,13 +6981,12 @@
"startup_script": {
"type": "string"
},
"startup_script_timeout_seconds": {
"description": "StartupScriptTimeoutSeconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout.",
"type": "integer"
},
"status": {
"enum": ["connecting", "connected", "disconnected", "timeout"],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentStatus"
}
]
"$ref": "#/definitions/codersdk.WorkspaceAgentStatus"
},
"troubleshooting_url": {
"type": "string"
@ -6984,6 +7030,17 @@
}
}
},
"codersdk.WorkspaceAgentLifecycle": {
"type": "string",
"enum": ["created", "starting", "start_timeout", "start_error", "ready"],
"x-enum-varnames": [
"WorkspaceAgentLifecycleCreated",
"WorkspaceAgentLifecycleStarting",
"WorkspaceAgentLifecycleStartTimeout",
"WorkspaceAgentLifecycleStartError",
"WorkspaceAgentLifecycleReady"
]
},
"codersdk.WorkspaceAgentMetadata": {
"type": "object",
"properties": {
@ -7015,6 +7072,9 @@
"startup_script": {
"type": "string"
},
"startup_script_timeout": {
"type": "integer"
},
"vscode_port_proxy_uri": {
"type": "string"
}

View File

@ -541,6 +541,7 @@ func New(options *Options) *API {
r.Get("/gitsshkey", api.agentGitSSHKey)
r.Get("/coordinate", api.workspaceAgentCoordinate)
r.Post("/report-stats", api.workspaceAgentReportStats)
r.Post("/report-lifecycle", api.workspaceAgentReportLifecycle)
})
r.Route("/{workspaceagent}", func(r chi.Router) {
r.Use(

View File

@ -75,6 +75,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
"POST:/api/v2/workspaceagents/me/version": {NoAuthorize: true},
"POST:/api/v2/workspaceagents/me/app-health": {NoAuthorize: true},
"POST:/api/v2/workspaceagents/me/report-stats": {NoAuthorize: true},
"POST:/api/v2/workspaceagents/me/report-lifecycle": {NoAuthorize: true},
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.WithID(a.Admin.OrganizationID).InOrg(a.Admin.OrganizationID)},
@ -276,9 +277,11 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
// Routes like proxy routes support all HTTP methods. A helper func to expand
// 1 url to all http methods.
assertAllHTTPMethods := func(url string, check RouteCheck) {
methods := []string{http.MethodGet, http.MethodHead, http.MethodPost,
methods := []string{
http.MethodGet, http.MethodHead, http.MethodPost,
http.MethodPut, http.MethodPatch, http.MethodDelete,
http.MethodConnect, http.MethodOptions, http.MethodTrace}
http.MethodConnect, http.MethodOptions, http.MethodTrace,
}
for _, method := range methods {
route := method + ":" + url

View File

@ -2789,6 +2789,7 @@ func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser
ConnectionTimeoutSeconds: arg.ConnectionTimeoutSeconds,
TroubleshootingURL: arg.TroubleshootingURL,
MOTDFile: arg.MOTDFile,
LifecycleState: database.WorkspaceAgentLifecycleStateCreated,
}
q.workspaceAgents = append(q.workspaceAgents, agent)
@ -4294,3 +4295,20 @@ func (q *fakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUI
}
return sum, nil
}
func (q *fakeQuerier) UpdateWorkspaceAgentLifecycleStateByID(_ context.Context, arg database.UpdateWorkspaceAgentLifecycleStateByIDParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i, agent := range q.workspaceAgents {
if agent.ID == arg.ID {
agent.LifecycleState = arg.LifecycleState
q.workspaceAgents[i] = agent
return nil
}
}
return sql.ErrNoRows
}

View File

@ -99,6 +99,14 @@ CREATE TYPE user_status AS ENUM (
'suspended'
);
CREATE TYPE workspace_agent_lifecycle_state AS ENUM (
'created',
'starting',
'start_timeout',
'start_error',
'ready'
);
CREATE TYPE workspace_app_health AS ENUM (
'disabled',
'initializing',
@ -448,7 +456,10 @@ CREATE TABLE workspace_agents (
last_connected_replica_id uuid,
connection_timeout_seconds integer DEFAULT 0 NOT NULL,
troubleshooting_url text DEFAULT ''::text NOT NULL,
motd_file text DEFAULT ''::text NOT NULL
motd_file text DEFAULT ''::text NOT NULL,
lifecycle_state workspace_agent_lifecycle_state DEFAULT 'created'::workspace_agent_lifecycle_state NOT NULL,
delay_login_until_ready boolean DEFAULT false NOT NULL,
startup_script_timeout_seconds integer DEFAULT 0 NOT NULL
);
COMMENT ON COLUMN workspace_agents.version IS 'Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.';
@ -459,6 +470,12 @@ COMMENT ON COLUMN workspace_agents.troubleshooting_url IS 'URL for troubleshooti
COMMENT ON COLUMN workspace_agents.motd_file IS 'Path to file inside workspace containing the message of the day (MOTD) to show to the user when logging in via SSH.';
COMMENT ON COLUMN workspace_agents.lifecycle_state IS 'The current lifecycle state reported by the workspace agent.';
COMMENT ON COLUMN workspace_agents.delay_login_until_ready IS 'If true, the agent will delay logins until it is ready (e.g. executing startup script has ended).';
COMMENT ON COLUMN workspace_agents.startup_script_timeout_seconds IS 'The number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout.';
CREATE TABLE workspace_apps (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,

View File

@ -0,0 +1,6 @@
ALTER TABLE workspace_agents DROP COLUMN startup_script_timeout_seconds;
ALTER TABLE workspace_agents DROP COLUMN delay_login_until_ready;
ALTER TABLE workspace_agents DROP COLUMN lifecycle_state;
DROP TYPE workspace_agent_lifecycle_state;

View File

@ -0,0 +1,19 @@
CREATE TYPE workspace_agent_lifecycle_state AS ENUM ('created', 'starting', 'start_timeout', 'start_error', 'ready');
-- Set all existing workspace agents to 'ready' so that only newly created agents will be in the 'created' state.
ALTER TABLE workspace_agents ADD COLUMN lifecycle_state workspace_agent_lifecycle_state NOT NULL DEFAULT 'ready';
-- Change the default for newly created agents.
ALTER TABLE workspace_agents ALTER COLUMN lifecycle_state SET DEFAULT 'created';
COMMENT ON COLUMN workspace_agents.lifecycle_state IS 'The current lifecycle state reported by the workspace agent.';
-- Set default values that conform to current behavior.
-- Allow logins immediately after agent connect.
ALTER TABLE workspace_agents ADD COLUMN delay_login_until_ready boolean NOT NULL DEFAULT false;
COMMENT ON COLUMN workspace_agents.delay_login_until_ready IS 'If true, the agent will delay logins until it is ready (e.g. executing startup script has ended).';
-- Disable startup script timeouts by default.
ALTER TABLE workspace_agents ADD COLUMN startup_script_timeout_seconds int4 NOT NULL DEFAULT 0;
COMMENT ON COLUMN workspace_agents.startup_script_timeout_seconds IS 'The number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout.';

View File

@ -1002,6 +1002,73 @@ func AllUserStatusValues() []UserStatus {
}
}
type WorkspaceAgentLifecycleState string
const (
WorkspaceAgentLifecycleStateCreated WorkspaceAgentLifecycleState = "created"
WorkspaceAgentLifecycleStateStarting WorkspaceAgentLifecycleState = "starting"
WorkspaceAgentLifecycleStateStartTimeout WorkspaceAgentLifecycleState = "start_timeout"
WorkspaceAgentLifecycleStateStartError WorkspaceAgentLifecycleState = "start_error"
WorkspaceAgentLifecycleStateReady WorkspaceAgentLifecycleState = "ready"
)
func (e *WorkspaceAgentLifecycleState) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = WorkspaceAgentLifecycleState(s)
case string:
*e = WorkspaceAgentLifecycleState(s)
default:
return fmt.Errorf("unsupported scan type for WorkspaceAgentLifecycleState: %T", src)
}
return nil
}
type NullWorkspaceAgentLifecycleState struct {
WorkspaceAgentLifecycleState WorkspaceAgentLifecycleState
Valid bool // Valid is true if WorkspaceAgentLifecycleState is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullWorkspaceAgentLifecycleState) Scan(value interface{}) error {
if value == nil {
ns.WorkspaceAgentLifecycleState, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.WorkspaceAgentLifecycleState.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullWorkspaceAgentLifecycleState) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return ns.WorkspaceAgentLifecycleState, nil
}
func (e WorkspaceAgentLifecycleState) Valid() bool {
switch e {
case WorkspaceAgentLifecycleStateCreated,
WorkspaceAgentLifecycleStateStarting,
WorkspaceAgentLifecycleStateStartTimeout,
WorkspaceAgentLifecycleStateStartError,
WorkspaceAgentLifecycleStateReady:
return true
}
return false
}
func AllWorkspaceAgentLifecycleStateValues() []WorkspaceAgentLifecycleState {
return []WorkspaceAgentLifecycleState{
WorkspaceAgentLifecycleStateCreated,
WorkspaceAgentLifecycleStateStarting,
WorkspaceAgentLifecycleStateStartTimeout,
WorkspaceAgentLifecycleStateStartError,
WorkspaceAgentLifecycleStateReady,
}
}
type WorkspaceAppHealth string
const (
@ -1448,6 +1515,12 @@ type WorkspaceAgent struct {
TroubleshootingURL string `db:"troubleshooting_url" json:"troubleshooting_url"`
// Path to file inside workspace containing the message of the day (MOTD) to show to the user when logging in via SSH.
MOTDFile string `db:"motd_file" json:"motd_file"`
// The current lifecycle state reported by the workspace agent.
LifecycleState WorkspaceAgentLifecycleState `db:"lifecycle_state" json:"lifecycle_state"`
// If true, the agent will delay logins until it is ready (e.g. executing startup script has ended).
DelayLoginUntilReady bool `db:"delay_login_until_ready" json:"delay_login_until_ready"`
// The number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout.
StartupScriptTimeoutSeconds int32 `db:"startup_script_timeout_seconds" json:"startup_script_timeout_seconds"`
}
type WorkspaceApp struct {

View File

@ -199,6 +199,7 @@ type sqlcQuerier interface {
UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error)
UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error)
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error
UpdateWorkspaceAgentVersionByID(ctx context.Context, arg UpdateWorkspaceAgentVersionByIDParams) error
UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error

View File

@ -4869,7 +4869,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, delay_login_until_ready, startup_script_timeout_seconds
FROM
workspace_agents
WHERE
@ -4904,13 +4904,16 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.DelayLoginUntilReady,
&i.StartupScriptTimeoutSeconds,
)
return i, err
}
const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, delay_login_until_ready, startup_script_timeout_seconds
FROM
workspace_agents
WHERE
@ -4943,13 +4946,16 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.DelayLoginUntilReady,
&i.StartupScriptTimeoutSeconds,
)
return i, err
}
const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, delay_login_until_ready, startup_script_timeout_seconds
FROM
workspace_agents
WHERE
@ -4984,13 +4990,16 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.DelayLoginUntilReady,
&i.StartupScriptTimeoutSeconds,
)
return i, err
}
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, delay_login_until_ready, startup_script_timeout_seconds
FROM
workspace_agents
WHERE
@ -5029,6 +5038,9 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.DelayLoginUntilReady,
&i.StartupScriptTimeoutSeconds,
); err != nil {
return nil, err
}
@ -5044,7 +5056,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
}
const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file FROM workspace_agents WHERE created_at > $1
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, delay_login_until_ready, startup_script_timeout_seconds FROM workspace_agents WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
@ -5079,6 +5091,9 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.DelayLoginUntilReady,
&i.StartupScriptTimeoutSeconds,
); err != nil {
return nil, err
}
@ -5112,30 +5127,34 @@ INSERT INTO
resource_metadata,
connection_timeout_seconds,
troubleshooting_url,
motd_file
motd_file,
delay_login_until_ready,
startup_script_timeout_seconds
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, delay_login_until_ready, startup_script_timeout_seconds
`
type InsertWorkspaceAgentParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
AuthToken uuid.UUID `db:"auth_token" json:"auth_token"`
AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"`
Architecture string `db:"architecture" json:"architecture"`
EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"`
OperatingSystem string `db:"operating_system" json:"operating_system"`
StartupScript sql.NullString `db:"startup_script" json:"startup_script"`
Directory string `db:"directory" json:"directory"`
InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"`
ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"`
ConnectionTimeoutSeconds int32 `db:"connection_timeout_seconds" json:"connection_timeout_seconds"`
TroubleshootingURL string `db:"troubleshooting_url" json:"troubleshooting_url"`
MOTDFile string `db:"motd_file" json:"motd_file"`
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
AuthToken uuid.UUID `db:"auth_token" json:"auth_token"`
AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"`
Architecture string `db:"architecture" json:"architecture"`
EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"`
OperatingSystem string `db:"operating_system" json:"operating_system"`
StartupScript sql.NullString `db:"startup_script" json:"startup_script"`
Directory string `db:"directory" json:"directory"`
InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"`
ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"`
ConnectionTimeoutSeconds int32 `db:"connection_timeout_seconds" json:"connection_timeout_seconds"`
TroubleshootingURL string `db:"troubleshooting_url" json:"troubleshooting_url"`
MOTDFile string `db:"motd_file" json:"motd_file"`
DelayLoginUntilReady bool `db:"delay_login_until_ready" json:"delay_login_until_ready"`
StartupScriptTimeoutSeconds int32 `db:"startup_script_timeout_seconds" json:"startup_script_timeout_seconds"`
}
func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) {
@ -5157,6 +5176,8 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
arg.ConnectionTimeoutSeconds,
arg.TroubleshootingURL,
arg.MOTDFile,
arg.DelayLoginUntilReady,
arg.StartupScriptTimeoutSeconds,
)
var i WorkspaceAgent
err := row.Scan(
@ -5182,6 +5203,9 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.DelayLoginUntilReady,
&i.StartupScriptTimeoutSeconds,
)
return i, err
}
@ -5220,6 +5244,25 @@ func (q *sqlQuerier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg
return err
}
const updateWorkspaceAgentLifecycleStateByID = `-- name: UpdateWorkspaceAgentLifecycleStateByID :exec
UPDATE
workspace_agents
SET
lifecycle_state = $2
WHERE
id = $1
`
type UpdateWorkspaceAgentLifecycleStateByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
LifecycleState WorkspaceAgentLifecycleState `db:"lifecycle_state" json:"lifecycle_state"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentLifecycleStateByID, arg.ID, arg.LifecycleState)
return err
}
const updateWorkspaceAgentVersionByID = `-- name: UpdateWorkspaceAgentVersionByID :exec
UPDATE
workspace_agents

View File

@ -56,10 +56,12 @@ INSERT INTO
resource_metadata,
connection_timeout_seconds,
troubleshooting_url,
motd_file
motd_file,
delay_login_until_ready,
startup_script_timeout_seconds
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING *;
-- name: UpdateWorkspaceAgentConnectionByID :exec
UPDATE
@ -80,3 +82,11 @@ SET
version = $2
WHERE
id = $1;
-- name: UpdateWorkspaceAgentLifecycleStateByID :exec
UPDATE
workspace_agents
SET
lifecycle_state = $2
WHERE
id = $1;

View File

@ -949,9 +949,11 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
String: prAgent.StartupScript,
Valid: prAgent.StartupScript != "",
},
ConnectionTimeoutSeconds: prAgent.GetConnectionTimeoutSeconds(),
TroubleshootingURL: prAgent.GetTroubleshootingUrl(),
MOTDFile: prAgent.GetMotdFile(),
ConnectionTimeoutSeconds: prAgent.GetConnectionTimeoutSeconds(),
TroubleshootingURL: prAgent.GetTroubleshootingUrl(),
MOTDFile: prAgent.GetMotdFile(),
DelayLoginUntilReady: prAgent.GetDelayLoginUntilReady(),
StartupScriptTimeoutSeconds: prAgent.GetStartupScriptTimeoutSeconds(),
})
if err != nil {
return xerrors.Errorf("insert agent: %w", err)

View File

@ -150,6 +150,7 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
Directory: apiAgent.Directory,
VSCodePortProxyURI: vscodeProxyURI,
MOTDFile: workspaceAgent.MOTDFile,
StartupScriptTimeout: time.Duration(apiAgent.StartupScriptTimeoutSeconds) * time.Second,
})
}
@ -739,21 +740,24 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
troubleshootingURL = dbAgent.TroubleshootingURL
}
workspaceAgent := codersdk.WorkspaceAgent{
ID: dbAgent.ID,
CreatedAt: dbAgent.CreatedAt,
UpdatedAt: dbAgent.UpdatedAt,
ResourceID: dbAgent.ResourceID,
InstanceID: dbAgent.AuthInstanceID.String,
Name: dbAgent.Name,
Architecture: dbAgent.Architecture,
OperatingSystem: dbAgent.OperatingSystem,
StartupScript: dbAgent.StartupScript.String,
Version: dbAgent.Version,
EnvironmentVariables: envs,
Directory: dbAgent.Directory,
Apps: apps,
ConnectionTimeoutSeconds: dbAgent.ConnectionTimeoutSeconds,
TroubleshootingURL: troubleshootingURL,
ID: dbAgent.ID,
CreatedAt: dbAgent.CreatedAt,
UpdatedAt: dbAgent.UpdatedAt,
ResourceID: dbAgent.ResourceID,
InstanceID: dbAgent.AuthInstanceID.String,
Name: dbAgent.Name,
Architecture: dbAgent.Architecture,
OperatingSystem: dbAgent.OperatingSystem,
StartupScript: dbAgent.StartupScript.String,
Version: dbAgent.Version,
EnvironmentVariables: envs,
Directory: dbAgent.Directory,
Apps: apps,
ConnectionTimeoutSeconds: dbAgent.ConnectionTimeoutSeconds,
TroubleshootingURL: troubleshootingURL,
LifecycleState: codersdk.WorkspaceAgentLifecycle(dbAgent.LifecycleState),
DelayLoginUntilReady: dbAgent.DelayLoginUntilReady,
StartupScriptTimeoutSeconds: dbAgent.StartupScriptTimeoutSeconds,
}
node := coordinator.Node(dbAgent.ID)
if node != nil {
@ -900,6 +904,61 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
})
}
// @Summary Submit workspace agent lifecycle state
// @ID submit-workspace-agent-lifecycle-state
// @Security CoderSessionToken
// @Accept json
// @Tags Agents
// @Param request body codersdk.PostWorkspaceAgentLifecycleRequest true "Workspace agent lifecycle request"
// @Success 204 "Success"
// @Router /workspaceagents/me/report-lifecycle [post]
// @x-apidocgen {"skip": true}
func (api *API) workspaceAgentReportLifecycle(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
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
}
var req codersdk.PostWorkspaceAgentLifecycleRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
api.Logger.Debug(ctx, "workspace agent state report",
slog.F("agent", workspaceAgent.ID),
slog.F("workspace", workspace.ID),
slog.F("payload", req),
)
lifecycleState := database.WorkspaceAgentLifecycleState(req.State)
if !lifecycleState.Valid() {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid lifecycle state.",
Detail: fmt.Sprintf("Invalid lifecycle state %q, must be be one of %q.", req.State, database.AllWorkspaceAgentLifecycleStateValues()),
})
return
}
err = api.Database.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{
ID: workspaceAgent.ID,
LifecycleState: lifecycleState,
})
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
api.publishWorkspaceUpdate(ctx, workspace.ID)
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
}
// @Summary Submit workspace agent application health
// @ID submit-workspace-agent-application-health
// @Security CoderSessionToken

View File

@ -1220,3 +1220,88 @@ func gitAuthCallback(t *testing.T, id string, client *codersdk.Client) *http.Res
})
return res
}
func TestWorkspaceAgent_LifecycleState(t *testing.T) {
t.Parallel()
t.Run("Set", func(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{{
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 := codersdk.New(client.URL)
agentClient.SetSessionToken(authToken)
tests := []struct {
state codersdk.WorkspaceAgentLifecycle
wantErr bool
}{
{codersdk.WorkspaceAgentLifecycleCreated, false},
{codersdk.WorkspaceAgentLifecycleStarting, false},
{codersdk.WorkspaceAgentLifecycleStartTimeout, false},
{codersdk.WorkspaceAgentLifecycleStartError, false},
{codersdk.WorkspaceAgentLifecycleReady, false},
{codersdk.WorkspaceAgentLifecycle("nonexistent_state"), true},
{codersdk.WorkspaceAgentLifecycle(""), true},
}
//nolint:paralleltest // No race between setting the state and getting the workspace.
for _, tt := range tests {
tt := tt
t.Run(string(tt.state), func(t *testing.T) {
ctx, _ := testutil.Context(t)
err := agentClient.PostWorkspaceAgentLifecycle(ctx, codersdk.PostWorkspaceAgentLifecycleRequest{
State: tt.state,
})
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err, "post lifecycle state %q", tt.state)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "get workspace")
for _, res := range workspace.LatestBuild.Resources {
for _, agent := range res.Agents {
require.Equal(t, tt.state, agent.LifecycleState)
}
}
})
}
})
}

View File

@ -217,6 +217,10 @@ func (*client) AgentReportStats(_ context.Context, _ slog.Logger, _ func() *code
return io.NopCloser(strings.NewReader("")), nil
}
func (*client) PostWorkspaceAgentLifecycle(_ context.Context, _ codersdk.PostWorkspaceAgentLifecycleRequest) error {
return nil
}
func (*client) PostWorkspaceAgentAppHealth(_ context.Context, _ codersdk.PostWorkspaceAppHealthsRequest) error {
return nil
}