2022-04-11 21:06:15 +00:00
package coderd
import (
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-11 21:06:15 +00:00
"net/http"
2022-10-11 15:10:02 +00:00
"net/url"
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"
2022-04-11 21:06:15 +00:00
"time"
2022-04-29 22:30:10 +00:00
"github.com/google/uuid"
2023-10-10 04:02:16 +00:00
"github.com/sqlc-dev/pqtype"
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-12-18 12:53:28 +00:00
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/agentapi"
2023-11-15 15:42:27 +00:00
"github.com/coder/coder/v2/coderd/autobuild"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/database"
2023-12-18 12:53:28 +00:00
"github.com/coder/coder/v2/coderd/database/db2sdk"
2023-08-18 18:55:43 +00:00
"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-09-30 19:30:01 +00:00
"github.com/coder/coder/v2/coderd/externalauth"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
2023-12-13 17:45:43 +00:00
"github.com/coder/coder/v2/coderd/prometheusmetrics"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
2024-03-26 17:44:31 +00:00
"github.com/coder/coder/v2/codersdk/workspacesdk"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/tailnet"
2024-01-23 10:27:49 +00:00
"github.com/coder/coder/v2/tailnet/proto"
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
2023-09-25 21:47:17 +00:00
var (
dbApps [ ] database . WorkspaceApp
scripts [ ] database . WorkspaceAgentScript
logSources [ ] database . WorkspaceAgentLogSource
)
var eg errgroup . Group
eg . Go ( func ( ) ( err error ) {
dbApps , err = api . Database . GetWorkspaceAppsByAgentID ( ctx , workspaceAgent . ID )
return err
} )
eg . Go ( func ( ) ( err error ) {
2023-10-04 14:50:51 +00:00
//nolint:gocritic // TODO: can we make this not require system restricted?
scripts , err = api . Database . GetWorkspaceAgentScriptsByAgentIDs ( dbauthz . AsSystemRestricted ( ctx ) , [ ] uuid . UUID { workspaceAgent . ID } )
2023-09-25 21:47:17 +00:00
return err
} )
eg . Go ( func ( ) ( err error ) {
2023-10-04 14:50:51 +00:00
//nolint:gocritic // TODO: can we make this not require system restricted?
logSources , err = api . Database . GetWorkspaceAgentLogSourcesByAgentIDs ( dbauthz . AsSystemRestricted ( ctx ) , [ ] uuid . UUID { workspaceAgent . ID } )
2023-09-25 21:47:17 +00:00
return err
} )
err := eg . Wait ( )
2023-12-12 21:14:32 +00:00
if httpapi . Is404Error ( err ) {
httpapi . ResourceNotFound ( rw )
return
}
2023-09-25 21:47:17 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2023-09-25 21:47:17 +00:00
Message : "Internal error fetching workspace agent." ,
2022-06-07 14:33:06 +00:00
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-12-18 12:53:28 +00:00
apiAgent , err := db2sdk . WorkspaceAgent (
api . DERPMap ( ) , * api . TailnetCoordinator . Load ( ) , workspaceAgent , db2sdk . Apps ( dbApps , workspaceAgent , owner . Username , workspace ) , convertScripts ( scripts ) , convertLogSources ( logSources ) , 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-12-18 12:53:28 +00:00
// As this API becomes deprecated, use the new protobuf API and convert the
// types back to the SDK types.
manifestAPI := & agentapi . ManifestAPI {
2024-01-26 07:04:19 +00:00
AccessURL : api . AccessURL ,
AppHostname : api . AppHostname ,
ExternalAuthConfigs : api . ExternalAuthConfigs ,
DisableDirectConnections : api . DeploymentValues . DERP . Config . BlockDirect . Value ( ) ,
DerpForceWebSockets : api . DeploymentValues . DERP . Config . ForceWebSockets . Value ( ) ,
2023-12-18 12:53:28 +00:00
2024-01-26 07:04:19 +00:00
AgentFn : func ( _ context . Context ) ( database . WorkspaceAgent , error ) { return workspaceAgent , nil } ,
WorkspaceIDFn : func ( ctx context . Context , wa * database . WorkspaceAgent ) ( uuid . UUID , error ) {
// Sadly this results in a double query, but it's only temporary for
// now.
ws , err := api . Database . GetWorkspaceByAgentID ( ctx , wa . ID )
if err != nil {
return uuid . Nil , err
}
return ws . Workspace . ID , nil
} ,
Database : api . Database ,
DerpMapFn : api . DERPMap ,
2023-12-18 12:53:28 +00:00
}
manifest , err := manifestAPI . GetManifest ( ctx , & agentproto . GetManifestRequest { } )
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 {
2023-12-18 12:53:28 +00:00
Message : "Internal error fetching workspace agent manifest." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-04-25 18:30:39 +00:00
} )
return
}
2024-01-30 05:04:56 +00:00
sdkManifest , err := agentsdk . ManifestFromProto ( manifest )
2023-12-18 12:53:28 +00:00
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2024-01-30 05:04:56 +00:00
Message : "Internal error converting manifest." ,
2023-12-18 12:53:28 +00:00
Detail : err . Error ( ) ,
} )
return
2023-10-09 23:04:35 +00:00
}
2024-01-30 05:04:56 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , sdkManifest )
2022-04-25 18:30:39 +00:00
}
2023-10-31 06:08:43 +00:00
const AgentAPIVersionREST = "1.0"
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-12-18 12:53:28 +00:00
apiAgent , err := db2sdk . WorkspaceAgent (
2023-09-25 21:47:17 +00:00
api . DERPMap ( ) , * api . TailnetCoordinator . Load ( ) , workspaceAgent , nil , nil , 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 ) ,
2023-10-31 06:08:43 +00:00
APIVersion : AgentAPIVersionREST ,
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
}
2023-09-25 21:47:17 +00:00
// This is to support the legacy API where the log source ID was
// not provided in the request body. We default to the external
// log source in this case.
if req . LogSourceID == uuid . Nil {
// Use the external log source
externalSources , err := api . Database . InsertWorkspaceAgentLogSources ( ctx , database . InsertWorkspaceAgentLogSourcesParams {
WorkspaceAgentID : workspaceAgent . ID ,
CreatedAt : dbtime . Now ( ) ,
ID : [ ] uuid . UUID { agentsdk . ExternalLogSourceID } ,
DisplayName : [ ] string { "External" } ,
Icon : [ ] string { "/emojis/1f310.png" } ,
} )
if database . IsUniqueViolation ( err , database . UniqueWorkspaceAgentLogSourcesPkey ) {
err = nil
req . LogSourceID = agentsdk . ExternalLogSourceID
}
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to create external log source." ,
Detail : err . Error ( ) ,
} )
return
}
if len ( externalSources ) == 1 {
req . LogSourceID = externalSources [ 0 ] . ID
}
}
2023-03-23 19:09:13 +00:00
output := make ( [ ] string , 0 )
2023-04-10 19:29:59 +00:00
level := make ( [ ] database . LogLevel , 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
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-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 ,
2023-09-25 21:47:17 +00:00
CreatedAt : dbtime . Now ( ) ,
2023-07-18 15:57:29 +00:00
Output : output ,
Level : level ,
2023-09-25 21:47:17 +00:00
LogSourceID : req . LogSourceID ,
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-12-13 17:45:43 +00:00
row , err := api . Database . GetWorkspaceByAgentID ( ctx , workspaceAgent . ID )
2023-08-28 19:46:42 +00:00
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching workspace by agent id." ,
Detail : err . Error ( ) ,
} )
return
}
2023-12-13 17:45:43 +00:00
workspace := row . Workspace
2023-08-28 19:46:42 +00:00
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 )
2024-02-09 07:39:08 +00:00
ctx , wsNetConn := codersdk . WebsocketNetConn ( ctx , conn , websocket . MessageText )
2023-03-23 19:09:13 +00:00
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 )
2024-01-30 19:53:52 +00:00
// If the agent is unreachable, the request will hang. Assume that if we
// don't get a response after 30s that the agent is unreachable.
ctx , cancel := context . WithTimeout ( ctx , 30 * time . Second )
defer cancel ( )
2023-12-18 12:53:28 +00:00
apiAgent , err := db2sdk . WorkspaceAgent (
2023-09-25 21:47:17 +00:00
api . DERPMap ( ) , * api . TailnetCoordinator . Load ( ) , workspaceAgent , nil , nil , 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 {
2024-03-26 17:44:31 +00:00
if port . Port < workspacesdk . AgentMinimumListeningPort {
2022-10-11 15:10:02 +00:00
continue
}
if _ , ok := appPorts [ port . Port ] ; ok {
continue
}
2024-03-26 17:44:31 +00:00
if _ , ok := workspacesdk . AgentIgnoredListeningPorts [ 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-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)
2024-03-26 17:44:31 +00:00
// @Success 200 {object} workspacesdk.AgentConnectionInfo
2023-01-13 11:27:21 +00:00
// @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
2024-03-26 17:44:31 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , workspacesdk . AgentConnectionInfo {
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
2024-03-26 17:44:31 +00:00
// @Success 200 {object} workspacesdk.AgentConnectionInfo
2023-06-21 19:33:19 +00:00
// @Router /workspaceagents/connection [get]
// @x-apidocgen {"skip": true}
func ( api * API ) workspaceAgentConnectionGeneric ( rw http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
2024-03-26 17:44:31 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , workspacesdk . AgentConnectionInfo {
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
}
2024-02-09 07:39:08 +00:00
ctx , nconn := codersdk . 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 ( )
2024-03-14 16:27:32 +00:00
// The middleware only accept agents for resources on the latest build.
2022-09-01 01:09:44 +00:00
workspaceAgent := httpmw . WorkspaceAgent ( r )
2024-03-14 16:27:32 +00:00
build := httpmw . LatestBuild ( r )
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-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
2024-02-09 07:39:08 +00:00
ctx , wsNetConn := codersdk . WebsocketNetConn ( ctx , conn , websocket . MessageBinary )
2022-09-20 00:46:29 +00:00
defer wsNetConn . Close ( )
2024-01-02 12:04:37 +00:00
closeCtx , closeCtxCancel := context . WithCancel ( ctx )
defer closeCtxCancel ( )
monitor := api . startAgentWebsocketMonitor ( closeCtx , workspaceAgent , build , conn )
defer monitor . close ( )
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 , "" )
2024-01-02 12:04:37 +00:00
err = ( * api . TailnetCoordinator . Load ( ) ) . ServeAgent ( wsNetConn , workspaceAgent . ID ,
fmt . Sprintf ( "%s-%s-%s" , owner . Username , workspace . Name , workspaceAgent . Name ) ,
)
if err != nil {
api . Logger . Warn ( ctx , "tailnet coordinator agent error" , slog . Error ( err ) )
_ = conn . Close ( websocket . StatusInternalError , err . Error ( ) )
return
2022-09-20 00:46:29 +00:00
}
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
2023-12-15 10:10:24 +00:00
version := "1.0"
qv := r . URL . Query ( ) . Get ( "version" )
if qv != "" {
version = qv
}
2024-01-23 10:27:49 +00:00
if err := proto . CurrentVersion . Validate ( version ) ; err != nil {
2023-12-15 10:10:24 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Unknown or unsupported API version" ,
Validations : [ ] codersdk . ValidationError {
{ Field : "version" , Detail : err . Error ( ) } ,
} ,
} )
return
}
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
}
2024-02-09 07:39:08 +00:00
ctx , wsNetConn := codersdk . WebsocketNetConn ( ctx , conn , websocket . MessageBinary )
2023-02-14 14:42:55 +00:00
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-12-15 10:10:24 +00:00
err = api . TailnetClientService . ServeClient ( ctx , version , wsNetConn , uuid . New ( ) , workspaceAgent . ID )
if err != nil && ! xerrors . Is ( err , io . EOF ) && ! xerrors . Is ( err , context . Canceled ) {
2022-09-01 01:09:44 +00:00
_ = 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 {
2023-12-18 12:53:28 +00:00
return db2sdk . Apps ( dbApps , database . WorkspaceAgent { } , "" , database . Workspace { } )
2022-06-04 20:13:37 +00:00
}
2023-09-25 21:47:17 +00:00
func convertLogSources ( dbLogSources [ ] database . WorkspaceAgentLogSource ) [ ] codersdk . WorkspaceAgentLogSource {
logSources := make ( [ ] codersdk . WorkspaceAgentLogSource , 0 )
for _ , dbLogSource := range dbLogSources {
logSources = append ( logSources , codersdk . WorkspaceAgentLogSource {
ID : dbLogSource . ID ,
DisplayName : dbLogSource . DisplayName ,
WorkspaceAgentID : dbLogSource . WorkspaceAgentID ,
CreatedAt : dbLogSource . CreatedAt ,
Icon : dbLogSource . Icon ,
} )
}
return logSources
}
func convertScripts ( dbScripts [ ] database . WorkspaceAgentScript ) [ ] codersdk . WorkspaceAgentScript {
scripts := make ( [ ] codersdk . WorkspaceAgentScript , 0 )
for _ , dbScript := range dbScripts {
scripts = append ( scripts , codersdk . WorkspaceAgentScript {
LogPath : dbScript . LogPath ,
LogSourceID : dbScript . LogSourceID ,
Script : dbScript . Script ,
Cron : dbScript . Cron ,
RunOnStart : dbScript . RunOnStart ,
RunOnStop : dbScript . RunOnStop ,
StartBlocksLogin : dbScript . StartBlocksLogin ,
Timeout : time . Duration ( dbScript . TimeoutSeconds ) * time . Second ,
} )
}
return scripts
}
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]
2024-04-09 14:38:26 +00:00
// @Deprecated Uses agent API v2 endpoint instead.
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 )
2023-12-13 17:45:43 +00:00
row , 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-12-13 17:45:43 +00:00
workspace := row . Workspace
2022-09-01 19:58:23 +00:00
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 {
2023-11-15 15:42:27 +00:00
var nextAutostart time . Time
if workspace . AutostartSchedule . String != "" {
templateSchedule , err := ( * ( api . TemplateScheduleStore . Load ( ) ) ) . Get ( ctx , api . Database , workspace . TemplateID )
2023-12-13 17:45:43 +00:00
// If the template schedule fails to load, just default to bumping without the next transition and log it.
2023-11-15 15:42:27 +00:00
if err != nil {
2024-03-25 10:20:16 +00:00
// There's nothing we can do if the query was canceled, the
// client most likely went away so we just return an internal
// server error.
if database . IsQueryCanceledError ( err ) {
httpapi . InternalServerError ( rw , err )
return
}
2023-12-20 17:38:49 +00:00
api . Logger . Error ( ctx , "failed to load template schedule bumping activity, defaulting to bumping by 60min" ,
2023-11-15 15:42:27 +00:00
slog . F ( "workspace_id" , workspace . ID ) ,
slog . F ( "template_id" , workspace . TemplateID ) ,
slog . Error ( err ) ,
)
} else {
next , allowed := autobuild . NextAutostartSchedule ( time . Now ( ) , workspace . AutostartSchedule . String , templateSchedule )
if allowed {
nextAutostart = next
}
}
}
2023-12-18 12:53:28 +00:00
agentapi . ActivityBumpWorkspace ( ctx , api . Logger . Named ( "activity_bump" ) , api . Database , workspace . ID , nextAutostart )
2023-03-07 14:25:04 +00:00
}
2022-11-18 22:46:53 +00:00
2023-09-01 16:50:12 +00:00
now := dbtime . Now ( )
2023-12-18 12:53:28 +00:00
protoStats := & agentproto . Stats {
ConnectionsByProto : req . ConnectionsByProto ,
ConnectionCount : req . ConnectionCount ,
ConnectionMedianLatencyMs : req . ConnectionMedianLatencyMS ,
RxPackets : req . RxPackets ,
RxBytes : req . RxBytes ,
TxPackets : req . TxPackets ,
TxBytes : req . TxBytes ,
SessionCountVscode : req . SessionCountVSCode ,
SessionCountJetbrains : req . SessionCountJetBrains ,
SessionCountReconnectingPty : req . SessionCountReconnectingPTY ,
SessionCountSsh : req . SessionCountSSH ,
Metrics : make ( [ ] * agentproto . Stats_Metric , len ( req . Metrics ) ) ,
}
for i , metric := range req . Metrics {
metricType := agentproto . Stats_Metric_TYPE_UNSPECIFIED
switch metric . Type {
case agentsdk . AgentMetricTypeCounter :
metricType = agentproto . Stats_Metric_COUNTER
case agentsdk . AgentMetricTypeGauge :
metricType = agentproto . Stats_Metric_GAUGE
}
protoStats . Metrics [ i ] = & agentproto . Stats_Metric {
Name : metric . Name ,
Type : metricType ,
Value : metric . Value ,
Labels : make ( [ ] * agentproto . Stats_Metric_Label , len ( metric . Labels ) ) ,
}
for j , label := range metric . Labels {
protoStats . Metrics [ i ] . Labels [ j ] = & agentproto . Stats_Metric_Label {
Name : label . Name ,
Value : label . Value ,
}
}
}
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-12-18 12:53:28 +00:00
err := api . statsBatcher . Add ( time . Now ( ) , workspaceAgent . ID , workspace . TemplateID , workspace . OwnerID , workspace . ID , protoStats )
if 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
} )
2023-11-22 00:10:41 +00:00
if req . SessionCount ( ) > 0 {
errGroup . Go ( func ( ) error {
err := api . Database . UpdateWorkspaceLastUsedAt ( ctx , database . UpdateWorkspaceLastUsedAtParams {
ID : workspace . ID ,
LastUsedAt : now ,
} )
if err != nil {
return xerrors . Errorf ( "can't update workspace LastUsedAt: %w" , err )
}
return nil
2023-03-07 14:25:04 +00:00
} )
2023-11-22 00:10:41 +00:00
}
2023-04-27 10:34:00 +00:00
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 )
}
2023-12-13 17:45:43 +00:00
api . Options . UpdateAgentMetrics ( ctx , prometheusmetrics . AgentMetricLabels {
Username : user . Username ,
WorkspaceName : workspace . Name ,
AgentName : workspaceAgent . Name ,
TemplateName : row . TemplateName ,
2023-12-18 12:53:28 +00:00
} , protoStats . Metrics )
2023-04-27 10:34:00 +00:00
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
2023-10-13 13:37:55 +00:00
// @Param request body []agentsdk.PostMetadataRequest true "Workspace agent metadata request"
2023-03-31 20:26:19 +00:00
// @Success 204 "Success"
2023-10-13 13:37:55 +00:00
// @Router /workspaceagents/me/metadata [post]
2023-03-31 20:26:19 +00:00
// @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 )
2023-10-13 13:37:55 +00:00
// Split into function to allow call by deprecated handler.
err := api . workspaceAgentUpdateMetadata ( ctx , workspaceAgent , req )
2023-03-31 20:26:19 +00:00
if err != nil {
2023-10-13 13:37:55 +00:00
api . Logger . Error ( ctx , "failed to handle metadata request" , slog . Error ( err ) )
httpapi . InternalServerError ( rw , err )
2023-03-31 20:26:19 +00:00
return
}
2023-10-13 13:37:55 +00:00
httpapi . Write ( ctx , rw , http . StatusNoContent , nil )
}
2023-03-31 20:26:19 +00:00
2023-10-13 13:37:55 +00:00
func ( api * API ) workspaceAgentUpdateMetadata ( ctx context . Context , workspaceAgent database . WorkspaceAgent , req agentsdk . PostMetadataRequest ) error {
2023-03-31 20:26:19 +00:00
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
)
2023-10-13 13:37:55 +00:00
collectedAt := time . Now ( )
2023-03-31 20:26:19 +00:00
2023-10-13 13:37:55 +00:00
datum := database . UpdateWorkspaceAgentMetadataParams {
WorkspaceAgentID : workspaceAgent . ID ,
2023-11-16 15:03:53 +00:00
Key : make ( [ ] string , 0 , len ( req . Metadata ) ) ,
Value : make ( [ ] string , 0 , len ( req . Metadata ) ) ,
Error : make ( [ ] string , 0 , len ( req . Metadata ) ) ,
CollectedAt : make ( [ ] time . Time , 0 , len ( req . Metadata ) ) ,
2023-03-31 20:26:19 +00:00
}
2023-10-13 13:37:55 +00:00
for _ , md := range req . Metadata {
metadataError := md . Error
// We overwrite the error if the provided payload is too long.
if len ( md . Value ) > maxValueLen {
metadataError = fmt . Sprintf ( "value of %d bytes exceeded %d bytes" , len ( md . Value ) , maxValueLen )
md . Value = md . Value [ : maxValueLen ]
}
if len ( md . Error ) > maxErrorLen {
metadataError = fmt . Sprintf ( "error of %d bytes exceeded %d bytes" , len ( md . Error ) , maxErrorLen )
md . Error = md . Error [ : maxErrorLen ]
}
2023-03-31 20:26:19 +00:00
// We don't want a misconfigured agent to fill the database.
2023-10-13 13:37:55 +00:00
datum . Key = append ( datum . Key , md . Key )
datum . Value = append ( datum . Value , md . Value )
datum . Error = append ( datum . Error , metadataError )
2023-03-31 20:26:19 +00:00
// We ignore the CollectedAt from the agent to avoid bugs caused by
// clock skew.
2023-10-13 13:37:55 +00:00
datum . CollectedAt = append ( datum . CollectedAt , collectedAt )
api . Logger . Debug (
ctx , "accepted metadata report" ,
slog . F ( "workspace_agent_id" , workspaceAgent . ID ) ,
slog . F ( "collected_at" , collectedAt ) ,
slog . F ( "original_collected_at" , md . CollectedAt ) ,
slog . F ( "key" , md . Key ) ,
slog . F ( "value" , ellipse ( md . Value , 16 ) ) ,
)
2023-03-31 20:26:19 +00:00
}
2023-12-18 12:53:28 +00:00
payload , err := json . Marshal ( agentapi . WorkspaceAgentMetadataChannelPayload {
2023-10-13 13:37:55 +00:00
CollectedAt : collectedAt ,
Keys : datum . Key ,
} )
2023-03-31 20:26:19 +00:00
if err != nil {
2023-10-13 13:37:55 +00:00
return err
2023-03-31 20:26:19 +00:00
}
2023-10-13 13:37:55 +00:00
err = api . Database . UpdateWorkspaceAgentMetadata ( ctx , datum )
2023-08-24 20:18:42 +00:00
if err != nil {
2023-10-13 13:37:55 +00:00
return err
2023-08-24 20:18:42 +00:00
}
2023-12-18 12:53:28 +00:00
err = api . Pubsub . Publish ( agentapi . WatchWorkspaceAgentMetadataChannel ( workspaceAgent . ID ) , payload )
2023-03-31 20:26:19 +00:00
if err != nil {
2023-10-13 13:37:55 +00:00
return err
2023-03-31 20:26:19 +00:00
}
2023-10-13 13:37:55 +00:00
return nil
2023-03-31 20:26:19 +00:00
}
// @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 ) {
2023-11-16 15:03:53 +00:00
// Allow us to interrupt watch via cancel.
ctx , cancel := context . WithCancel ( r . Context ( ) )
defer cancel ( )
r = r . WithContext ( ctx ) // Rewire context for SSE cancellation.
workspaceAgent := httpmw . WorkspaceAgentParam ( r )
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
// Send metadata on updates, we must ensure subscription before sending
// initial metadata to guarantee that events in-between are not missed.
2023-12-18 12:53:28 +00:00
update := make ( chan agentapi . WorkspaceAgentMetadataChannelPayload , 1 )
cancelSub , err := api . Pubsub . Subscribe ( agentapi . WatchWorkspaceAgentMetadataChannel ( workspaceAgent . ID ) , func ( _ context . Context , byt [ ] byte ) {
2023-11-16 15:03:53 +00:00
if ctx . Err ( ) != nil {
return
}
2023-12-18 12:53:28 +00:00
var payload agentapi . WorkspaceAgentMetadataChannelPayload
2023-10-13 13:37:55 +00:00
err := json . Unmarshal ( byt , & payload )
2023-08-24 20:18:42 +00:00
if err != nil {
2023-11-16 15:03:53 +00:00
log . Error ( ctx , "failed to unmarshal pubsub message" , slog . Error ( err ) )
2023-08-24 20:18:42 +00:00
return
}
2023-10-13 13:37:55 +00:00
log . Debug ( ctx , "received metadata update" , "payload" , payload )
2023-08-24 20:18:42 +00:00
2023-10-13 13:37:55 +00:00
select {
case prev := <- update :
2023-11-16 15:03:53 +00:00
payload . Keys = appendUnique ( prev . Keys , payload . Keys )
2023-10-13 13:37:55 +00:00
default :
}
// This can never block since we pop and merge beforehand.
update <- payload
2023-08-24 20:18:42 +00:00
} )
if err != nil {
httpapi . InternalServerError ( rw , err )
return
}
defer cancelSub ( )
// We always use the original Request context because it contains
// the RBAC actor.
2023-11-16 15:03:53 +00:00
initialMD , err := api . Database . GetWorkspaceAgentMetadata ( ctx , database . GetWorkspaceAgentMetadataParams {
2023-10-13 13:37:55 +00:00
WorkspaceAgentID : workspaceAgent . ID ,
Keys : nil ,
} )
2023-08-24 20:18:42 +00:00
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-11-16 15:03:53 +00:00
log . Debug ( ctx , "got initial metadata" , "num" , len ( initialMD ) )
metadataMap := make ( map [ string ] database . WorkspaceAgentMetadatum , len ( initialMD ) )
for _ , datum := range initialMD {
2023-08-24 20:18:42 +00:00
metadataMap [ datum . Key ] = datum
}
2023-11-16 15:03:53 +00:00
//nolint:ineffassign // Release memory.
initialMD = nil
sseSendEvent , sseSenderClosed , err := httpapi . ServerSentEventSender ( rw , r )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error setting up server-sent events." ,
Detail : err . Error ( ) ,
} )
return
}
// Prevent handler from returning until the sender is closed.
defer func ( ) {
cancel ( )
<- sseSenderClosed
} ( )
// Synchronize cancellation from SSE -> context, this lets us simplify the
// cancellation logic.
go func ( ) {
select {
case <- ctx . Done ( ) :
case <- sseSenderClosed :
cancel ( )
}
} ( )
2023-03-31 20:26:19 +00:00
2023-08-24 20:18:42 +00:00
var lastSend time . Time
sendMetadata := func ( ) {
lastSend = time . Now ( )
2023-10-13 13:37:55 +00:00
values := maps . Values ( metadataMap )
2023-11-16 15:03:53 +00:00
log . Debug ( ctx , "sending metadata" , "num" , len ( values ) )
2023-08-24 20:18:42 +00:00
_ = 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-10-13 13:37:55 +00:00
// We send updates exactly every second.
const sendInterval = time . Second * 1
sendTicker := time . NewTicker ( sendInterval )
defer sendTicker . Stop ( )
// Send initial metadata.
2023-08-24 20:18:42 +00:00
sendMetadata ( )
2023-06-13 12:21:06 +00:00
2023-10-13 13:37:55 +00:00
// Fetch updated metadata keys as they come in.
fetchedMetadata := make ( chan [ ] database . WorkspaceAgentMetadatum )
go func ( ) {
defer close ( fetchedMetadata )
2023-11-16 15:03:53 +00:00
defer cancel ( )
2023-10-13 13:37:55 +00:00
for {
select {
2023-11-16 15:03:53 +00:00
case <- ctx . Done ( ) :
2023-10-13 13:37:55 +00:00
return
case payload := <- update :
md , err := api . Database . GetWorkspaceAgentMetadata ( ctx , database . GetWorkspaceAgentMetadataParams {
WorkspaceAgentID : workspaceAgent . ID ,
Keys : payload . Keys ,
} )
if err != nil {
2023-11-22 15:32:31 +00:00
if ! database . IsQueryCanceledError ( err ) {
2023-10-13 13:37:55 +00:00
log . Error ( ctx , "failed to get metadata" , slog . Error ( err ) )
2023-11-16 15:03:53 +00:00
_ = sseSendEvent ( ctx , codersdk . ServerSentEvent {
Type : codersdk . ServerSentEventTypeError ,
Data : codersdk . Response {
Message : "Failed to get metadata." ,
Detail : err . Error ( ) ,
} ,
} )
2023-10-13 13:37:55 +00:00
}
return
}
select {
2023-11-16 15:03:53 +00:00
case <- ctx . Done ( ) :
2023-10-13 13:37:55 +00:00
return
// We want to block here to avoid constantly pinging the
// database when the metadata isn't being processed.
case fetchedMetadata <- md :
2023-11-16 15:03:53 +00:00
log . Debug ( ctx , "fetched metadata update for keys" , "keys" , payload . Keys , "num" , len ( md ) )
2023-10-13 13:37:55 +00:00
}
}
}
} ( )
2023-11-16 15:03:53 +00:00
defer func ( ) {
<- fetchedMetadata
} ( )
2023-10-13 13:37:55 +00:00
pendingChanges := true
2023-03-31 20:26:19 +00:00
for {
select {
2023-11-16 15:03:53 +00:00
case <- ctx . Done ( ) :
2023-10-13 13:37:55 +00:00
return
case md , ok := <- fetchedMetadata :
if ! ok {
return
}
for _ , datum := range md {
metadataMap [ datum . Key ] = datum
}
pendingChanges = true
continue
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".
2023-10-13 13:37:55 +00:00
if ! pendingChanges && time . Since ( lastSend ) < 5 * time . Second {
2023-08-24 20:18:42 +00:00
continue
}
2023-10-13 13:37:55 +00:00
pendingChanges = false
2023-03-31 20:26:19 +00:00
}
2023-10-13 13:37:55 +00:00
sendMetadata ( )
2023-03-31 20:26:19 +00:00
}
}
2023-11-16 15:03:53 +00:00
// appendUnique is like append and adds elements from src to dst,
// skipping any elements that already exist in dst.
func appendUnique [ T comparable ] ( dst , src [ ] T ) [ ] T {
exists := make ( map [ T ] struct { } , len ( dst ) )
for _ , key := range dst {
exists [ key ] = struct { } { }
}
for _ , key := range src {
if _ , ok := exists [ key ] ; ! ok {
dst = append ( dst , key )
}
}
return dst
}
2023-03-31 20:26:19 +00:00
func convertWorkspaceAgentMetadata ( db [ ] database . WorkspaceAgentMetadatum ) [ ] codersdk . WorkspaceAgentMetadata {
2024-02-08 16:29:34 +00:00
// Sort the input database slice by DisplayOrder and then by Key before processing
sort . Slice ( db , func ( i , j int ) bool {
if db [ i ] . DisplayOrder == db [ j ] . DisplayOrder {
return db [ i ] . Key < db [ j ] . Key
}
return db [ i ] . DisplayOrder < db [ j ] . DisplayOrder
} )
2023-03-31 20:26:19 +00:00
// An empty array is easier for clients to handle than a null.
2024-02-08 16:29:34 +00:00
result := make ( [ ] codersdk . WorkspaceAgentMetadata , len ( db ) )
for i , datum := range db {
result [ i ] = codersdk . WorkspaceAgentMetadata {
2023-03-31 20:26:19 +00:00
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 ,
} ,
2024-02-08 16:29:34 +00:00
}
2023-03-31 20:26:19 +00:00
}
return result
}
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 )
2023-12-13 17:45:43 +00:00
row , err := api . Database . GetWorkspaceByAgentID ( ctx , workspaceAgent . ID )
2023-01-24 12:24:27 +00:00
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Failed to get workspace." ,
Detail : err . Error ( ) ,
} )
return
}
2023-12-13 17:45:43 +00:00
workspace := row . Workspace
2023-01-24 12:24:27 +00:00
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-10-09 23:04:35 +00:00
// workspaceAgentsExternalAuth returns an access token for a given URL
// or finds a provider by ID.
2023-01-05 14:27:10 +00:00
//
2023-10-09 23:04:35 +00:00
// @Summary Get workspace agent external auth
// @ID get-workspace-agent-external-auth
2023-01-05 14:27:10 +00:00
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
2023-10-09 23:04:35 +00:00
// @Param match query string true "Match"
// @Param id query string true "Provider ID"
2023-01-05 14:27:10 +00:00
// @Param listen query bool false "Wait for a new token to be issued"
2023-10-09 23:04:35 +00:00
// @Success 200 {object} agentsdk.ExternalAuthResponse
// @Router /workspaceagents/me/external-auth [get]
func ( api * API ) workspaceAgentsExternalAuth ( rw http . ResponseWriter , r * http . Request ) {
2022-10-25 00:46:24 +00:00
ctx := r . Context ( )
2023-10-10 03:25:50 +00:00
query := r . URL . Query ( )
2023-10-09 23:04:35 +00:00
// Either match or configID must be provided!
2023-10-10 03:25:50 +00:00
match := query . Get ( "match" )
2023-10-09 23:04:35 +00:00
if match == "" {
// Support legacy agents!
2023-10-10 03:25:50 +00:00
match = query . Get ( "url" )
2023-10-09 23:04:35 +00:00
}
2023-10-10 03:25:50 +00:00
id := query . Get ( "id" )
2023-10-09 23:04:35 +00:00
if match == "" && id == "" {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "'url' or 'id' must be provided!" ,
} )
return
}
if match != "" && id != "" {
2022-10-25 00:46:24 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2023-10-09 23:04:35 +00:00
Message : "'url' and 'id' cannot be provided together!" ,
2022-10-25 00:46:24 +00:00
} )
return
}
2023-10-09 23:04:35 +00:00
2022-10-25 00:46:24 +00:00
// listen determines if the request will wait for a
// new token to be issued!
listen := r . URL . Query ( ) . Has ( "listen" )
2023-09-30 19:30:01 +00:00
var externalAuthConfig * externalauth . Config
2023-10-09 23:04:35 +00:00
for _ , extAuth := range api . ExternalAuthConfigs {
if extAuth . ID == id {
externalAuthConfig = extAuth
break
}
if match == "" || extAuth . Regex == nil {
2023-10-03 16:45:07 +00:00
continue
}
2023-10-09 23:04:35 +00:00
matches := extAuth . Regex . MatchString ( match )
2022-10-25 00:46:24 +00:00
if ! matches {
continue
}
2023-10-09 23:04:35 +00:00
externalAuthConfig = extAuth
2022-10-25 00:46:24 +00:00
}
2023-09-30 19:30:01 +00:00
if externalAuthConfig == nil {
2023-10-09 23:04:35 +00:00
detail := "External auth provider not found."
2023-09-29 19:13:20 +00:00
if len ( api . ExternalAuthConfigs ) > 0 {
regexURLs := make ( [ ] string , 0 , len ( api . ExternalAuthConfigs ) )
2023-09-30 19:30:01 +00:00
for _ , extAuth := range api . ExternalAuthConfigs {
2023-10-03 16:45:07 +00:00
if extAuth . Regex == nil {
continue
}
2023-09-30 19:30:01 +00:00
regexURLs = append ( regexURLs , fmt . Sprintf ( "%s=%q" , extAuth . ID , extAuth . Regex . String ( ) ) )
2023-08-30 13:58:31 +00:00
}
2023-10-09 23:04:35 +00:00
detail = fmt . Sprintf ( "The configured external auth provider have regex filters that do not match the url. Provider url regex: %s" , strings . Join ( regexURLs , "," ) )
2023-08-30 13:58:31 +00:00
}
2022-10-25 00:46:24 +00:00
httpapi . Write ( ctx , rw , http . StatusNotFound , codersdk . Response {
2023-10-09 23:04:35 +00:00
Message : fmt . Sprintf ( "No matching external auth provider found in Coder for the url %q." , match ) ,
2023-08-30 13:58:31 +00:00
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
}
2024-01-29 14:55:15 +00:00
var previousToken * database . ExternalAuthLink
// handleRetrying will attempt to continually check for a new token
// if listen is true. This is useful if an error is encountered in the
// original single flow.
//
// By default, if no errors are encountered, then the single flow response
// is returned.
handleRetrying := func ( code int , response any ) {
if ! listen {
httpapi . Write ( ctx , rw , code , response )
2022-10-25 00:46:24 +00:00
return
}
2024-01-29 14:55:15 +00:00
api . workspaceAgentsExternalAuthListen ( ctx , rw , previousToken , externalAuthConfig , workspace )
2022-10-25 00:46:24 +00:00
}
// This is the URL that will redirect the user with a state token.
2023-10-03 14:04:39 +00:00
redirectURL , err := api . AccessURL . Parse ( fmt . Sprintf ( "/external-auth/%s" , externalAuthConfig . ID ) )
2022-10-25 00:46:24 +00:00
if err != nil {
2024-01-29 14:55:15 +00:00
handleRetrying ( http . StatusInternalServerError , codersdk . Response {
2022-10-25 00:46:24 +00:00
Message : "Failed to parse access URL." ,
Detail : err . Error ( ) ,
} )
return
}
2023-09-30 19:30:01 +00:00
externalAuthLink , err := api . Database . GetExternalAuthLink ( ctx , database . GetExternalAuthLinkParams {
ProviderID : externalAuthConfig . ID ,
2022-10-25 00:46:24 +00:00
UserID : workspace . OwnerID ,
} )
if err != nil {
if ! errors . Is ( err , sql . ErrNoRows ) {
2024-01-29 14:55:15 +00:00
handleRetrying ( http . StatusInternalServerError , codersdk . Response {
2023-09-30 19:30:01 +00:00
Message : "Failed to get external auth link." ,
2022-10-25 00:46:24 +00:00
Detail : err . Error ( ) ,
} )
return
}
2024-01-29 14:55:15 +00:00
handleRetrying ( http . StatusOK , agentsdk . ExternalAuthResponse {
2022-10-25 00:46:24 +00:00
URL : redirectURL . String ( ) ,
2022-11-15 21:06:13 +00:00
} )
return
}
2024-01-29 14:55:15 +00:00
externalAuthLink , valid , err := externalAuthConfig . RefreshToken ( ctx , api . Database , externalAuthLink )
2023-02-27 16:18:19 +00:00
if err != nil {
2024-01-29 14:55:15 +00:00
handleRetrying ( http . StatusInternalServerError , codersdk . Response {
2023-09-30 19:30:01 +00:00
Message : "Failed to refresh external auth token." ,
2023-02-27 16:18:19 +00:00
Detail : err . Error ( ) ,
} )
return
}
2024-01-29 14:55:15 +00:00
if ! valid {
// Set the previous token so the retry logic will skip validating the
// same token again. This should only be set if the token is invalid and there
// was no error. If it is invalid because of an error, then we should recheck.
previousToken = & externalAuthLink
handleRetrying ( http . StatusOK , agentsdk . ExternalAuthResponse {
2022-11-15 21:06:13 +00:00
URL : redirectURL . String ( ) ,
2022-10-25 00:46:24 +00:00
} )
return
}
2023-10-10 04:02:16 +00:00
resp , err := createExternalAuthResponse ( externalAuthConfig . Type , externalAuthLink . OAuthAccessToken , externalAuthLink . OAuthExtra )
if err != nil {
2024-01-29 14:55:15 +00:00
handleRetrying ( http . StatusInternalServerError , codersdk . Response {
2023-10-10 04:02:16 +00:00
Message : "Failed to create external auth response." ,
Detail : err . Error ( ) ,
} )
return
}
httpapi . Write ( ctx , rw , http . StatusOK , resp )
2023-02-27 16:18:19 +00:00
}
2024-01-29 14:55:15 +00:00
func ( api * API ) workspaceAgentsExternalAuthListen ( ctx context . Context , rw http . ResponseWriter , previous * database . ExternalAuthLink , externalAuthConfig * externalauth . Config , workspace database . Workspace ) {
// Since we're ticking frequently and this sign-in operation is rare,
// we are OK with polling to avoid the complexity of pubsub.
ticker , done := api . NewTicker ( time . Second )
defer done ( )
// If we have a previous token that is invalid, we should not check this again.
// This serves to prevent doing excessive unauthorized requests to the external
// auth provider. For github, this limit is 60 per hour, so saving a call
// per invalid token can be significant.
var previousToken database . ExternalAuthLink
if previous != nil {
previousToken = * previous
}
for {
select {
case <- ctx . Done ( ) :
return
case <- ticker :
}
externalAuthLink , err := api . Database . GetExternalAuthLink ( ctx , database . GetExternalAuthLinkParams {
ProviderID : externalAuthConfig . ID ,
UserID : workspace . OwnerID ,
} )
if err != nil {
if errors . Is ( err , sql . ErrNoRows ) {
continue
}
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to get external auth link." ,
Detail : err . Error ( ) ,
} )
return
}
// 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.
if externalAuthLink . OAuthExpiry . Before ( dbtime . Now ( ) ) && ! externalAuthLink . OAuthExpiry . IsZero ( ) {
continue
}
// Only attempt to revalidate an oauth token if it has actually changed.
// No point in trying to validate the same token over and over again.
if previousToken . OAuthAccessToken == externalAuthLink . OAuthAccessToken &&
previousToken . OAuthRefreshToken == externalAuthLink . OAuthRefreshToken &&
previousToken . OAuthExpiry == externalAuthLink . OAuthExpiry {
continue
}
valid , _ , err := externalAuthConfig . ValidateToken ( ctx , externalAuthLink . OAuthToken ( ) )
if err != nil {
api . Logger . Warn ( ctx , "failed to validate external auth token" ,
slog . F ( "workspace_owner_id" , workspace . OwnerID . String ( ) ) ,
slog . F ( "validate_url" , externalAuthConfig . ValidateURL ) ,
slog . Error ( err ) ,
)
}
previousToken = externalAuthLink
if ! valid {
continue
}
resp , err := createExternalAuthResponse ( externalAuthConfig . Type , externalAuthLink . OAuthAccessToken , externalAuthLink . OAuthExtra )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to create external auth response." ,
Detail : err . Error ( ) ,
} )
return
}
httpapi . Write ( ctx , rw , http . StatusOK , resp )
return
}
}
2023-10-09 23:04:35 +00:00
// createExternalAuthResponse creates an ExternalAuthResponse based on the
// provider type. This is to support legacy `/workspaceagents/me/gitauth`
// which uses `Username` and `Password`.
2023-10-10 04:02:16 +00:00
func createExternalAuthResponse ( typ , token string , extra pqtype . NullRawMessage ) ( agentsdk . ExternalAuthResponse , error ) {
2023-10-09 23:04:35 +00:00
var resp agentsdk . ExternalAuthResponse
2022-10-25 00:46:24 +00:00
switch typ {
2023-10-09 23:04:35 +00:00
case string ( codersdk . EnhancedExternalAuthProviderGitLab ) :
2022-10-25 00:46:24 +00:00
// https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication
2023-10-09 23:04:35 +00:00
resp = agentsdk . ExternalAuthResponse {
2022-10-25 00:46:24 +00:00
Username : "oauth2" ,
Password : token ,
}
2023-11-08 17:05:51 +00:00
case string ( codersdk . EnhancedExternalAuthProviderBitBucketCloud ) , string ( codersdk . EnhancedExternalAuthProviderBitBucketServer ) :
// The string "bitbucket" was a legacy parameter that needs to still be supported.
2022-10-25 00:46:24 +00:00
// https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token
2023-10-09 23:04:35 +00:00
resp = agentsdk . ExternalAuthResponse {
2022-10-25 00:46:24 +00:00
Username : "x-token-auth" ,
Password : token ,
}
default :
2023-10-09 23:04:35 +00:00
resp = agentsdk . ExternalAuthResponse {
2022-10-25 00:46:24 +00:00
Username : token ,
}
}
2023-10-09 23:04:35 +00:00
resp . AccessToken = token
resp . Type = typ
2023-10-10 04:02:16 +00:00
var err error
if extra . Valid {
err = json . Unmarshal ( extra . RawMessage , & resp . TokenExtra )
}
return resp , err
2022-10-25 00:46:24 +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-09-25 21:47:17 +00:00
SourceID : logEntry . LogSourceID ,
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
}