mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: add AgentAPI using DRPC (#10811)
Co-authored-by: Spike Curtis <spike@coder.com>
This commit is contained in:
@ -3,17 +3,23 @@ package db2sdk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/parameter"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
)
|
||||
|
||||
type ExternalAuthMeta struct {
|
||||
@ -218,3 +224,178 @@ func templateVersionParameterOptions(rawOptions json.RawMessage) ([]codersdk.Tem
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func WorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordinator,
|
||||
dbAgent database.WorkspaceAgent, apps []codersdk.WorkspaceApp, scripts []codersdk.WorkspaceAgentScript, logSources []codersdk.WorkspaceAgentLogSource,
|
||||
agentInactiveDisconnectTimeout time.Duration, agentFallbackTroubleshootingURL string,
|
||||
) (codersdk.WorkspaceAgent, error) {
|
||||
var envs map[string]string
|
||||
if dbAgent.EnvironmentVariables.Valid {
|
||||
err := json.Unmarshal(dbAgent.EnvironmentVariables.RawMessage, &envs)
|
||||
if err != nil {
|
||||
return codersdk.WorkspaceAgent{}, xerrors.Errorf("unmarshal env vars: %w", err)
|
||||
}
|
||||
}
|
||||
troubleshootingURL := agentFallbackTroubleshootingURL
|
||||
if dbAgent.TroubleshootingURL != "" {
|
||||
troubleshootingURL = dbAgent.TroubleshootingURL
|
||||
}
|
||||
subsystems := make([]codersdk.AgentSubsystem, len(dbAgent.Subsystems))
|
||||
for i, subsystem := range dbAgent.Subsystems {
|
||||
subsystems[i] = codersdk.AgentSubsystem(subsystem)
|
||||
}
|
||||
|
||||
legacyStartupScriptBehavior := codersdk.WorkspaceAgentStartupScriptBehaviorNonBlocking
|
||||
for _, script := range scripts {
|
||||
if !script.RunOnStart {
|
||||
continue
|
||||
}
|
||||
if !script.StartBlocksLogin {
|
||||
continue
|
||||
}
|
||||
legacyStartupScriptBehavior = codersdk.WorkspaceAgentStartupScriptBehaviorBlocking
|
||||
}
|
||||
|
||||
workspaceAgent := codersdk.WorkspaceAgent{
|
||||
ID: dbAgent.ID,
|
||||
CreatedAt: dbAgent.CreatedAt,
|
||||
UpdatedAt: dbAgent.UpdatedAt,
|
||||
ResourceID: dbAgent.ResourceID,
|
||||
InstanceID: dbAgent.AuthInstanceID.String,
|
||||
Name: dbAgent.Name,
|
||||
Architecture: dbAgent.Architecture,
|
||||
OperatingSystem: dbAgent.OperatingSystem,
|
||||
Scripts: scripts,
|
||||
StartupScriptBehavior: legacyStartupScriptBehavior,
|
||||
LogsLength: dbAgent.LogsLength,
|
||||
LogsOverflowed: dbAgent.LogsOverflowed,
|
||||
LogSources: logSources,
|
||||
Version: dbAgent.Version,
|
||||
APIVersion: dbAgent.APIVersion,
|
||||
EnvironmentVariables: envs,
|
||||
Directory: dbAgent.Directory,
|
||||
ExpandedDirectory: dbAgent.ExpandedDirectory,
|
||||
Apps: apps,
|
||||
ConnectionTimeoutSeconds: dbAgent.ConnectionTimeoutSeconds,
|
||||
TroubleshootingURL: troubleshootingURL,
|
||||
LifecycleState: codersdk.WorkspaceAgentLifecycle(dbAgent.LifecycleState),
|
||||
Subsystems: subsystems,
|
||||
DisplayApps: convertDisplayApps(dbAgent.DisplayApps),
|
||||
}
|
||||
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 {
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
workspaceAgent.DERPLatency[region.RegionName] = codersdk.DERPRegion{
|
||||
Preferred: node.PreferredDERP == regionID,
|
||||
LatencyMilliseconds: latency * 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
status := dbAgent.Status(agentInactiveDisconnectTimeout)
|
||||
workspaceAgent.Status = codersdk.WorkspaceAgentStatus(status.Status)
|
||||
workspaceAgent.FirstConnectedAt = status.FirstConnectedAt
|
||||
workspaceAgent.LastConnectedAt = status.LastConnectedAt
|
||||
workspaceAgent.DisconnectedAt = status.DisconnectedAt
|
||||
|
||||
if dbAgent.StartedAt.Valid {
|
||||
workspaceAgent.StartedAt = &dbAgent.StartedAt.Time
|
||||
}
|
||||
if dbAgent.ReadyAt.Valid {
|
||||
workspaceAgent.ReadyAt = &dbAgent.ReadyAt.Time
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return workspaceAgent, nil
|
||||
}
|
||||
|
||||
func AppSubdomain(dbApp database.WorkspaceApp, agentName, workspaceName, ownerName string) string {
|
||||
if !dbApp.Subdomain || agentName == "" || ownerName == "" || workspaceName == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
appSlug := dbApp.Slug
|
||||
if appSlug == "" {
|
||||
appSlug = dbApp.DisplayName
|
||||
}
|
||||
return httpapi.ApplicationURL{
|
||||
// We never generate URLs with a prefix. We only allow prefixes when
|
||||
// parsing URLs from the hostname. Users that want this feature can
|
||||
// write out their own URLs.
|
||||
Prefix: "",
|
||||
AppSlugOrPort: appSlug,
|
||||
AgentName: agentName,
|
||||
WorkspaceName: workspaceName,
|
||||
Username: ownerName,
|
||||
}.String()
|
||||
}
|
||||
|
||||
func Apps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, ownerName string, workspace database.Workspace) []codersdk.WorkspaceApp {
|
||||
apps := make([]codersdk.WorkspaceApp, 0)
|
||||
for _, dbApp := range dbApps {
|
||||
apps = append(apps, codersdk.WorkspaceApp{
|
||||
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: AppSubdomain(dbApp, agent.Name, workspace.Name, ownerName),
|
||||
SharingLevel: codersdk.WorkspaceAppSharingLevel(dbApp.SharingLevel),
|
||||
Healthcheck: codersdk.Healthcheck{
|
||||
URL: dbApp.HealthcheckUrl,
|
||||
Interval: dbApp.HealthcheckInterval,
|
||||
Threshold: dbApp.HealthcheckThreshold,
|
||||
},
|
||||
Health: codersdk.WorkspaceAppHealth(dbApp.Health),
|
||||
})
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
Reference in New Issue
Block a user