2022-04-11 21:06:15 +00:00
package coderd
import (
2023-03-14 14:46:47 +00:00
"bufio"
2022-06-03 09:50:10 +00:00
"context"
2022-04-11 21:06:15 +00:00
"database/sql"
"encoding/json"
2022-10-25 00:46:24 +00:00
"errors"
2022-04-11 21:06:15 +00:00
"fmt"
2023-08-02 08:31:51 +00:00
"io"
2022-04-18 22:40:25 +00:00
"net"
2022-04-11 21:06:15 +00:00
"net/http"
2022-09-01 01:09:44 +00:00
"net/netip"
2022-10-11 15:10:02 +00:00
"net/url"
2023-03-13 09:54:53 +00:00
"runtime/pprof"
2023-08-09 05:10:28 +00:00
"sort"
2022-04-18 22:40:25 +00:00
"strconv"
2022-09-01 01:09:44 +00:00
"strings"
2023-03-13 09:54:53 +00:00
"sync"
2023-03-23 19:09:13 +00:00
"sync/atomic"
2022-04-11 21:06:15 +00:00
"time"
2023-03-30 13:24:51 +00:00
"github.com/go-chi/chi/v5"
2022-04-29 22:30:10 +00:00
"github.com/google/uuid"
2023-08-24 20:18:42 +00:00
"golang.org/x/exp/maps"
2023-08-28 19:46:42 +00:00
"golang.org/x/exp/slices"
2022-08-31 15:33:50 +00:00
"golang.org/x/mod/semver"
2023-04-27 10:34:00 +00:00
"golang.org/x/sync/errgroup"
2022-04-11 21:06:15 +00:00
"golang.org/x/xerrors"
"nhooyr.io/websocket"
2022-09-01 01:09:44 +00:00
"tailscale.com/tailcfg"
2022-04-11 21:06:15 +00:00
"cdr.dev/slog"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
2023-09-01 16:50:12 +00:00
"github.com/coder/coder/v2/coderd/database/dbtime"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/gitauth"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/tailnet"
2022-04-11 21:06:15 +00:00
)
2023-01-13 11:27:21 +00:00
// @Summary Get workspace agent by ID
// @ID get-workspace-agent-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Success 200 {object} codersdk.WorkspaceAgent
// @Router /workspaceagents/{workspaceagent} [get]
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceAgent ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-04-26 01:03:54 +00:00
workspaceAgent := httpmw . WorkspaceAgentParam ( r )
2023-03-21 14:10:22 +00:00
2022-09-21 22:07:00 +00:00
dbApps , err := api . Database . GetWorkspaceAppsByAgentID ( ctx , workspaceAgent . ID )
2022-06-04 20:13:37 +00:00
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching workspace agent applications." ,
Detail : err . Error ( ) ,
2022-06-04 20:13:37 +00:00
} )
return
}
2023-09-12 13:25:10 +00:00
resource , err := api . Database . GetWorkspaceResourceByID ( ctx , workspaceAgent . ResourceID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching workspace resource." ,
Detail : err . Error ( ) ,
} )
return
}
build , err := api . Database . GetWorkspaceBuildByJobID ( ctx , resource . JobID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching workspace build." ,
Detail : err . Error ( ) ,
} )
return
}
workspace , err := api . Database . GetWorkspaceByID ( ctx , build . WorkspaceID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching workspace." ,
Detail : err . Error ( ) ,
} )
return
}
owner , err := api . Database . GetUserByID ( ctx , workspace . OwnerID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching workspace owner." ,
Detail : err . Error ( ) ,
} )
return
}
2023-03-07 21:10:01 +00:00
apiAgent , err := convertWorkspaceAgent (
2023-09-12 13:25:10 +00:00
api . DERPMap ( ) , * api . TailnetCoordinator . Load ( ) , workspaceAgent , convertApps ( dbApps , workspaceAgent , owner , workspace ) , api . AgentInactiveDisconnectTimeout ,
2023-03-07 21:10:01 +00:00
api . DeploymentValues . AgentFallbackTroubleshootingURL . String ( ) ,
)
2022-04-11 21:06:15 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error reading workspace agent." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-04-11 21:06:15 +00:00
} )
return
}
2022-04-12 15:17:33 +00:00
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , apiAgent )
2022-04-11 21:06:15 +00:00
}
2023-03-31 20:26:19 +00:00
// @Summary Get authorized workspace agent manifest
// @ID get-authorized-workspace-agent-manifest
2023-01-05 14:27:10 +00:00
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
2023-03-31 20:26:19 +00:00
// @Success 200 {object} agentsdk.Manifest
// @Router /workspaceagents/me/manifest [get]
func ( api * API ) workspaceAgentManifest ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-04-26 01:03:54 +00:00
workspaceAgent := httpmw . WorkspaceAgent ( r )
2023-03-07 21:10:01 +00:00
apiAgent , err := convertWorkspaceAgent (
2023-07-26 16:21:04 +00:00
api . DERPMap ( ) , * api . TailnetCoordinator . Load ( ) , workspaceAgent , nil , api . AgentInactiveDisconnectTimeout ,
2023-03-07 21:10:01 +00:00
api . DeploymentValues . AgentFallbackTroubleshootingURL . String ( ) ,
)
2022-04-25 18:30:39 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error reading workspace agent." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-04-25 18:30:39 +00:00
} )
return
}
2022-11-18 22:46:53 +00:00
dbApps , err := api . Database . GetWorkspaceAppsByAgentID ( ctx , workspaceAgent . ID )
2022-10-24 03:35:08 +00:00
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-10-24 03:35:08 +00:00
Message : "Internal error fetching workspace agent applications." ,
Detail : err . Error ( ) ,
} )
return
}
2023-03-31 20:26:19 +00:00
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
}
2022-11-18 22:46:53 +00:00
resource , err := api . Database . GetWorkspaceResourceByID ( ctx , workspaceAgent . ResourceID )
2022-11-04 04:45:43 +00:00
if err != nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-11-04 04:45:43 +00:00
Message : "Internal error fetching workspace resource." ,
Detail : err . Error ( ) ,
} )
return
}
2022-11-18 22:46:53 +00:00
build , err := api . Database . GetWorkspaceBuildByJobID ( ctx , resource . JobID )
2022-11-04 04:45:43 +00:00
if err != nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-11-04 04:45:43 +00:00
Message : "Internal error fetching workspace build." ,
Detail : err . Error ( ) ,
} )
return
}
2022-11-18 22:46:53 +00:00
workspace , err := api . Database . GetWorkspaceByID ( ctx , build . WorkspaceID )
2022-11-04 04:45:43 +00:00
if err != nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-11-04 04:45:43 +00:00
Message : "Internal error fetching workspace." ,
Detail : err . Error ( ) ,
} )
return
}
2022-11-18 22:46:53 +00:00
owner , err := api . Database . GetUserByID ( ctx , workspace . OwnerID )
2022-11-04 04:45:43 +00:00
if err != nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-11-04 04:45:43 +00:00
Message : "Internal error fetching workspace owner." ,
Detail : err . Error ( ) ,
} )
return
}
2023-09-08 14:01:57 +00:00
appHost := httpapi . ApplicationURL {
AppSlugOrPort : "{{port}}" ,
AgentName : workspaceAgent . Name ,
WorkspaceName : workspace . Name ,
Username : owner . Username ,
}
vscodeProxyURI := api . AccessURL . Scheme + "://" + strings . ReplaceAll ( api . AppHostname , "*" , appHost . String ( ) )
2022-12-01 20:39:19 +00:00
if api . AccessURL . Port ( ) != "" {
vscodeProxyURI += fmt . Sprintf ( ":%s" , api . AccessURL . Port ( ) )
}
2022-06-24 15:25:01 +00:00
2023-03-31 20:26:19 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , agentsdk . Manifest {
2023-07-12 22:37:31 +00:00
AgentID : apiAgent . ID ,
2023-09-12 13:25:10 +00:00
Apps : convertApps ( dbApps , workspaceAgent , owner , workspace ) ,
2023-07-26 16:21:04 +00:00
DERPMap : api . DERPMap ( ) ,
2023-08-24 17:22:31 +00:00
DERPForceWebSockets : api . DeploymentValues . DERP . Config . ForceWebSockets . Value ( ) ,
2023-06-21 22:02:05 +00:00
GitAuthConfigs : len ( api . GitAuthConfigs ) ,
EnvironmentVariables : apiAgent . EnvironmentVariables ,
StartupScript : apiAgent . StartupScript ,
Directory : apiAgent . Directory ,
VSCodePortProxyURI : vscodeProxyURI ,
MOTDFile : workspaceAgent . MOTDFile ,
StartupScriptTimeout : time . Duration ( apiAgent . StartupScriptTimeoutSeconds ) * time . Second ,
ShutdownScript : apiAgent . ShutdownScript ,
ShutdownScriptTimeout : time . Duration ( apiAgent . ShutdownScriptTimeoutSeconds ) * time . Second ,
DisableDirectConnections : api . DeploymentValues . DERP . Config . BlockDirect . Value ( ) ,
Metadata : convertWorkspaceAgentMetadataDesc ( metadata ) ,
2022-04-26 01:03:54 +00:00
} )
2022-04-25 18:30:39 +00:00
}
2023-02-07 21:35:09 +00:00
// @Summary Submit workspace agent startup
// @ID submit-workspace-agent-startup
2023-01-05 14:27:10 +00:00
// @Security CoderSessionToken
2023-01-13 11:27:21 +00:00
// @Accept json
// @Produce json
2023-01-05 14:27:10 +00:00
// @Tags Agents
2023-02-07 21:35:09 +00:00
// @Param request body agentsdk.PostStartupRequest true "Startup request"
2023-01-05 14:27:10 +00:00
// @Success 200
2023-02-07 21:35:09 +00:00
// @Router /workspaceagents/me/startup [post]
2023-01-05 14:27:10 +00:00
// @x-apidocgen {"skip": true}
2023-02-07 21:35:09 +00:00
func ( api * API ) postWorkspaceAgentStartup ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-08-31 15:33:50 +00:00
workspaceAgent := httpmw . WorkspaceAgent ( r )
2023-03-07 21:10:01 +00:00
apiAgent , err := convertWorkspaceAgent (
2023-07-26 16:21:04 +00:00
api . DERPMap ( ) , * api . TailnetCoordinator . Load ( ) , workspaceAgent , nil , api . AgentInactiveDisconnectTimeout ,
2023-03-07 21:10:01 +00:00
api . DeploymentValues . AgentFallbackTroubleshootingURL . String ( ) ,
)
2022-08-31 15:33:50 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-08-31 15:33:50 +00:00
Message : "Internal error reading workspace agent." ,
Detail : err . Error ( ) ,
} )
return
}
2023-02-07 21:35:09 +00:00
var req agentsdk . PostStartupRequest
2022-09-21 22:07:00 +00:00
if ! httpapi . Read ( ctx , rw , r , & req ) {
2022-08-31 15:33:50 +00:00
return
}
2023-08-17 21:01:55 +00:00
api . Logger . Debug (
ctx ,
"post workspace agent version" ,
slog . F ( "agent_id" , apiAgent . ID ) ,
slog . F ( "agent_version" , req . Version ) ,
slog . F ( "remote_addr" , r . RemoteAddr ) ,
)
2022-08-31 15:33:50 +00:00
if ! semver . IsValid ( req . Version ) {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-08-31 15:33:50 +00:00
Message : "Invalid workspace agent version provided." ,
Detail : fmt . Sprintf ( "invalid semver version: %q" , req . Version ) ,
} )
return
}
2023-08-09 05:10:28 +00:00
// Validate subsystems.
seen := make ( map [ codersdk . AgentSubsystem ] bool )
for _ , s := range req . Subsystems {
if ! s . Valid ( ) {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Invalid workspace agent subsystem provided." ,
Detail : fmt . Sprintf ( "invalid subsystem: %q" , s ) ,
} )
return
}
if seen [ s ] {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Invalid workspace agent subsystem provided." ,
Detail : fmt . Sprintf ( "duplicate subsystem: %q" , s ) ,
} )
return
}
seen [ s ] = true
}
2023-02-07 21:35:09 +00:00
if err := api . Database . UpdateWorkspaceAgentStartupByID ( ctx , database . UpdateWorkspaceAgentStartupByIDParams {
ID : apiAgent . ID ,
Version : req . Version ,
ExpandedDirectory : req . ExpandedDirectory ,
2023-08-09 05:10:28 +00:00
Subsystems : convertWorkspaceAgentSubsystems ( req . Subsystems ) ,
2022-08-31 15:33:50 +00:00
} ) ; err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-08-31 15:33:50 +00:00
Message : "Error setting agent version" ,
Detail : err . Error ( ) ,
} )
return
}
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , nil )
2022-08-31 15:33:50 +00:00
}
2023-07-28 15:57:23 +00:00
// @Summary Patch workspace agent logs
// @ID patch-workspace-agent-logs
2023-03-23 19:09:13 +00:00
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Agents
2023-07-28 15:57:23 +00:00
// @Param request body agentsdk.PatchLogs true "logs"
2023-03-23 19:09:13 +00:00
// @Success 200 {object} codersdk.Response
2023-07-28 15:57:23 +00:00
// @Router /workspaceagents/me/logs [patch]
func ( api * API ) patchWorkspaceAgentLogs ( rw http . ResponseWriter , r * http . Request ) {
2023-03-23 19:09:13 +00:00
ctx := r . Context ( )
workspaceAgent := httpmw . WorkspaceAgent ( r )
2023-07-28 15:57:23 +00:00
var req agentsdk . PatchLogs
2023-03-23 19:09:13 +00:00
if ! httpapi . Read ( ctx , rw , r , & req ) {
return
}
if len ( req . Logs ) == 0 {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "No logs provided." ,
} )
return
}
createdAt := make ( [ ] time . Time , 0 )
output := make ( [ ] string , 0 )
2023-04-10 19:29:59 +00:00
level := make ( [ ] database . LogLevel , 0 )
2023-07-28 15:57:23 +00:00
source := make ( [ ] database . WorkspaceAgentLogSource , 0 )
2023-03-23 19:09:13 +00:00
outputLength := 0
2023-06-20 11:41:55 +00:00
for _ , logEntry := range req . Logs {
2023-04-27 10:34:00 +00:00
createdAt = append ( createdAt , logEntry . CreatedAt )
output = append ( output , logEntry . Output )
outputLength += len ( logEntry . Output )
if logEntry . Level == "" {
2023-04-10 19:29:59 +00:00
// Default to "info" to support older agents that didn't have the level field.
2023-04-27 10:34:00 +00:00
logEntry . Level = codersdk . LogLevelInfo
2023-04-10 19:29:59 +00:00
}
2023-04-27 10:34:00 +00:00
parsedLevel := database . LogLevel ( logEntry . Level )
2023-04-10 19:29:59 +00:00
if ! parsedLevel . Valid ( ) {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Invalid log level provided." ,
2023-04-27 10:34:00 +00:00
Detail : fmt . Sprintf ( "invalid log level: %q" , logEntry . Level ) ,
2023-04-10 19:29:59 +00:00
} )
return
}
level = append ( level , parsedLevel )
2023-07-28 15:57:23 +00:00
if logEntry . Source == "" {
// Default to "startup_script" to support older agents that didn't have the source field.
logEntry . Source = codersdk . WorkspaceAgentLogSourceStartupScript
}
parsedSource := database . WorkspaceAgentLogSource ( logEntry . Source )
if ! parsedSource . Valid ( ) {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Invalid log source provided." ,
Detail : fmt . Sprintf ( "invalid log source: %q" , logEntry . Source ) ,
} )
return
}
source = append ( source , parsedSource )
2023-03-23 19:09:13 +00:00
}
2023-06-16 14:14:22 +00:00
2023-07-28 15:57:23 +00:00
logs , err := api . Database . InsertWorkspaceAgentLogs ( ctx , database . InsertWorkspaceAgentLogsParams {
2023-07-18 15:57:29 +00:00
AgentID : workspaceAgent . ID ,
CreatedAt : createdAt ,
Output : output ,
Level : level ,
2023-07-28 15:57:23 +00:00
Source : source ,
2023-07-18 15:57:29 +00:00
OutputLength : int32 ( outputLength ) ,
} )
if err != nil {
2023-07-28 15:57:23 +00:00
if ! database . IsWorkspaceAgentLogsLimitError ( err ) {
2023-07-18 15:57:29 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2023-07-28 15:57:23 +00:00
Message : "Failed to upload logs" ,
2023-07-18 15:57:29 +00:00
Detail : err . Error ( ) ,
} )
return
2023-06-16 14:14:22 +00:00
}
2023-07-28 15:57:23 +00:00
if workspaceAgent . LogsOverflowed {
2023-07-18 15:57:29 +00:00
httpapi . Write ( ctx , rw , http . StatusRequestEntityTooLarge , codersdk . Response {
2023-07-28 15:57:23 +00:00
Message : "Logs limit exceeded" ,
2023-07-18 15:57:29 +00:00
Detail : err . Error ( ) ,
} )
return
2023-06-16 14:14:22 +00:00
}
2023-07-28 15:57:23 +00:00
err := api . Database . UpdateWorkspaceAgentLogOverflowByID ( ctx , database . UpdateWorkspaceAgentLogOverflowByIDParams {
ID : workspaceAgent . ID ,
LogsOverflowed : true ,
2023-06-16 14:14:22 +00:00
} )
2023-07-18 15:57:29 +00:00
if err != nil {
// We don't want to return here, because the agent will retry
// on failure and this isn't a huge deal. The overflow state
// is just a hint to the user that the logs are incomplete.
2023-07-28 15:57:23 +00:00
api . Logger . Warn ( ctx , "failed to update workspace agent log overflow" , slog . Error ( err ) )
2023-07-18 15:57:29 +00:00
}
resource , err := api . Database . GetWorkspaceResourceByID ( ctx , workspaceAgent . ResourceID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Failed to get workspace resource." ,
2023-06-16 14:14:22 +00:00
Detail : err . Error ( ) ,
} )
return
}
2023-03-23 19:09:13 +00:00
2023-07-18 15:57:29 +00:00
build , err := api . Database . GetWorkspaceBuildByJobID ( ctx , resource . JobID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Internal error fetching workspace build job." ,
2023-03-23 19:09:13 +00:00
Detail : err . Error ( ) ,
} )
return
}
2023-07-18 15:57:29 +00:00
api . publishWorkspaceUpdate ( ctx , build . WorkspaceID )
httpapi . Write ( ctx , rw , http . StatusRequestEntityTooLarge , codersdk . Response {
2023-07-28 15:57:23 +00:00
Message : "Logs limit exceeded" ,
2023-03-23 19:09:13 +00:00
} )
return
}
2023-06-16 14:14:22 +00:00
2023-06-20 11:41:55 +00:00
lowestLogID := logs [ 0 ] . ID
2023-06-16 14:14:22 +00:00
// Publish by the lowest log ID inserted so the
// log stream will fetch everything from that point.
2023-07-28 15:57:23 +00:00
api . publishWorkspaceAgentLogsUpdate ( ctx , workspaceAgent . ID , agentsdk . LogsNotifyMessage {
2023-06-20 11:41:55 +00:00
CreatedAfter : lowestLogID - 1 ,
2023-06-16 14:14:22 +00:00
} )
2023-07-28 15:57:23 +00:00
if workspaceAgent . LogsLength == 0 {
2023-03-23 19:09:13 +00:00
// If these are the first logs being appended, we publish a UI update
// to notify the UI that logs are now available.
resource , err := api . Database . GetWorkspaceResourceByID ( ctx , workspaceAgent . ResourceID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Failed to get workspace resource." ,
Detail : err . Error ( ) ,
} )
return
}
build , err := api . Database . GetWorkspaceBuildByJobID ( ctx , resource . JobID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Internal error fetching workspace build job." ,
Detail : err . Error ( ) ,
} )
return
}
api . publishWorkspaceUpdate ( ctx , build . WorkspaceID )
}
httpapi . Write ( ctx , rw , http . StatusOK , nil )
}
2023-07-28 15:57:23 +00:00
// workspaceAgentLogs returns the logs associated with a workspace agent
2023-03-23 19:09:13 +00:00
//
2023-07-28 15:57:23 +00:00
// @Summary Get logs by workspace agent
// @ID get-logs-by-workspace-agent
2023-03-23 19:09:13 +00:00
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Param before query int false "Before log id"
// @Param after query int false "After log id"
// @Param follow query bool false "Follow log stream"
2023-06-20 13:29:32 +00:00
// @Param no_compression query bool false "Disable compression for WebSocket connection"
2023-07-28 15:57:23 +00:00
// @Success 200 {array} codersdk.WorkspaceAgentLog
// @Router /workspaceagents/{workspaceagent}/logs [get]
func ( api * API ) workspaceAgentLogs ( rw http . ResponseWriter , r * http . Request ) {
2023-03-23 19:09:13 +00:00
// This mostly copies how provisioner job logs are streamed!
var (
ctx = r . Context ( )
workspaceAgent = httpmw . WorkspaceAgentParam ( r )
logger = api . Logger . With ( slog . F ( "workspace_agent_id" , workspaceAgent . ID ) )
follow = r . URL . Query ( ) . Has ( "follow" )
afterRaw = r . URL . Query ( ) . Get ( "after" )
2023-06-20 13:29:32 +00:00
noCompression = r . URL . Query ( ) . Has ( "no_compression" )
2023-03-23 19:09:13 +00:00
)
var after int64
// Only fetch logs created after the time provided.
if afterRaw != "" {
var err error
after , err = strconv . ParseInt ( afterRaw , 10 , 64 )
2023-06-16 14:14:22 +00:00
if err != nil || after < 0 {
2023-03-23 19:09:13 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2023-06-16 14:14:22 +00:00
Message : "Query param \"after\" must be an integer greater than or equal to zero." ,
2023-03-23 19:09:13 +00:00
Validations : [ ] codersdk . ValidationError {
2023-06-16 14:14:22 +00:00
{ Field : "after" , Detail : "Must be an integer greater than or equal to zero" } ,
2023-03-23 19:09:13 +00:00
} ,
} )
return
}
}
2023-07-28 15:57:23 +00:00
logs , err := api . Database . GetWorkspaceAgentLogsAfter ( ctx , database . GetWorkspaceAgentLogsAfterParams {
2023-03-23 19:09:13 +00:00
AgentID : workspaceAgent . ID ,
CreatedAfter : after ,
} )
if errors . Is ( err , sql . ErrNoRows ) {
err = nil
}
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching provisioner logs." ,
Detail : err . Error ( ) ,
} )
return
}
if logs == nil {
2023-07-28 15:57:23 +00:00
logs = [ ] database . WorkspaceAgentLog { }
2023-03-23 19:09:13 +00:00
}
if ! follow {
2023-07-28 15:57:23 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , convertWorkspaceAgentLogs ( logs ) )
2023-03-23 19:09:13 +00:00
return
}
2023-08-28 19:46:42 +00:00
workspace , err := api . Database . GetWorkspaceByAgentID ( ctx , workspaceAgent . ID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching workspace by agent id." ,
Detail : err . Error ( ) ,
} )
return
}
2023-03-23 19:09:13 +00:00
api . WebsocketWaitMutex . Lock ( )
api . WebsocketWaitGroup . Add ( 1 )
api . WebsocketWaitMutex . Unlock ( )
defer api . WebsocketWaitGroup . Done ( )
2023-06-20 13:29:32 +00:00
opts := & websocket . AcceptOptions { }
// Allow client to request no compression. This is useful for buggy
// clients or if there's a client/server incompatibility. This is
// needed with e.g. nhooyr/websocket and Safari (confirmed in 16.5).
//
// See:
// * https://github.com/nhooyr/websocket/issues/218
// * https://github.com/gobwas/ws/issues/169
if noCompression {
opts . CompressionMode = websocket . CompressionDisabled
}
conn , err := websocket . Accept ( rw , r , opts )
2023-03-23 19:09:13 +00:00
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Failed to accept websocket." ,
Detail : err . Error ( ) ,
} )
return
}
go httpapi . Heartbeat ( ctx , conn )
ctx , wsNetConn := websocketNetConn ( ctx , conn , websocket . MessageText )
defer wsNetConn . Close ( ) // Also closes conn.
// The Go stdlib JSON encoder appends a newline character after message write.
encoder := json . NewEncoder ( wsNetConn )
2023-07-28 15:57:23 +00:00
err = encoder . Encode ( convertWorkspaceAgentLogs ( logs ) )
2023-03-23 19:09:13 +00:00
if err != nil {
return
}
2023-06-16 14:14:22 +00:00
2023-06-20 11:41:55 +00:00
lastSentLogID := after
if len ( logs ) > 0 {
lastSentLogID = logs [ len ( logs ) - 1 ] . ID
}
2023-09-19 17:02:27 +00:00
workspaceNotifyCh := make ( chan struct { } , 1 )
2023-06-16 14:14:22 +00:00
notifyCh := make ( chan struct { } , 1 )
// Allow us to immediately check if we missed any logs
// between initial fetch and subscribe.
notifyCh <- struct { } { }
2023-03-23 19:09:13 +00:00
2023-09-19 17:02:27 +00:00
// Subscribe to workspace to detect new builds.
closeSubscribeWorkspace , err := api . Pubsub . Subscribe ( codersdk . WorkspaceNotifyChannel ( workspace . ID ) , func ( _ context . Context , _ [ ] byte ) {
select {
case workspaceNotifyCh <- struct { } { } :
default :
}
} )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to subscribe to workspace for log streaming." ,
Detail : err . Error ( ) ,
} )
return
}
defer closeSubscribeWorkspace ( )
2023-06-16 14:14:22 +00:00
// Subscribe early to prevent missing log events.
2023-07-28 15:57:23 +00:00
closeSubscribe , err := api . Pubsub . Subscribe ( agentsdk . LogsNotifyChannel ( workspaceAgent . ID ) , func ( _ context . Context , _ [ ] byte ) {
2023-06-16 14:14:22 +00:00
// The message is not important, we're tracking lastSentLogID manually.
2023-03-23 19:09:13 +00:00
select {
2023-06-16 14:14:22 +00:00
case notifyCh <- struct { } { } :
2023-03-23 19:09:13 +00:00
default :
}
2023-06-16 14:14:22 +00:00
} )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2023-09-19 17:02:27 +00:00
Message : "Failed to subscribe to agent for log streaming." ,
2023-06-16 14:14:22 +00:00
Detail : err . Error ( ) ,
} )
return
2023-03-23 19:09:13 +00:00
}
2023-06-16 14:14:22 +00:00
defer closeSubscribe ( )
2023-03-23 19:09:13 +00:00
2023-06-16 14:14:22 +00:00
// Buffer size controls the log prefetch capacity.
2023-07-28 15:57:23 +00:00
bufferedLogs := make ( chan [ ] database . WorkspaceAgentLog , 8 )
2023-06-16 14:14:22 +00:00
// Check at least once per minute in case we didn't receive a pubsub message.
recheckInterval := time . Minute
t := time . NewTicker ( recheckInterval )
defer t . Stop ( )
go func ( ) {
2023-09-19 17:02:27 +00:00
defer func ( ) {
logger . Debug ( ctx , "end log streaming loop" )
close ( bufferedLogs )
} ( )
logger . Debug ( ctx , "start log streaming loop" , slog . F ( "last_sent_log_id" , lastSentLogID ) )
2023-06-20 11:41:55 +00:00
2023-08-28 19:46:42 +00:00
keepGoing := true
for keepGoing {
2023-09-19 17:02:27 +00:00
var (
debugTriggeredBy string
onlyCheckLatestBuild bool
)
2023-06-16 14:14:22 +00:00
select {
case <- ctx . Done ( ) :
2023-03-23 19:09:13 +00:00
return
2023-06-16 14:14:22 +00:00
case <- t . C :
2023-09-19 17:02:27 +00:00
debugTriggeredBy = "timer"
case <- workspaceNotifyCh :
debugTriggeredBy = "workspace"
onlyCheckLatestBuild = true
2023-06-16 14:14:22 +00:00
case <- notifyCh :
2023-09-19 17:02:27 +00:00
debugTriggeredBy = "log"
2023-06-16 14:14:22 +00:00
t . Reset ( recheckInterval )
2023-03-23 19:09:13 +00:00
}
2023-08-28 19:46:42 +00:00
agents , err := api . Database . GetWorkspaceAgentsInLatestBuildByWorkspaceID ( ctx , workspace . ID )
2023-09-19 17:02:27 +00:00
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
2023-08-28 19:46:42 +00:00
if xerrors . Is ( err , context . Canceled ) {
return
}
logger . Warn ( ctx , "failed to get workspace agents in latest build" , slog . Error ( err ) )
continue
}
// If the agent is no longer in the latest build, we can stop after
// checking once.
keepGoing = slices . ContainsFunc ( agents , func ( agent database . WorkspaceAgent ) bool { return agent . ID == workspaceAgent . ID } )
2023-09-19 17:02:27 +00:00
logger . Debug (
ctx ,
"checking for new logs" ,
slog . F ( "triggered_by" , debugTriggeredBy ) ,
slog . F ( "only_check_latest_build" , onlyCheckLatestBuild ) ,
slog . F ( "keep_going" , keepGoing ) ,
slog . F ( "last_sent_log_id" , lastSentLogID ) ,
slog . F ( "workspace_has_agents" , len ( agents ) > 0 ) ,
)
if onlyCheckLatestBuild && keepGoing {
continue
}
2023-07-28 15:57:23 +00:00
logs , err := api . Database . GetWorkspaceAgentLogsAfter ( ctx , database . GetWorkspaceAgentLogsAfterParams {
2023-06-16 14:14:22 +00:00
AgentID : workspaceAgent . ID ,
CreatedAfter : lastSentLogID ,
} )
if err != nil {
if xerrors . Is ( err , context . Canceled ) {
2023-03-23 19:09:13 +00:00
return
}
2023-07-28 15:57:23 +00:00
logger . Warn ( ctx , "failed to get workspace agent logs after" , slog . Error ( err ) )
2023-06-16 14:14:22 +00:00
continue
}
if len ( logs ) == 0 {
2023-07-18 15:57:29 +00:00
// Just keep listening - more logs might come in the future!
2023-06-16 14:14:22 +00:00
continue
2023-03-23 19:09:13 +00:00
}
2023-06-16 14:14:22 +00:00
select {
case <- ctx . Done ( ) :
return
case bufferedLogs <- logs :
lastSentLogID = logs [ len ( logs ) - 1 ] . ID
2023-03-23 19:09:13 +00:00
}
2023-06-16 14:14:22 +00:00
}
} ( )
defer func ( ) {
// Ensure that we don't return until the goroutine has exited.
//nolint:revive // Consume channel to wait until it's closed.
for range bufferedLogs {
}
} ( )
2023-03-23 19:09:13 +00:00
for {
select {
case <- ctx . Done ( ) :
2023-06-16 14:14:22 +00:00
logger . Debug ( ctx , "job logs context canceled" )
2023-03-23 19:09:13 +00:00
return
case logs , ok := <- bufferedLogs :
2023-06-16 14:14:22 +00:00
if ! ok {
select {
case <- ctx . Done ( ) :
logger . Debug ( ctx , "job logs context canceled" )
default :
logger . Debug ( ctx , "reached the end of published logs" )
}
2023-03-23 19:09:13 +00:00
return
}
2023-07-28 15:57:23 +00:00
err = encoder . Encode ( convertWorkspaceAgentLogs ( logs ) )
2023-03-23 19:09:13 +00:00
if err != nil {
return
}
}
}
}
2023-01-13 11:27:21 +00:00
// @Summary Get listening ports for workspace agent
// @ID get-listening-ports-for-workspace-agent
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
2023-01-29 21:47:24 +00:00
// @Success 200 {object} codersdk.WorkspaceAgentListeningPortsResponse
2023-01-13 11:27:21 +00:00
// @Router /workspaceagents/{workspaceagent}/listening-ports [get]
2022-10-06 12:38:22 +00:00
func ( api * API ) workspaceAgentListeningPorts ( rw http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
workspaceAgent := httpmw . WorkspaceAgentParam ( r )
2023-03-07 21:10:01 +00:00
apiAgent , err := convertWorkspaceAgent (
2023-07-26 16:21:04 +00:00
api . DERPMap ( ) , * api . TailnetCoordinator . Load ( ) , workspaceAgent , nil , api . AgentInactiveDisconnectTimeout ,
2023-03-07 21:10:01 +00:00
api . DeploymentValues . AgentFallbackTroubleshootingURL . String ( ) ,
)
2022-10-06 12:38:22 +00:00
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 {
2023-01-13 14:30:48 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-10-06 12:38:22 +00:00
Message : fmt . Sprintf ( "Agent state is %q, it must be in the %q state." , apiAgent . Status , codersdk . WorkspaceAgentConnected ) ,
} )
return
}
2023-07-12 22:37:31 +00:00
agentConn , release , err := api . agentProvider . AgentConn ( ctx , workspaceAgent . ID )
2022-10-06 12:38:22 +00:00
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error dialing workspace agent." ,
Detail : err . Error ( ) ,
} )
return
}
defer release ( )
portsResponse , err := agentConn . ListeningPorts ( ctx )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching listening ports." ,
Detail : err . Error ( ) ,
} )
return
}
2022-10-11 15:10:02 +00:00
// Get a list of ports that are in-use by applications.
apps , err := api . Database . GetWorkspaceAppsByAgentID ( ctx , workspaceAgent . ID )
if xerrors . Is ( err , sql . ErrNoRows ) {
apps = [ ] database . WorkspaceApp { }
err = nil
}
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching workspace apps." ,
Detail : err . Error ( ) ,
} )
return
}
appPorts := make ( map [ uint16 ] struct { } , len ( apps ) )
for _ , app := range apps {
if ! app . Url . Valid || app . Url . String == "" {
continue
}
u , err := url . Parse ( app . Url . String )
if err != nil {
continue
}
port := u . Port ( )
if port == "" {
continue
}
2022-12-19 19:25:59 +00:00
portNum , err := strconv . ParseUint ( port , 10 , 16 )
2022-10-11 15:10:02 +00:00
if err != nil {
continue
}
if portNum < 1 || portNum > 65535 {
continue
}
appPorts [ uint16 ( portNum ) ] = struct { } { }
}
// Filter out ports that are globally blocked, in-use by applications, or
// common non-HTTP ports such as databases, FTP, SSH, etc.
2023-01-29 21:47:24 +00:00
filteredPorts := make ( [ ] codersdk . WorkspaceAgentListeningPort , 0 , len ( portsResponse . Ports ) )
2022-10-11 15:10:02 +00:00
for _ , port := range portsResponse . Ports {
2023-01-29 21:47:24 +00:00
if port . Port < codersdk . WorkspaceAgentMinimumListeningPort {
2022-10-11 15:10:02 +00:00
continue
}
if _ , ok := appPorts [ port . Port ] ; ok {
continue
}
2023-01-29 21:47:24 +00:00
if _ , ok := codersdk . WorkspaceAgentIgnoredListeningPorts [ port . Port ] ; ok {
2022-10-11 15:10:02 +00:00
continue
}
filteredPorts = append ( filteredPorts , port )
}
portsResponse . Ports = filteredPorts
2022-10-06 12:38:22 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , portsResponse )
}
2023-07-12 22:37:31 +00:00
// Deprecated: use api.tailnet.AgentConn instead.
// See: https://github.com/coder/coder/issues/8218
func ( api * API ) _dialWorkspaceAgentTailnet ( agentID uuid . UUID ) ( * codersdk . WorkspaceAgentConn , error ) {
2022-09-01 01:09:44 +00:00
clientConn , serverConn := net . Pipe ( )
2023-07-26 16:21:04 +00:00
derpMap := api . DERPMap ( )
2022-09-01 01:09:44 +00:00
conn , err := tailnet . NewConn ( & tailnet . Options {
2023-08-24 17:22:31 +00:00
Addresses : [ ] netip . Prefix { netip . PrefixFrom ( tailnet . IP ( ) , 128 ) } ,
DERPMap : api . DERPMap ( ) ,
DERPForceWebSockets : api . DeploymentValues . DERP . Config . ForceWebSockets . Value ( ) ,
Logger : api . Logger . Named ( "net.tailnet" ) ,
BlockEndpoints : api . DeploymentValues . DERP . Config . BlockDirect . Value ( ) ,
2022-09-01 01:09:44 +00:00
} )
if err != nil {
2023-02-14 14:57:48 +00:00
_ = clientConn . Close ( )
_ = serverConn . Close ( )
2022-09-01 01:09:44 +00:00
return nil , xerrors . Errorf ( "create tailnet conn: %w" , err )
}
2023-03-23 14:54:07 +00:00
ctx , cancel := context . WithCancel ( api . ctx )
2023-03-14 14:46:47 +00:00
conn . SetDERPRegionDialer ( func ( _ context . Context , region * tailcfg . DERPRegion ) net . Conn {
if ! region . EmbeddedRelay {
return nil
}
left , right := net . Pipe ( )
go func ( ) {
defer left . Close ( )
defer right . Close ( )
brw := bufio . NewReadWriter ( bufio . NewReader ( right ) , bufio . NewWriter ( right ) )
2023-03-23 14:54:07 +00:00
api . DERPServer . Accept ( ctx , right , brw , "internal" )
2023-03-14 14:46:47 +00:00
} ( )
return left
} )
2022-09-01 01:09:44 +00:00
2023-07-26 16:21:04 +00:00
sendNodes , _ := tailnet . ServeCoordinator ( clientConn , func ( nodes [ ] * tailnet . Node ) error {
return conn . UpdateNodes ( nodes , true )
2022-09-01 01:09:44 +00:00
} )
conn . SetNodeCallback ( sendNodes )
2023-07-26 16:21:04 +00:00
// Check for updated DERP map every 5 seconds.
go func ( ) {
ticker := time . NewTicker ( 5 * time . Second )
defer ticker . Stop ( )
for {
lastDERPMap := derpMap
for {
select {
case <- ctx . Done ( ) :
return
case <- ticker . C :
}
derpMap := api . DERPMap ( )
if lastDERPMap == nil || tailnet . CompareDERPMaps ( lastDERPMap , derpMap ) {
conn . SetDERPMap ( derpMap )
lastDERPMap = derpMap
}
ticker . Reset ( 5 * time . Second )
}
}
} ( )
2023-07-12 22:37:31 +00:00
agentConn := codersdk . NewWorkspaceAgentConn ( conn , codersdk . WorkspaceAgentConnOptions {
AgentID : agentID ,
AgentIP : codersdk . WorkspaceAgentIP ,
CloseFunc : func ( ) error {
2023-03-23 14:54:07 +00:00
cancel ( )
2023-02-24 16:16:29 +00:00
_ = clientConn . Close ( )
_ = serverConn . Close ( )
2023-07-12 22:37:31 +00:00
return nil
2023-02-24 16:16:29 +00:00
} ,
2023-07-12 22:37:31 +00:00
} )
2022-09-01 01:09:44 +00:00
go func ( ) {
2022-10-17 13:43:30 +00:00
err := ( * api . TailnetCoordinator . Load ( ) ) . ServeClient ( serverConn , uuid . New ( ) , agentID )
2022-09-01 01:09:44 +00:00
if err != nil {
2023-03-28 01:01:25 +00:00
// Sometimes, we get benign closed pipe errors when the server is
// shutting down.
if api . ctx . Err ( ) == nil {
api . Logger . Warn ( ctx , "tailnet coordinator client error" , slog . Error ( err ) )
}
2023-02-24 16:16:29 +00:00
_ = agentConn . Close ( )
2022-09-01 01:09:44 +00:00
}
} ( )
2023-02-24 16:16:29 +00:00
if ! agentConn . AwaitReachable ( ctx ) {
_ = agentConn . Close ( )
2023-07-26 16:21:04 +00:00
_ = serverConn . Close ( )
_ = clientConn . Close ( )
cancel ( )
2023-02-24 16:16:29 +00:00
return nil , xerrors . Errorf ( "agent not reachable" )
}
return agentConn , nil
2022-09-01 01:09:44 +00:00
}
2023-01-13 11:27:21 +00:00
// @Summary Get connection info for workspace agent
// @ID get-connection-info-for-workspace-agent
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Success 200 {object} codersdk.WorkspaceAgentConnectionInfo
// @Router /workspaceagents/{workspaceagent}/connection [get]
2022-09-01 01:09:44 +00:00
func ( api * API ) workspaceAgentConnection ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2023-03-21 14:10:22 +00:00
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , codersdk . WorkspaceAgentConnectionInfo {
2023-07-26 16:21:04 +00:00
DERPMap : api . DERPMap ( ) ,
2023-08-24 17:22:31 +00:00
DERPForceWebSockets : api . DeploymentValues . DERP . Config . ForceWebSockets . Value ( ) ,
2023-06-21 22:02:05 +00:00
DisableDirectConnections : api . DeploymentValues . DERP . Config . BlockDirect . Value ( ) ,
2022-09-01 01:09:44 +00:00
} )
}
2023-06-21 19:33:19 +00:00
// workspaceAgentConnectionGeneric is the same as workspaceAgentConnection but
// without the workspaceagent path parameter.
//
// @Summary Get connection info for workspace agent generic
// @ID get-connection-info-for-workspace-agent-generic
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Success 200 {object} codersdk.WorkspaceAgentConnectionInfo
// @Router /workspaceagents/connection [get]
// @x-apidocgen {"skip": true}
func ( api * API ) workspaceAgentConnectionGeneric ( rw http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
httpapi . Write ( ctx , rw , http . StatusOK , codersdk . WorkspaceAgentConnectionInfo {
2023-07-26 16:21:04 +00:00
DERPMap : api . DERPMap ( ) ,
2023-08-24 17:22:31 +00:00
DERPForceWebSockets : api . DeploymentValues . DERP . Config . ForceWebSockets . Value ( ) ,
2023-07-26 16:21:04 +00:00
DisableDirectConnections : api . DeploymentValues . DERP . Config . BlockDirect . Value ( ) ,
2023-06-21 19:33:19 +00:00
} )
}
2023-07-26 16:21:04 +00:00
// @Summary Get DERP map updates
// @ID get-derp-map-updates
// @Security CoderSessionToken
// @Tags Agents
// @Success 101
// @Router /derp-map [get]
func ( api * API ) derpMapUpdates ( rw http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
api . WebsocketWaitMutex . Lock ( )
api . WebsocketWaitGroup . Add ( 1 )
api . WebsocketWaitMutex . Unlock ( )
defer api . WebsocketWaitGroup . Done ( )
ws , err := websocket . Accept ( rw , r , nil )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Failed to accept websocket." ,
Detail : err . Error ( ) ,
} )
return
}
2023-08-28 18:13:19 +00:00
ctx , nconn := websocketNetConn ( ctx , ws , websocket . MessageBinary )
2023-07-26 16:21:04 +00:00
defer nconn . Close ( )
2023-08-02 08:31:51 +00:00
// Slurp all packets from the connection into io.Discard so pongs get sent
2023-08-28 18:13:19 +00:00
// by the websocket package. We don't do any reads ourselves so this is
// necessary.
2023-08-02 08:31:51 +00:00
go func ( ) {
_ , _ = io . Copy ( io . Discard , nconn )
2023-08-28 18:13:19 +00:00
_ = nconn . Close ( )
2023-08-02 08:31:51 +00:00
} ( )
go func ( ctx context . Context ) {
// TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout?
t := time . NewTicker ( api . AgentConnectionUpdateFrequency )
defer t . Stop ( )
for {
select {
case <- t . C :
case <- ctx . Done ( ) :
return
}
2023-08-28 18:13:19 +00:00
ctx , cancel := context . WithTimeout ( ctx , 30 * time . Second )
2023-08-02 08:31:51 +00:00
err := ws . Ping ( ctx )
2023-08-28 18:13:19 +00:00
cancel ( )
2023-08-02 08:31:51 +00:00
if err != nil {
2023-08-28 18:13:19 +00:00
_ = nconn . Close ( )
2023-08-02 08:31:51 +00:00
return
}
}
} ( ctx )
2023-07-26 16:21:04 +00:00
ticker := time . NewTicker ( api . Options . DERPMapUpdateFrequency )
defer ticker . Stop ( )
var lastDERPMap * tailcfg . DERPMap
for {
derpMap := api . DERPMap ( )
if lastDERPMap == nil || ! tailnet . CompareDERPMaps ( lastDERPMap , derpMap ) {
err := json . NewEncoder ( nconn ) . Encode ( derpMap )
if err != nil {
2023-08-28 18:13:19 +00:00
_ = nconn . Close ( )
2023-07-26 16:21:04 +00:00
return
}
lastDERPMap = derpMap
}
2023-08-01 15:50:43 +00:00
ticker . Reset ( api . Options . DERPMapUpdateFrequency )
2023-07-26 16:21:04 +00:00
select {
case <- ctx . Done ( ) :
return
case <- api . ctx . Done ( ) :
return
case <- ticker . C :
}
}
}
2023-01-05 14:27:10 +00:00
// @Summary Coordinate workspace agent via Tailnet
// @Description It accepts a WebSocket connection to an agent that listens to
// @Description incoming connections and publishes node updates.
2023-01-13 11:27:21 +00:00
// @ID coordinate-workspace-agent-via-tailnet
2023-01-05 14:27:10 +00:00
// @Security CoderSessionToken
// @Tags Agents
// @Success 101
// @Router /workspaceagents/me/coordinate [get]
2022-09-01 01:09:44 +00:00
func ( api * API ) workspaceAgentCoordinate ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-11-16 22:34:06 +00:00
api . WebsocketWaitMutex . Lock ( )
api . WebsocketWaitGroup . Add ( 1 )
api . WebsocketWaitMutex . Unlock ( )
defer api . WebsocketWaitGroup . Done ( )
2022-09-01 01:09:44 +00:00
workspaceAgent := httpmw . WorkspaceAgent ( r )
2022-09-21 22:07:00 +00:00
resource , err := api . Database . GetWorkspaceResourceByID ( ctx , workspaceAgent . ResourceID )
2022-09-20 00:46:29 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-09-20 00:46:29 +00:00
Message : "Failed to accept websocket." ,
Detail : err . Error ( ) ,
} )
return
}
2022-09-21 22:07:00 +00:00
build , err := api . Database . GetWorkspaceBuildByJobID ( ctx , resource . JobID )
2022-09-20 00:46:29 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-09-20 00:46:29 +00:00
Message : "Internal error fetching workspace build job." ,
Detail : err . Error ( ) ,
} )
return
}
2023-01-25 21:27:36 +00:00
workspace , err := api . Database . GetWorkspaceByID ( ctx , build . WorkspaceID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Internal error fetching workspace." ,
Detail : err . Error ( ) ,
} )
return
}
2023-01-26 03:23:14 +00:00
owner , err := api . Database . GetUserByID ( ctx , workspace . OwnerID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Internal error fetching user." ,
Detail : err . Error ( ) ,
} )
return
}
2022-09-20 00:46:29 +00:00
// Ensure the resource is still valid!
// We only accept agents for resources on the latest build.
ensureLatestBuild := func ( ) error {
2022-09-21 22:07:00 +00:00
latestBuild , err := api . Database . GetLatestWorkspaceBuildByWorkspaceID ( ctx , build . WorkspaceID )
2022-09-20 00:46:29 +00:00
if err != nil {
return err
}
if build . ID != latestBuild . ID {
return xerrors . New ( "build is outdated" )
}
return nil
}
err = ensureLatestBuild ( )
if err != nil {
2022-09-21 22:07:00 +00:00
api . Logger . Debug ( ctx , "agent tried to connect from non-latest built" ,
2022-09-20 00:46:29 +00:00
slog . F ( "resource" , resource ) ,
slog . F ( "agent" , workspaceAgent ) ,
)
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusForbidden , codersdk . Response {
2022-09-20 00:46:29 +00:00
Message : "Agent trying to connect from non-latest build." ,
Detail : err . Error ( ) ,
} )
return
}
2022-09-01 01:09:44 +00:00
conn , err := websocket . Accept ( rw , r , nil )
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-09-01 01:09:44 +00:00
Message : "Failed to accept websocket." ,
Detail : err . Error ( ) ,
} )
return
}
2022-10-04 22:10:58 +00:00
2022-09-21 22:07:00 +00:00
ctx , wsNetConn := websocketNetConn ( ctx , conn , websocket . MessageBinary )
2022-09-20 00:46:29 +00:00
defer wsNetConn . Close ( )
2023-03-13 09:54:53 +00:00
// We use a custom heartbeat routine here instead of `httpapi.Heartbeat`
// because we want to log the agent's last ping time.
2023-04-06 01:58:54 +00:00
var lastPing atomic . Pointer [ time . Time ]
lastPing . Store ( ptr . Ref ( time . Now ( ) ) ) // Since the agent initiated the request, assume it's alive.
2023-03-13 09:54:53 +00:00
go pprof . Do ( ctx , pprof . Labels ( "agent" , workspaceAgent . ID . String ( ) ) , func ( ctx context . Context ) {
// TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout?
t := time . NewTicker ( api . AgentConnectionUpdateFrequency )
defer t . Stop ( )
for {
select {
case <- t . C :
case <- ctx . Done ( ) :
return
}
// We don't need a context that times out here because the ping will
// eventually go through. If the context times out, then other
// websocket read operations will receive an error, obfuscating the
// actual problem.
err := conn . Ping ( ctx )
if err != nil {
return
}
2023-04-06 01:58:54 +00:00
lastPing . Store ( ptr . Ref ( time . Now ( ) ) )
2023-03-13 09:54:53 +00:00
}
} )
2022-09-20 00:46:29 +00:00
firstConnectedAt := workspaceAgent . FirstConnectedAt
if ! firstConnectedAt . Valid {
firstConnectedAt = sql . NullTime {
2023-09-01 16:50:12 +00:00
Time : dbtime . Now ( ) ,
2022-09-20 00:46:29 +00:00
Valid : true ,
}
}
lastConnectedAt := sql . NullTime {
2023-09-01 16:50:12 +00:00
Time : dbtime . Now ( ) ,
2022-09-20 00:46:29 +00:00
Valid : true ,
}
disconnectedAt := workspaceAgent . DisconnectedAt
2023-02-10 18:23:02 +00:00
updateConnectionTimes := func ( ctx context . Context ) error {
2023-04-07 20:21:52 +00:00
//nolint:gocritic // We only update ourself.
err = api . Database . UpdateWorkspaceAgentConnectionByID ( dbauthz . AsSystemRestricted ( ctx ) , database . UpdateWorkspaceAgentConnectionByIDParams {
2022-09-20 00:46:29 +00:00
ID : workspaceAgent . ID ,
FirstConnectedAt : firstConnectedAt ,
LastConnectedAt : lastConnectedAt ,
DisconnectedAt : disconnectedAt ,
2023-09-01 16:50:12 +00:00
UpdatedAt : dbtime . Now ( ) ,
2022-11-06 21:27:09 +00:00
LastConnectedReplicaID : uuid . NullUUID {
UUID : api . ID ,
Valid : true ,
} ,
2022-09-20 00:46:29 +00:00
} )
if err != nil {
return err
}
return nil
}
defer func ( ) {
2023-02-10 18:23:02 +00:00
// If connection closed then context will be canceled, try to
// ensure our final update is sent. By waiting at most the agent
// inactive disconnect timeout we ensure that we don't block but
// also guarantee that the agent will be considered disconnected
// by normal status check.
2023-02-14 14:27:06 +00:00
//
// Use a system context as the agent has disconnected and that token
// may no longer be valid.
//nolint:gocritic
2023-02-15 16:14:37 +00:00
ctx , cancel := context . WithTimeout ( dbauthz . AsSystemRestricted ( api . ctx ) , api . AgentInactiveDisconnectTimeout )
2023-02-10 18:23:02 +00:00
defer cancel ( )
2023-03-13 09:54:53 +00:00
// Only update timestamp if the disconnect is new.
if ! disconnectedAt . Valid {
disconnectedAt = sql . NullTime {
2023-09-01 16:50:12 +00:00
Time : dbtime . Now ( ) ,
2023-03-13 09:54:53 +00:00
Valid : true ,
}
2022-09-20 00:46:29 +00:00
}
2023-02-14 14:27:06 +00:00
err := updateConnectionTimes ( ctx )
if err != nil {
// This is a bug with unit tests that cancel the app context and
// cause this error log to be generated. We should fix the unit tests
// as this is a valid log.
2023-02-22 22:07:26 +00:00
//
// The pq error occurs when the server is shutting down.
if ! xerrors . Is ( err , context . Canceled ) && ! database . IsQueryCanceledError ( err ) {
2023-02-14 14:27:06 +00:00
api . Logger . Error ( ctx , "failed to update agent disconnect time" ,
slog . Error ( err ) ,
2023-06-20 10:30:45 +00:00
slog . F ( "workspace_id" , build . WorkspaceID ) ,
2023-02-14 14:27:06 +00:00
)
}
}
2023-02-10 18:23:02 +00:00
api . publishWorkspaceUpdate ( ctx , build . WorkspaceID )
2022-09-20 00:46:29 +00:00
} ( )
2023-02-10 18:23:02 +00:00
err = updateConnectionTimes ( ctx )
2022-09-01 01:09:44 +00:00
if err != nil {
2022-09-22 18:26:05 +00:00
_ = conn . Close ( websocket . StatusGoingAway , err . Error ( ) )
2022-09-01 01:09:44 +00:00
return
}
2022-11-07 15:25:18 +00:00
api . publishWorkspaceUpdate ( ctx , build . WorkspaceID )
2022-09-20 00:46:29 +00:00
2023-06-21 10:00:38 +00:00
api . Logger . Debug ( ctx , "accepting agent" ,
2023-05-14 20:23:13 +00:00
slog . F ( "owner" , owner . Username ) ,
slog . F ( "workspace" , workspace . Name ) ,
slog . F ( "name" , workspaceAgent . Name ) ,
)
api . Logger . Debug ( ctx , "accepting agent details" , slog . F ( "agent" , workspaceAgent ) )
2022-09-20 00:46:29 +00:00
defer conn . Close ( websocket . StatusNormalClosure , "" )
closeChan := make ( chan struct { } )
go func ( ) {
defer close ( closeChan )
2023-01-26 03:23:14 +00:00
err := ( * api . TailnetCoordinator . Load ( ) ) . ServeAgent ( wsNetConn , workspaceAgent . ID ,
fmt . Sprintf ( "%s-%s-%s" , owner . Username , workspace . Name , workspaceAgent . Name ) ,
)
2022-09-20 00:46:29 +00:00
if err != nil {
2022-10-17 13:43:30 +00:00
api . Logger . Warn ( ctx , "tailnet coordinator agent error" , slog . Error ( err ) )
2022-09-20 00:46:29 +00:00
_ = conn . Close ( websocket . StatusInternalError , err . Error ( ) )
return
}
} ( )
ticker := time . NewTicker ( api . AgentConnectionUpdateFrequency )
defer ticker . Stop ( )
for {
select {
case <- closeChan :
return
case <- ticker . C :
}
2023-03-13 09:54:53 +00:00
2023-04-06 01:58:54 +00:00
lastPing := * lastPing . Load ( )
2023-03-13 09:54:53 +00:00
var connectionStatusChanged bool
if time . Since ( lastPing ) > api . AgentInactiveDisconnectTimeout {
if ! disconnectedAt . Valid {
connectionStatusChanged = true
disconnectedAt = sql . NullTime {
2023-09-01 16:50:12 +00:00
Time : dbtime . Now ( ) ,
2023-03-13 09:54:53 +00:00
Valid : true ,
}
}
} else {
connectionStatusChanged = disconnectedAt . Valid
// TODO(mafredri): Should we update it here or allow lastConnectedAt to shadow it?
disconnectedAt = sql . NullTime { }
lastConnectedAt = sql . NullTime {
2023-09-01 16:50:12 +00:00
Time : dbtime . Now ( ) ,
2023-03-13 09:54:53 +00:00
Valid : true ,
}
2022-09-20 00:46:29 +00:00
}
2023-02-10 18:23:02 +00:00
err = updateConnectionTimes ( ctx )
2022-09-20 00:46:29 +00:00
if err != nil {
2022-09-22 18:26:05 +00:00
_ = conn . Close ( websocket . StatusGoingAway , err . Error ( ) )
2022-09-20 00:46:29 +00:00
return
}
2023-03-13 09:54:53 +00:00
if connectionStatusChanged {
api . publishWorkspaceUpdate ( ctx , build . WorkspaceID )
}
2022-09-20 00:46:29 +00:00
err := ensureLatestBuild ( )
if err != nil {
// Disconnect agents that are no longer valid.
_ = conn . Close ( websocket . StatusGoingAway , "" )
return
}
}
2022-09-01 01:09:44 +00:00
}
// workspaceAgentClientCoordinate accepts a WebSocket that reads node network updates.
// After accept a PubSub starts listening for new connection node updates
// which are written to the WebSocket.
2023-01-13 11:27:21 +00:00
//
// @Summary Coordinate workspace agent
// @ID coordinate-workspace-agent
// @Security CoderSessionToken
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Success 101
// @Router /workspaceagents/{workspaceagent}/coordinate [get]
2022-09-01 01:09:44 +00:00
func ( api * API ) workspaceAgentClientCoordinate ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2023-04-17 19:57:21 +00:00
// This route accepts user API key auth and workspace proxy auth. The moon actor has
// full permissions so should be able to pass this authz check.
2022-09-01 01:09:44 +00:00
workspace := httpmw . WorkspaceParam ( r )
if ! api . Authorize ( r , rbac . ActionCreate , workspace . ExecutionRBAC ( ) ) {
httpapi . ResourceNotFound ( rw )
return
}
2023-04-17 19:57:21 +00:00
2022-09-22 15:14:22 +00:00
// This is used by Enterprise code to control the functionality of this route.
override := api . WorkspaceClientCoordinateOverride . Load ( )
2022-09-22 16:03:49 +00:00
if override != nil {
overrideFunc := * override
if overrideFunc != nil && overrideFunc ( rw ) {
return
}
2022-09-22 15:14:22 +00:00
}
2022-09-01 01:09:44 +00:00
2022-11-16 22:34:06 +00:00
api . WebsocketWaitMutex . Lock ( )
api . WebsocketWaitGroup . Add ( 1 )
api . WebsocketWaitMutex . Unlock ( )
defer api . WebsocketWaitGroup . Done ( )
2022-09-01 01:09:44 +00:00
workspaceAgent := httpmw . WorkspaceAgentParam ( r )
conn , err := websocket . Accept ( rw , r , nil )
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-09-01 01:09:44 +00:00
Message : "Failed to accept websocket." ,
Detail : err . Error ( ) ,
} )
return
}
2023-02-14 14:42:55 +00:00
ctx , wsNetConn := websocketNetConn ( ctx , conn , websocket . MessageBinary )
defer wsNetConn . Close ( )
2022-10-04 22:10:58 +00:00
go httpapi . Heartbeat ( ctx , conn )
2022-09-01 01:09:44 +00:00
defer conn . Close ( websocket . StatusNormalClosure , "" )
2023-02-14 14:42:55 +00:00
err = ( * api . TailnetCoordinator . Load ( ) ) . ServeClient ( wsNetConn , uuid . New ( ) , workspaceAgent . ID )
2022-09-01 01:09:44 +00:00
if err != nil {
_ = conn . Close ( websocket . StatusInternalError , err . Error ( ) )
return
}
}
2023-09-12 13:25:10 +00:00
// convertProvisionedApps converts applications that are in the middle of provisioning process.
// It means that they may not have an agent or workspace assigned (dry-run job).
func convertProvisionedApps ( dbApps [ ] database . WorkspaceApp ) [ ] codersdk . WorkspaceApp {
return convertApps ( dbApps , database . WorkspaceAgent { } , database . User { } , database . Workspace { } )
}
func convertApps ( dbApps [ ] database . WorkspaceApp , agent database . WorkspaceAgent , owner database . User , workspace database . Workspace ) [ ] codersdk . WorkspaceApp {
2022-06-04 20:13:37 +00:00
apps := make ( [ ] codersdk . WorkspaceApp , 0 )
for _ , dbApp := range dbApps {
2023-09-12 13:25:10 +00:00
var subdomainName string
if dbApp . Subdomain && agent . Name != "" && owner . Username != "" && workspace . Name != "" {
appSlug := dbApp . Slug
if appSlug == "" {
appSlug = dbApp . DisplayName
}
subdomainName = httpapi . ApplicationURL {
AppSlugOrPort : appSlug ,
AgentName : agent . Name ,
WorkspaceName : workspace . Name ,
Username : owner . Username ,
} . String ( )
}
2022-06-04 20:13:37 +00:00
apps = append ( apps , codersdk . WorkspaceApp {
2023-09-12 13:25:10 +00:00
ID : dbApp . ID ,
URL : dbApp . Url . String ,
External : dbApp . External ,
Slug : dbApp . Slug ,
DisplayName : dbApp . DisplayName ,
Command : dbApp . Command . String ,
Icon : dbApp . Icon ,
Subdomain : dbApp . Subdomain ,
SubdomainName : subdomainName ,
SharingLevel : codersdk . WorkspaceAppSharingLevel ( dbApp . SharingLevel ) ,
2022-09-23 19:51:04 +00:00
Healthcheck : codersdk . Healthcheck {
URL : dbApp . HealthcheckUrl ,
Interval : dbApp . HealthcheckInterval ,
Threshold : dbApp . HealthcheckThreshold ,
} ,
Health : codersdk . WorkspaceAppHealth ( dbApp . Health ) ,
2022-06-04 20:13:37 +00:00
} )
}
return apps
}
2023-03-31 20:26:19 +00:00
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
}
2022-11-16 10:53:02 +00:00
func convertWorkspaceAgent ( derpMap * tailcfg . DERPMap , coordinator tailnet . Coordinator , dbAgent database . WorkspaceAgent , apps [ ] codersdk . WorkspaceApp , agentInactiveDisconnectTimeout time . Duration , agentFallbackTroubleshootingURL string ) ( codersdk . WorkspaceAgent , error ) {
2022-04-11 21:06:15 +00:00
var envs map [ string ] string
if dbAgent . EnvironmentVariables . Valid {
err := json . Unmarshal ( dbAgent . EnvironmentVariables . RawMessage , & envs )
if err != nil {
2022-09-01 01:09:44 +00:00
return codersdk . WorkspaceAgent { } , xerrors . Errorf ( "unmarshal env vars: %w" , err )
2022-04-11 21:06:15 +00:00
}
}
2022-11-16 10:53:02 +00:00
troubleshootingURL := agentFallbackTroubleshootingURL
if dbAgent . TroubleshootingURL != "" {
troubleshootingURL = dbAgent . TroubleshootingURL
}
2023-08-09 05:10:28 +00:00
subsystems := make ( [ ] codersdk . AgentSubsystem , len ( dbAgent . Subsystems ) )
for i , subsystem := range dbAgent . Subsystems {
subsystems [ i ] = codersdk . AgentSubsystem ( subsystem )
}
2022-04-26 01:03:54 +00:00
workspaceAgent := codersdk . WorkspaceAgent {
2023-03-06 19:34:00 +00:00
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 ,
2023-06-20 11:41:55 +00:00
StartupScriptBehavior : codersdk . WorkspaceAgentStartupScriptBehavior ( dbAgent . StartupScriptBehavior ) ,
StartupScriptTimeoutSeconds : dbAgent . StartupScriptTimeoutSeconds ,
2023-07-28 15:57:23 +00:00
LogsLength : dbAgent . LogsLength ,
LogsOverflowed : dbAgent . LogsOverflowed ,
2023-03-06 19:34:00 +00:00
Version : dbAgent . Version ,
EnvironmentVariables : envs ,
Directory : dbAgent . Directory ,
ExpandedDirectory : dbAgent . ExpandedDirectory ,
Apps : apps ,
ConnectionTimeoutSeconds : dbAgent . ConnectionTimeoutSeconds ,
TroubleshootingURL : troubleshootingURL ,
LifecycleState : codersdk . WorkspaceAgentLifecycle ( dbAgent . LifecycleState ) ,
2023-06-09 13:01:56 +00:00
LoginBeforeReady : dbAgent . StartupScriptBehavior != database . StartupScriptBehaviorBlocking ,
2023-03-06 19:34:00 +00:00
ShutdownScript : dbAgent . ShutdownScript . String ,
ShutdownScriptTimeoutSeconds : dbAgent . ShutdownScriptTimeoutSeconds ,
2023-08-09 05:10:28 +00:00
Subsystems : subsystems ,
2023-08-30 19:53:42 +00:00
DisplayApps : convertDisplayApps ( dbAgent . DisplayApps ) ,
2022-09-01 01:09:44 +00:00
}
node := coordinator . Node ( dbAgent . ID )
if node != nil {
workspaceAgent . DERPLatency = map [ string ] codersdk . DERPRegion { }
for rawRegion , latency := range node . DERPLatency {
regionParts := strings . SplitN ( rawRegion , "-" , 2 )
regionID , err := strconv . Atoi ( regionParts [ 0 ] )
if err != nil {
return codersdk . WorkspaceAgent { } , xerrors . Errorf ( "convert derp region id %q: %w" , rawRegion , err )
}
region , found := derpMap . Regions [ regionID ]
if ! found {
2022-09-01 18:43:52 +00:00
// It's possible that a workspace agent is using an old DERPMap
// and reports regions that do not exist. If that's the case,
// report the region as unknown!
region = & tailcfg . DERPRegion {
RegionID : regionID ,
RegionName : fmt . Sprintf ( "Unnamed %d" , regionID ) ,
}
2022-09-01 01:09:44 +00:00
}
workspaceAgent . DERPLatency [ region . RegionName ] = codersdk . DERPRegion {
Preferred : node . PreferredDERP == regionID ,
LatencyMilliseconds : latency * 1000 ,
}
}
2022-06-24 15:25:01 +00:00
}
2023-03-30 13:24:51 +00:00
status := dbAgent . Status ( agentInactiveDisconnectTimeout )
workspaceAgent . Status = codersdk . WorkspaceAgentStatus ( status . Status )
workspaceAgent . FirstConnectedAt = status . FirstConnectedAt
workspaceAgent . LastConnectedAt = status . LastConnectedAt
workspaceAgent . DisconnectedAt = status . DisconnectedAt
2022-04-11 21:06:15 +00:00
2023-06-20 11:41:55 +00:00
if dbAgent . StartedAt . Valid {
workspaceAgent . StartedAt = & dbAgent . StartedAt . Time
}
if dbAgent . ReadyAt . Valid {
workspaceAgent . ReadyAt = & dbAgent . ReadyAt . Time
}
2023-07-10 09:40:11 +00:00
switch {
case workspaceAgent . Status != codersdk . WorkspaceAgentConnected && workspaceAgent . LifecycleState == codersdk . WorkspaceAgentLifecycleOff :
workspaceAgent . Health . Reason = "agent is not running"
case workspaceAgent . Status == codersdk . WorkspaceAgentTimeout :
workspaceAgent . Health . Reason = "agent is taking too long to connect"
case workspaceAgent . Status == codersdk . WorkspaceAgentDisconnected :
workspaceAgent . Health . Reason = "agent has lost connection"
// Note: We could also handle codersdk.WorkspaceAgentLifecycleStartTimeout
// here, but it's more of a soft issue, so we don't want to mark the agent
// as unhealthy.
case workspaceAgent . LifecycleState == codersdk . WorkspaceAgentLifecycleStartError :
workspaceAgent . Health . Reason = "agent startup script exited with an error"
case workspaceAgent . LifecycleState . ShuttingDown ( ) :
workspaceAgent . Health . Reason = "agent is shutting down"
default :
workspaceAgent . Health . Healthy = true
}
2022-04-26 01:03:54 +00:00
return workspaceAgent , nil
2022-04-11 21:06:15 +00:00
}
2022-10-04 22:10:58 +00:00
2023-08-30 19:53:42 +00:00
func convertDisplayApps ( apps [ ] database . DisplayApp ) [ ] codersdk . DisplayApp {
dapps := make ( [ ] codersdk . DisplayApp , 0 , len ( apps ) )
for _ , app := range apps {
switch codersdk . DisplayApp ( app ) {
case codersdk . DisplayAppVSCodeDesktop , codersdk . DisplayAppVSCodeInsiders , codersdk . DisplayAppPortForward , codersdk . DisplayAppWebTerminal , codersdk . DisplayAppSSH :
dapps = append ( dapps , codersdk . DisplayApp ( app ) )
}
}
return dapps
}
2023-01-05 14:27:10 +00:00
// @Summary Submit workspace agent stats
2023-01-13 11:27:21 +00:00
// @ID submit-workspace-agent-stats
2023-01-05 14:27:10 +00:00
// @Security CoderSessionToken
2023-01-13 11:27:21 +00:00
// @Accept json
2023-01-13 15:47:38 +00:00
// @Produce json
2023-01-05 14:27:10 +00:00
// @Tags Agents
2023-01-29 21:47:24 +00:00
// @Param request body agentsdk.Stats true "Stats request"
// @Success 200 {object} agentsdk.StatsResponse
2023-01-05 14:27:10 +00:00
// @Router /workspaceagents/me/report-stats [post]
2022-09-01 19:58:23 +00:00
func ( api * API ) workspaceAgentReportStats ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-09-01 19:58:23 +00:00
workspaceAgent := httpmw . WorkspaceAgent ( r )
2022-11-18 22:46:53 +00:00
workspace , err := api . Database . GetWorkspaceByAgentID ( ctx , workspaceAgent . ID )
2022-09-01 19:58:23 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-11-18 22:46:53 +00:00
Message : "Failed to get workspace." ,
2022-09-01 19:58:23 +00:00
Detail : err . Error ( ) ,
} )
return
}
2023-01-29 21:47:24 +00:00
var req agentsdk . Stats
2022-11-18 22:46:53 +00:00
if ! httpapi . Read ( ctx , rw , r , & req ) {
return
}
2023-03-07 14:25:04 +00:00
// An empty stat means it's just looking for the report interval.
if req . ConnectionsByProto == nil {
2023-01-29 21:47:24 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , agentsdk . StatsResponse {
2022-11-18 22:46:53 +00:00
ReportInterval : api . AgentStatsRefreshInterval ,
2022-09-01 19:58:23 +00:00
} )
return
}
2022-12-13 07:03:03 +00:00
api . Logger . Debug ( ctx , "read stats report" ,
slog . F ( "interval" , api . AgentStatsRefreshInterval ) ,
2023-06-20 10:30:45 +00:00
slog . F ( "workspace_agent_id" , workspaceAgent . ID ) ,
slog . F ( "workspace_id" , workspace . ID ) ,
2022-12-13 07:03:03 +00:00
slog . F ( "payload" , req ) ,
)
2023-03-07 14:25:04 +00:00
if req . ConnectionCount > 0 {
activityBumpWorkspace ( ctx , api . Logger . Named ( "activity_bump" ) , api . Database , workspace . ID )
}
2022-11-18 22:46:53 +00:00
2023-09-01 16:50:12 +00:00
now := dbtime . Now ( )
2022-11-18 22:46:53 +00:00
2023-04-27 10:34:00 +00:00
var errGroup errgroup . Group
errGroup . Go ( func ( ) error {
2023-08-08 02:55:31 +00:00
if err := api . statsBatcher . Add ( time . Now ( ) , workspaceAgent . ID , workspace . TemplateID , workspace . OwnerID , workspace . ID , req ) ; err != nil {
2023-08-04 16:00:42 +00:00
api . Logger . Error ( ctx , "failed to add stats to batcher" , slog . Error ( err ) )
2023-04-27 10:34:00 +00:00
return xerrors . Errorf ( "can't insert workspace agent stat: %w" , err )
}
return nil
} )
errGroup . Go ( func ( ) error {
err := api . Database . UpdateWorkspaceLastUsedAt ( ctx , database . UpdateWorkspaceLastUsedAtParams {
2023-03-07 14:25:04 +00:00
ID : workspace . ID ,
LastUsedAt : now ,
} )
if err != nil {
2023-04-27 10:34:00 +00:00
return xerrors . Errorf ( "can't update workspace LastUsedAt: %w" , err )
2023-03-07 14:25:04 +00:00
}
2023-04-27 10:34:00 +00:00
return nil
} )
if api . Options . UpdateAgentMetrics != nil {
errGroup . Go ( func ( ) error {
user , err := api . Database . GetUserByID ( ctx , workspace . OwnerID )
if err != nil {
return xerrors . Errorf ( "can't get user: %w" , err )
}
api . Options . UpdateAgentMetrics ( ctx , user . Username , workspace . Name , workspaceAgent . Name , req . Metrics )
return nil
} )
}
err = errGroup . Wait ( )
if err != nil {
httpapi . InternalServerError ( rw , err )
return
2022-11-18 22:46:53 +00:00
}
2023-01-29 21:47:24 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , agentsdk . StatsResponse {
2022-11-18 22:46:53 +00:00
ReportInterval : api . AgentStatsRefreshInterval ,
} )
}
2023-08-22 18:55:00 +00:00
func ellipse ( v string , n int ) string {
if len ( v ) > n {
return v [ : n ] + "..."
}
return v
}
2023-03-31 20:26:19 +00:00
// @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 (
2023-08-24 20:18:42 +00:00
// maxValueLen is set to 2048 to stay under the 8000 byte Postgres
// NOTIFY limit. Since both value and error can be set, the real
// payload limit is 2 * 2048 * 4/3 <base64 expansion> = 5461 bytes + a few hundred bytes for JSON
// syntax, key names, and metadata.
maxValueLen = 2048
2023-03-31 20:26:19 +00:00
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" ,
2023-06-20 10:30:45 +00:00
slog . F ( "workspace_agent_id" , workspaceAgent . ID ) ,
slog . F ( "workspace_id" , workspace . ID ) ,
2023-03-31 20:26:19 +00:00
slog . F ( "collected_at" , datum . CollectedAt ) ,
slog . F ( "key" , datum . Key ) ,
2023-08-22 18:55:00 +00:00
slog . F ( "value" , ellipse ( datum . Value , 16 ) ) ,
2023-03-31 20:26:19 +00:00
)
2023-08-24 20:18:42 +00:00
datumJSON , err := json . Marshal ( datum )
if err != nil {
httpapi . InternalServerError ( rw , err )
return
}
err = api . Pubsub . Publish ( watchWorkspaceAgentMetadataChannel ( workspaceAgent . ID ) , datumJSON )
2023-03-31 20:26:19 +00:00
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 )
2023-08-22 18:55:00 +00:00
log = api . Logger . Named ( "workspace_metadata_watcher" ) . With (
slog . F ( "workspace_agent_id" , workspaceAgent . ID ) ,
)
2023-03-31 20:26:19 +00:00
)
2023-08-24 20:18:42 +00:00
// We avoid channel-based synchronization here to avoid backpressure problems.
var (
metadataMapMu sync . Mutex
metadataMap = make ( map [ string ] database . WorkspaceAgentMetadatum )
// pendingChanges must only be mutated when metadataMapMu is held.
pendingChanges atomic . Bool
)
// Send metadata on updates, we must ensure subscription before sending
// initial metadata to guarantee that events in-between are not missed.
cancelSub , err := api . Pubsub . Subscribe ( watchWorkspaceAgentMetadataChannel ( workspaceAgent . ID ) , func ( _ context . Context , byt [ ] byte ) {
var update database . UpdateWorkspaceAgentMetadataParams
err := json . Unmarshal ( byt , & update )
if err != nil {
api . Logger . Error ( ctx , "failed to unmarshal pubsub message" , slog . Error ( err ) )
return
}
log . Debug ( ctx , "received metadata update" , "key" , update . Key )
metadataMapMu . Lock ( )
defer metadataMapMu . Unlock ( )
md := metadataMap [ update . Key ]
md . Value = update . Value
md . Error = update . Error
md . CollectedAt = update . CollectedAt
metadataMap [ update . Key ] = md
pendingChanges . Store ( true )
} )
if err != nil {
httpapi . InternalServerError ( rw , err )
return
}
defer cancelSub ( )
sseSendEvent , sseSenderClosed , err := httpapi . ServerSentEventSender ( rw , r )
2023-03-31 20:26:19 +00:00
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 ( ) {
2023-08-24 20:18:42 +00:00
<- sseSenderClosed
2023-03-31 20:26:19 +00:00
} ( )
2023-08-24 20:18:42 +00:00
// We send updates exactly every second.
const sendInterval = time . Second * 1
sendTicker := time . NewTicker ( sendInterval )
defer sendTicker . Stop ( )
2023-03-31 20:26:19 +00:00
2023-08-24 20:18:42 +00:00
// We always use the original Request context because it contains
// the RBAC actor.
md , err := api . Database . GetWorkspaceAgentMetadata ( ctx , workspaceAgent . ID )
if err != nil {
// If we can't successfully pull the initial metadata, pubsub
// updates will be no-op so we may as well terminate the
// connection early.
httpapi . InternalServerError ( rw , err )
return
}
2023-03-31 20:26:19 +00:00
2023-08-24 20:18:42 +00:00
metadataMapMu . Lock ( )
for _ , datum := range md {
metadataMap [ datum . Key ] = datum
}
metadataMapMu . Unlock ( )
2023-03-31 20:26:19 +00:00
2023-08-24 20:18:42 +00:00
// Send initial metadata.
2023-03-31 20:26:19 +00:00
2023-08-24 20:18:42 +00:00
var lastSend time . Time
sendMetadata := func ( ) {
metadataMapMu . Lock ( )
values := maps . Values ( metadataMap )
pendingChanges . Store ( false )
metadataMapMu . Unlock ( )
2023-03-31 20:26:19 +00:00
2023-08-24 20:18:42 +00:00
lastSend = time . Now ( )
_ = sseSendEvent ( ctx , codersdk . ServerSentEvent {
2023-03-31 20:26:19 +00:00
Type : codersdk . ServerSentEventTypeData ,
2023-08-24 20:18:42 +00:00
Data : convertWorkspaceAgentMetadata ( values ) ,
2023-03-31 20:26:19 +00:00
} )
}
2023-08-24 20:18:42 +00:00
sendMetadata ( )
2023-06-13 12:21:06 +00:00
2023-03-31 20:26:19 +00:00
for {
select {
2023-08-24 20:18:42 +00:00
case <- sendTicker . C :
// We send an update even if there's no change every 5 seconds
// to ensure that the frontend always has an accurate "Result.Age".
if ! pendingChanges . Load ( ) && time . Since ( lastSend ) < time . Second * 5 {
continue
}
sendMetadata ( )
case <- sseSenderClosed :
2023-03-31 20:26:19 +00:00
return
}
}
}
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 ,
2023-09-18 08:17:18 +00:00
CollectedAt : datum . CollectedAt . UTC ( ) ,
2023-03-31 20:26:19 +00:00
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 ,
} ,
} )
}
2023-08-24 20:18:42 +00:00
// Sorting prevents the metadata from jumping around in the frontend.
sort . Slice ( result , func ( i , j int ) bool {
return result [ i ] . Description . Key < result [ j ] . Description . Key
} )
2023-03-31 20:26:19 +00:00
return result
}
func watchWorkspaceAgentMetadataChannel ( id uuid . UUID ) string {
return "workspace_agent_metadata:" + id . String ( )
}
2023-01-24 12:24:27 +00:00
// @Summary Submit workspace agent lifecycle state
// @ID submit-workspace-agent-lifecycle-state
// @Security CoderSessionToken
// @Accept json
// @Tags Agents
2023-01-29 21:47:24 +00:00
// @Param request body agentsdk.PostLifecycleRequest true "Workspace agent lifecycle request"
2023-01-24 12:24:27 +00:00
// @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
}
2023-01-29 21:47:24 +00:00
var req agentsdk . PostLifecycleRequest
2023-01-24 12:24:27 +00:00
if ! httpapi . Read ( ctx , rw , r , & req ) {
return
}
2023-06-16 14:14:22 +00:00
logger := api . Logger . With (
2023-06-20 10:30:45 +00:00
slog . F ( "workspace_agent_id" , workspaceAgent . ID ) ,
slog . F ( "workspace_id" , workspace . ID ) ,
2023-01-24 12:24:27 +00:00
slog . F ( "payload" , req ) ,
)
2023-06-16 14:14:22 +00:00
logger . Debug ( ctx , "workspace agent state report" )
2023-01-24 12:24:27 +00:00
2023-06-16 14:14:22 +00:00
lifecycleState := req . State
dbLifecycleState := database . WorkspaceAgentLifecycleState ( lifecycleState )
if ! dbLifecycleState . Valid ( ) {
2023-01-24 12:24:27 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Invalid lifecycle state." ,
2023-06-16 14:14:22 +00:00
Detail : fmt . Sprintf ( "Invalid lifecycle state %q, must be be one of %q." , lifecycleState , database . AllWorkspaceAgentLifecycleStateValues ( ) ) ,
2023-01-24 12:24:27 +00:00
} )
return
}
2023-06-20 11:41:55 +00:00
if req . ChangedAt . IsZero ( ) {
// Backwards compatibility with older agents.
2023-09-01 16:50:12 +00:00
req . ChangedAt = dbtime . Now ( )
2023-06-20 11:41:55 +00:00
}
changedAt := sql . NullTime { Time : req . ChangedAt , Valid : true }
startedAt := workspaceAgent . StartedAt
readyAt := workspaceAgent . ReadyAt
switch lifecycleState {
case codersdk . WorkspaceAgentLifecycleStarting :
startedAt = changedAt
readyAt . Valid = false // This agent is re-starting, so it's not ready yet.
case codersdk . WorkspaceAgentLifecycleReady , codersdk . WorkspaceAgentLifecycleStartError :
readyAt = changedAt
}
2023-01-24 12:24:27 +00:00
err = api . Database . UpdateWorkspaceAgentLifecycleStateByID ( ctx , database . UpdateWorkspaceAgentLifecycleStateByIDParams {
ID : workspaceAgent . ID ,
2023-06-16 14:14:22 +00:00
LifecycleState : dbLifecycleState ,
2023-06-20 11:41:55 +00:00
StartedAt : startedAt ,
ReadyAt : readyAt ,
2023-01-24 12:24:27 +00:00
} )
if err != nil {
2023-08-31 08:08:18 +00:00
if ! xerrors . Is ( err , context . Canceled ) {
// not an error if we are canceled
logger . Error ( ctx , "failed to update lifecycle state" , slog . Error ( err ) )
}
2023-01-24 12:24:27 +00:00
httpapi . InternalServerError ( rw , err )
return
}
2023-06-16 14:14:22 +00:00
2023-01-24 12:24:27 +00:00
api . publishWorkspaceUpdate ( ctx , workspace . ID )
httpapi . Write ( ctx , rw , http . StatusNoContent , nil )
}
2023-01-13 11:27:21 +00:00
// @Summary Submit workspace agent application health
// @ID submit-workspace-agent-application-health
2023-01-05 14:27:10 +00:00
// @Security CoderSessionToken
2023-01-13 11:27:21 +00:00
// @Accept json
2023-01-13 15:47:38 +00:00
// @Produce json
2023-01-05 14:27:10 +00:00
// @Tags Agents
2023-01-29 21:47:24 +00:00
// @Param request body agentsdk.PostAppHealthsRequest true "Application health request"
2023-01-05 14:27:10 +00:00
// @Success 200
// @Router /workspaceagents/me/app-health [post]
2022-09-23 19:51:04 +00:00
func ( api * API ) postWorkspaceAppHealth ( rw http . ResponseWriter , r * http . Request ) {
2022-11-18 22:46:53 +00:00
ctx := r . Context ( )
2022-09-23 19:51:04 +00:00
workspaceAgent := httpmw . WorkspaceAgent ( r )
2023-01-29 21:47:24 +00:00
var req agentsdk . PostAppHealthsRequest
2022-11-18 22:46:53 +00:00
if ! httpapi . Read ( ctx , rw , r , & req ) {
2022-09-23 19:51:04 +00:00
return
}
if req . Healths == nil || len ( req . Healths ) == 0 {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-09-23 19:51:04 +00:00
Message : "Health field is empty" ,
} )
return
}
2022-11-18 22:46:53 +00:00
apps , err := api . Database . GetWorkspaceAppsByAgentID ( ctx , workspaceAgent . ID )
2022-09-23 19:51:04 +00:00
if err != nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-23 19:51:04 +00:00
Message : "Error getting agent apps" ,
Detail : err . Error ( ) ,
} )
return
}
var newApps [ ] database . WorkspaceApp
2022-11-07 23:35:01 +00:00
for id , newHealth := range req . Healths {
2022-09-23 19:51:04 +00:00
old := func ( ) * database . WorkspaceApp {
for _ , app := range apps {
2022-11-07 23:35:01 +00:00
if app . ID == id {
2022-09-23 19:51:04 +00:00
return & app
}
}
return nil
} ( )
if old == nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusNotFound , codersdk . Response {
2022-09-23 19:51:04 +00:00
Message : "Error setting workspace app health" ,
2022-11-07 23:35:01 +00:00
Detail : xerrors . Errorf ( "workspace app name %s not found" , id ) . Error ( ) ,
2022-09-23 19:51:04 +00:00
} )
return
}
if old . HealthcheckUrl == "" {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusNotFound , codersdk . Response {
2022-09-23 19:51:04 +00:00
Message : "Error setting workspace app health" ,
2022-11-07 23:35:01 +00:00
Detail : xerrors . Errorf ( "health checking is disabled for workspace app %s" , id ) . Error ( ) ,
2022-09-23 19:51:04 +00:00
} )
return
}
switch newHealth {
case codersdk . WorkspaceAppHealthInitializing :
case codersdk . WorkspaceAppHealthHealthy :
case codersdk . WorkspaceAppHealthUnhealthy :
default :
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-09-23 19:51:04 +00:00
Message : "Error setting workspace app health" ,
Detail : xerrors . Errorf ( "workspace app health %s is not a valid value" , newHealth ) . Error ( ) ,
} )
return
}
// don't save if the value hasn't changed
if old . Health == database . WorkspaceAppHealth ( newHealth ) {
continue
}
old . Health = database . WorkspaceAppHealth ( newHealth )
newApps = append ( newApps , * old )
}
for _ , app := range newApps {
2022-11-18 22:46:53 +00:00
err = api . Database . UpdateWorkspaceAppHealthByID ( ctx , database . UpdateWorkspaceAppHealthByIDParams {
2022-09-23 19:51:04 +00:00
ID : app . ID ,
Health : app . Health ,
} )
if err != nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-23 19:51:04 +00:00
Message : "Error setting workspace app health" ,
Detail : err . Error ( ) ,
} )
return
}
}
2022-11-18 22:46:53 +00:00
resource , err := api . Database . GetWorkspaceResourceByID ( ctx , workspaceAgent . ResourceID )
2022-11-07 15:25:18 +00:00
if err != nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-11-07 15:25:18 +00:00
Message : "Internal error fetching workspace resource." ,
Detail : err . Error ( ) ,
} )
return
}
2022-11-18 22:46:53 +00:00
job , err := api . Database . GetWorkspaceBuildByJobID ( ctx , resource . JobID )
2022-11-07 15:25:18 +00:00
if err != nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-11-07 15:25:18 +00:00
Message : "Internal error fetching workspace build." ,
Detail : err . Error ( ) ,
} )
return
}
2022-11-18 22:46:53 +00:00
workspace , err := api . Database . GetWorkspaceByID ( ctx , job . WorkspaceID )
2022-11-07 15:25:18 +00:00
if err != nil {
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-11-07 15:25:18 +00:00
Message : "Internal error fetching workspace." ,
Detail : err . Error ( ) ,
} )
return
}
2022-11-18 22:46:53 +00:00
api . publishWorkspaceUpdate ( ctx , workspace . ID )
2022-11-07 15:25:18 +00:00
2022-11-18 22:46:53 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , nil )
2022-09-23 19:51:04 +00:00
}
2023-01-05 14:27:10 +00:00
// workspaceAgentsGitAuth returns a username and password for use
2022-10-25 00:46:24 +00:00
// with GIT_ASKPASS.
2023-01-05 14:27:10 +00:00
//
// @Summary Get workspace agent Git auth
// @ID get-workspace-agent-git-auth
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param url query string true "Git URL" format(uri)
// @Param listen query bool false "Wait for a new token to be issued"
2023-01-29 21:47:24 +00:00
// @Success 200 {object} agentsdk.GitAuthResponse
2023-01-05 14:27:10 +00:00
// @Router /workspaceagents/me/gitauth [get]
2022-10-25 00:46:24 +00:00
func ( api * API ) workspaceAgentsGitAuth ( rw http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
gitURL := r . URL . Query ( ) . Get ( "url" )
if gitURL == "" {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Missing 'url' query parameter!" ,
} )
return
}
// listen determines if the request will wait for a
// new token to be issued!
listen := r . URL . Query ( ) . Has ( "listen" )
var gitAuthConfig * gitauth . Config
for _ , gitAuth := range api . GitAuthConfigs {
matches := gitAuth . Regex . MatchString ( gitURL )
if ! matches {
continue
}
gitAuthConfig = gitAuth
}
if gitAuthConfig == nil {
2023-08-30 13:58:31 +00:00
detail := "No git providers are configured."
if len ( api . GitAuthConfigs ) > 0 {
regexURLs := make ( [ ] string , 0 , len ( api . GitAuthConfigs ) )
for _ , gitAuth := range api . GitAuthConfigs {
regexURLs = append ( regexURLs , fmt . Sprintf ( "%s=%q" , gitAuth . ID , gitAuth . Regex . String ( ) ) )
}
detail = fmt . Sprintf ( "The configured git provider have regex filters that do not match the git url. Provider url regexs: %s" , strings . Join ( regexURLs , "," ) )
}
2022-10-25 00:46:24 +00:00
httpapi . Write ( ctx , rw , http . StatusNotFound , codersdk . Response {
2023-08-30 13:58:31 +00:00
Message : fmt . Sprintf ( "No matching git provider found in Coder for the url %q." , gitURL ) ,
Detail : detail ,
2022-10-25 00:46:24 +00:00
} )
return
}
workspaceAgent := httpmw . WorkspaceAgent ( r )
// We must get the workspace to get the owner ID!
resource , err := api . Database . GetWorkspaceResourceByID ( ctx , workspaceAgent . ResourceID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to get workspace resource." ,
Detail : err . Error ( ) ,
} )
return
}
build , err := api . Database . GetWorkspaceBuildByJobID ( ctx , resource . JobID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to get build." ,
Detail : err . Error ( ) ,
} )
return
}
workspace , err := api . Database . GetWorkspaceByID ( ctx , build . WorkspaceID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to get workspace." ,
Detail : err . Error ( ) ,
} )
return
}
if listen {
2023-05-01 19:19:41 +00:00
// Since we're ticking frequently and this sign-in operation is rare,
// we are OK with polling to avoid the complexity of pubsub.
2022-10-25 00:46:24 +00:00
ticker := time . NewTicker ( time . Second )
defer ticker . Stop ( )
for {
select {
2022-11-18 22:46:53 +00:00
case <- ctx . Done ( ) :
2022-10-25 00:46:24 +00:00
return
case <- ticker . C :
}
gitAuthLink , err := api . Database . GetGitAuthLink ( ctx , database . GetGitAuthLinkParams {
ProviderID : gitAuthConfig . ID ,
UserID : workspace . OwnerID ,
} )
if err != nil {
2022-12-06 15:06:41 +00:00
if errors . Is ( err , sql . ErrNoRows ) {
continue
}
2022-10-25 00:46:24 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to get git auth link." ,
Detail : err . Error ( ) ,
} )
return
}
2023-05-01 19:19:41 +00:00
// Expiry may be unset if the application doesn't configure tokens
// to expire.
// See
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app.
2023-09-01 16:50:12 +00:00
if gitAuthLink . OAuthExpiry . Before ( dbtime . Now ( ) ) && ! gitAuthLink . OAuthExpiry . IsZero ( ) {
2022-10-25 00:46:24 +00:00
continue
}
2023-06-29 18:58:01 +00:00
valid , _ , err := gitAuthConfig . ValidateToken ( ctx , gitAuthLink . OAuthAccessToken )
if err != nil {
api . Logger . Warn ( ctx , "failed to validate git auth token" ,
slog . F ( "workspace_owner_id" , workspace . OwnerID . String ( ) ) ,
slog . F ( "validate_url" , gitAuthConfig . ValidateURL ) ,
slog . Error ( err ) ,
)
}
if ! valid {
continue
2022-11-29 18:08:27 +00:00
}
2022-10-25 00:46:24 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , formatGitAuthAccessToken ( gitAuthConfig . Type , gitAuthLink . OAuthAccessToken ) )
return
}
}
// This is the URL that will redirect the user with a state token.
redirectURL , err := api . AccessURL . Parse ( fmt . Sprintf ( "/gitauth/%s" , gitAuthConfig . ID ) )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to parse access URL." ,
Detail : err . Error ( ) ,
} )
return
}
gitAuthLink , err := api . Database . GetGitAuthLink ( ctx , database . GetGitAuthLinkParams {
ProviderID : gitAuthConfig . ID ,
UserID : workspace . OwnerID ,
} )
if err != nil {
if ! errors . Is ( err , sql . ErrNoRows ) {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to get git auth link." ,
Detail : err . Error ( ) ,
} )
return
}
2023-01-29 21:47:24 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , agentsdk . GitAuthResponse {
2022-10-25 00:46:24 +00:00
URL : redirectURL . String ( ) ,
2022-11-15 21:06:13 +00:00
} )
return
}
2023-03-22 19:37:08 +00:00
gitAuthLink , updated , err := gitAuthConfig . RefreshToken ( ctx , api . Database , gitAuthLink )
2023-02-27 16:18:19 +00:00
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to refresh git auth token." ,
Detail : err . Error ( ) ,
} )
return
}
if ! updated {
2023-01-29 21:47:24 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , agentsdk . GitAuthResponse {
2022-11-15 21:06:13 +00:00
URL : redirectURL . String ( ) ,
2022-10-25 00:46:24 +00:00
} )
return
}
2023-02-27 16:18:19 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , formatGitAuthAccessToken ( gitAuthConfig . Type , gitAuthLink . OAuthAccessToken ) )
}
2022-10-25 00:46:24 +00:00
// Provider types have different username/password formats.
2023-01-29 21:47:24 +00:00
func formatGitAuthAccessToken ( typ codersdk . GitProvider , token string ) agentsdk . GitAuthResponse {
var resp agentsdk . GitAuthResponse
2022-10-25 00:46:24 +00:00
switch typ {
case codersdk . GitProviderGitLab :
// https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication
2023-01-29 21:47:24 +00:00
resp = agentsdk . GitAuthResponse {
2022-10-25 00:46:24 +00:00
Username : "oauth2" ,
Password : token ,
}
case codersdk . GitProviderBitBucket :
// https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token
2023-01-29 21:47:24 +00:00
resp = agentsdk . GitAuthResponse {
2022-10-25 00:46:24 +00:00
Username : "x-token-auth" ,
Password : token ,
}
default :
2023-01-29 21:47:24 +00:00
resp = agentsdk . GitAuthResponse {
2022-10-25 00:46:24 +00:00
Username : token ,
}
}
return resp
}
2022-06-03 09:50:10 +00:00
// wsNetConn wraps net.Conn created by websocket.NetConn(). Cancel func
// is called if a read or write error is encountered.
type wsNetConn struct {
cancel context . CancelFunc
net . Conn
}
func ( c * wsNetConn ) Read ( b [ ] byte ) ( n int , err error ) {
n , err = c . Conn . Read ( b )
if err != nil {
c . cancel ( )
}
return n , err
}
func ( c * wsNetConn ) Write ( b [ ] byte ) ( n int , err error ) {
n , err = c . Conn . Write ( b )
if err != nil {
c . cancel ( )
}
return n , err
}
func ( c * wsNetConn ) Close ( ) error {
defer c . cancel ( )
return c . Conn . Close ( )
}
// websocketNetConn wraps websocket.NetConn and returns a context that
// is tied to the parent context and the lifetime of the conn. Any error
// during read or write will cancel the context, but not close the
// conn. Close should be called to release context resources.
func websocketNetConn ( ctx context . Context , conn * websocket . Conn , msgType websocket . MessageType ) ( context . Context , net . Conn ) {
ctx , cancel := context . WithCancel ( ctx )
nc := websocket . NetConn ( ctx , conn , msgType )
return ctx , & wsNetConn {
cancel : cancel ,
Conn : nc ,
}
}
2023-03-23 19:09:13 +00:00
2023-07-28 15:57:23 +00:00
func convertWorkspaceAgentLogs ( logs [ ] database . WorkspaceAgentLog ) [ ] codersdk . WorkspaceAgentLog {
sdk := make ( [ ] codersdk . WorkspaceAgentLog , 0 , len ( logs ) )
2023-04-27 10:34:00 +00:00
for _ , logEntry := range logs {
2023-07-28 15:57:23 +00:00
sdk = append ( sdk , convertWorkspaceAgentLog ( logEntry ) )
2023-03-23 19:09:13 +00:00
}
return sdk
}
2023-07-28 15:57:23 +00:00
func convertWorkspaceAgentLog ( logEntry database . WorkspaceAgentLog ) codersdk . WorkspaceAgentLog {
return codersdk . WorkspaceAgentLog {
2023-04-27 10:34:00 +00:00
ID : logEntry . ID ,
CreatedAt : logEntry . CreatedAt ,
Output : logEntry . Output ,
Level : codersdk . LogLevel ( logEntry . Level ) ,
2023-03-23 19:09:13 +00:00
}
}
2023-05-18 03:49:25 +00:00
2023-08-09 05:10:28 +00:00
func convertWorkspaceAgentSubsystems ( ss [ ] codersdk . AgentSubsystem ) [ ] database . WorkspaceAgentSubsystem {
out := make ( [ ] database . WorkspaceAgentSubsystem , 0 , len ( ss ) )
for _ , s := range ss {
switch s {
case codersdk . AgentSubsystemEnvbox :
out = append ( out , database . WorkspaceAgentSubsystemEnvbox )
case codersdk . AgentSubsystemEnvbuilder :
out = append ( out , database . WorkspaceAgentSubsystemEnvbuilder )
case codersdk . AgentSubsystemExectrace :
out = append ( out , database . WorkspaceAgentSubsystemExectrace )
default :
// Invalid, drop it.
}
2023-05-18 03:49:25 +00:00
}
2023-08-09 05:10:28 +00:00
sort . Slice ( out , func ( i , j int ) bool {
return out [ i ] < out [ j ]
} )
return out
2023-05-18 03:49:25 +00:00
}