chore: add derpserver to wsproxy, add proxies to derpmap (#7311)

This commit is contained in:
Dean Sheather
2023-07-27 02:21:04 +10:00
committed by GitHub
parent 70692c2e4e
commit 2f0a9996e7
58 changed files with 3001 additions and 386 deletions
+85 -9
View File
@@ -530,6 +530,25 @@ const docTemplate = `{
}
}
},
"/derp-map": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": [
"Agents"
],
"summary": "Get DERP map updates",
"operationId": "get-derp-map-updates",
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/entitlements": {
"get": {
"security": [
@@ -5381,28 +5400,36 @@ const docTemplate = `{
}
}
},
"/workspaceproxies/me/goingaway": {
"/workspaceproxies/me/deregister": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"consumes": [
"application/json"
],
"tags": [
"Enterprise"
],
"summary": "Workspace proxy going away",
"operationId": "workspace-proxy-going-away",
"responses": {
"201": {
"description": "Created",
"summary": "Deregister workspace proxy",
"operationId": "deregister-workspace-proxy",
"parameters": [
{
"description": "Deregister workspace proxy request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.Response"
"$ref": "#/definitions/wsproxysdk.DeregisterWorkspaceProxyRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
},
"x-apidocgen": {
"skip": true
@@ -5471,7 +5498,7 @@ const docTemplate = `{
"operationId": "register-workspace-proxy",
"parameters": [
{
"description": "Issue signed app token request",
"description": "Register workspace proxy request",
"name": "request",
"in": "body",
"required": true,
@@ -10770,6 +10797,9 @@ const docTemplate = `{
"deleted": {
"type": "boolean"
},
"derp_enabled": {
"type": "boolean"
},
"display_name": {
"type": "string"
},
@@ -11469,6 +11499,15 @@ const docTemplate = `{
}
}
},
"wsproxysdk.DeregisterWorkspaceProxyRequest": {
"type": "object",
"properties": {
"replica_id": {
"description": "ReplicaID is a unique identifier for the replica of the proxy that is\nderegistering. It should be generated by the client on startup and\nshould've already been passed to the register endpoint.",
"type": "string"
}
}
},
"wsproxysdk.IssueSignedAppTokenResponse": {
"type": "object",
"properties": {
@@ -11485,6 +11524,30 @@ const docTemplate = `{
"description": "AccessURL that hits the workspace proxy api.",
"type": "string"
},
"derp_enabled": {
"description": "DerpEnabled indicates whether the proxy should be included in the DERP\nmap or not.",
"type": "boolean"
},
"hostname": {
"description": "ReplicaHostname is the OS hostname of the machine that the proxy is running\non. This is only used for tracking purposes in the replicas table.",
"type": "string"
},
"replica_error": {
"description": "ReplicaError is the error that the replica encountered when trying to\ndial it's peers. This is stored in the replicas table for debugging\npurposes but does not affect the proxy's ability to register.\n\nThis value is only stored on subsequent requests to the register\nendpoint, not the first request.",
"type": "string"
},
"replica_id": {
"description": "ReplicaID is a unique identifier for the replica of the proxy that is\nregistering. It should be generated by the client on startup and\npersisted (in memory only) until the process is restarted.",
"type": "string"
},
"replica_relay_address": {
"description": "ReplicaRelayAddress is the DERP address of the replica that other\nreplicas may use to connect internally for DERP meshing.",
"type": "string"
},
"version": {
"description": "Version is the Coder version of the proxy.",
"type": "string"
},
"wildcard_hostname": {
"description": "WildcardHostname that the workspace proxy api is serving for subdomain apps.",
"type": "string"
@@ -11496,6 +11559,19 @@ const docTemplate = `{
"properties": {
"app_security_key": {
"type": "string"
},
"derp_mesh_key": {
"type": "string"
},
"derp_region_id": {
"type": "integer"
},
"sibling_replicas": {
"description": "SiblingReplicas is a list of all other replicas of the proxy that have\nnot timed out.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Replica"
}
}
}
}
+83 -9
View File
@@ -450,6 +450,23 @@
}
}
},
"/derp-map": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": ["Agents"],
"summary": "Get DERP map updates",
"operationId": "get-derp-map-updates",
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/entitlements": {
"get": {
"security": [
@@ -4743,24 +4760,32 @@
}
}
},
"/workspaceproxies/me/goingaway": {
"/workspaceproxies/me/deregister": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"consumes": ["application/json"],
"tags": ["Enterprise"],
"summary": "Workspace proxy going away",
"operationId": "workspace-proxy-going-away",
"responses": {
"201": {
"description": "Created",
"summary": "Deregister workspace proxy",
"operationId": "deregister-workspace-proxy",
"parameters": [
{
"description": "Deregister workspace proxy request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.Response"
"$ref": "#/definitions/wsproxysdk.DeregisterWorkspaceProxyRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
},
"x-apidocgen": {
"skip": true
@@ -4817,7 +4842,7 @@
"operationId": "register-workspace-proxy",
"parameters": [
{
"description": "Issue signed app token request",
"description": "Register workspace proxy request",
"name": "request",
"in": "body",
"required": true,
@@ -9772,6 +9797,9 @@
"deleted": {
"type": "boolean"
},
"derp_enabled": {
"type": "boolean"
},
"display_name": {
"type": "string"
},
@@ -10459,6 +10487,15 @@
}
}
},
"wsproxysdk.DeregisterWorkspaceProxyRequest": {
"type": "object",
"properties": {
"replica_id": {
"description": "ReplicaID is a unique identifier for the replica of the proxy that is\nderegistering. It should be generated by the client on startup and\nshould've already been passed to the register endpoint.",
"type": "string"
}
}
},
"wsproxysdk.IssueSignedAppTokenResponse": {
"type": "object",
"properties": {
@@ -10475,6 +10512,30 @@
"description": "AccessURL that hits the workspace proxy api.",
"type": "string"
},
"derp_enabled": {
"description": "DerpEnabled indicates whether the proxy should be included in the DERP\nmap or not.",
"type": "boolean"
},
"hostname": {
"description": "ReplicaHostname is the OS hostname of the machine that the proxy is running\non. This is only used for tracking purposes in the replicas table.",
"type": "string"
},
"replica_error": {
"description": "ReplicaError is the error that the replica encountered when trying to\ndial it's peers. This is stored in the replicas table for debugging\npurposes but does not affect the proxy's ability to register.\n\nThis value is only stored on subsequent requests to the register\nendpoint, not the first request.",
"type": "string"
},
"replica_id": {
"description": "ReplicaID is a unique identifier for the replica of the proxy that is\nregistering. It should be generated by the client on startup and\npersisted (in memory only) until the process is restarted.",
"type": "string"
},
"replica_relay_address": {
"description": "ReplicaRelayAddress is the DERP address of the replica that other\nreplicas may use to connect internally for DERP meshing.",
"type": "string"
},
"version": {
"description": "Version is the Coder version of the proxy.",
"type": "string"
},
"wildcard_hostname": {
"description": "WildcardHostname that the workspace proxy api is serving for subdomain apps.",
"type": "string"
@@ -10486,6 +10547,19 @@
"properties": {
"app_security_key": {
"type": "string"
},
"derp_mesh_key": {
"type": "string"
},
"derp_region_id": {
"type": "integer"
},
"sibling_replicas": {
"description": "SiblingReplicas is a list of all other replicas of the proxy that have\nnot timed out.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Replica"
}
}
}
}
+45 -24
View File
@@ -118,10 +118,13 @@ type Options struct {
RealIPConfig *httpmw.RealIPConfig
TrialGenerator func(ctx context.Context, email string) error
// TLSCertificates is used to mesh DERP servers securely.
TLSCertificates []tls.Certificate
TailnetCoordinator tailnet.Coordinator
DERPServer *derp.Server
DERPMap *tailcfg.DERPMap
TLSCertificates []tls.Certificate
TailnetCoordinator tailnet.Coordinator
DERPServer *derp.Server
// BaseDERPMap is used as the base DERP map for all clients and agents.
// Proxies are added to this list.
BaseDERPMap *tailcfg.DERPMap
DERPMapUpdateFrequency time.Duration
SwaggerEndpoint bool
SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
SetUserSiteRoles func(ctx context.Context, tx database.Store, userID uuid.UUID, roles []string) error
@@ -236,12 +239,15 @@ func New(options *Options) *API {
if options.PrometheusRegistry == nil {
options.PrometheusRegistry = prometheus.NewRegistry()
}
if options.TailnetCoordinator == nil {
options.TailnetCoordinator = tailnet.NewCoordinator(options.Logger)
}
if options.DERPServer == nil {
options.DERPServer = derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger.Named("derp")))
}
if options.DERPMapUpdateFrequency == 0 {
options.DERPMapUpdateFrequency = 5 * time.Second
}
if options.TailnetCoordinator == nil {
options.TailnetCoordinator = tailnet.NewCoordinator(options.Logger)
}
if options.Auditor == nil {
options.Auditor = audit.NewNop()
}
@@ -281,22 +287,6 @@ func New(options *Options) *API {
v := schedule.NewAGPLUserQuietHoursScheduleStore()
options.UserQuietHoursScheduleStore.Store(&v)
}
if options.HealthcheckFunc == nil {
options.HealthcheckFunc = func(ctx context.Context, apiKey string) *healthcheck.Report {
return healthcheck.Run(ctx, &healthcheck.ReportOptions{
DB: options.Database,
AccessURL: options.AccessURL,
DERPMap: options.DERPMap.Clone(),
APIKey: apiKey,
})
}
}
if options.HealthcheckTimeout == 0 {
options.HealthcheckTimeout = 30 * time.Second
}
if options.HealthcheckRefresh == 0 {
options.HealthcheckRefresh = 10 * time.Minute
}
siteCacheDir := options.CacheDir
if siteCacheDir != "" {
@@ -376,6 +366,22 @@ func New(options *Options) *API {
*options.UpdateCheckOptions,
)
}
if options.HealthcheckFunc == nil {
options.HealthcheckFunc = func(ctx context.Context, apiKey string) *healthcheck.Report {
return healthcheck.Run(ctx, &healthcheck.ReportOptions{
DB: options.Database,
AccessURL: options.AccessURL,
DERPMap: api.DERPMap(),
APIKey: apiKey,
})
}
}
if options.HealthcheckTimeout == 0 {
options.HealthcheckTimeout = 30 * time.Second
}
if options.HealthcheckRefresh == 0 {
options.HealthcheckRefresh = 10 * time.Minute
}
var oidcAuthURLParams map[string]string
if options.OIDCConfig != nil {
@@ -388,7 +394,7 @@ func New(options *Options) *API {
api.agentProvider, err = NewServerTailnet(api.ctx,
options.Logger,
options.DERPServer,
options.DERPMap,
options.BaseDERPMap,
func(context.Context) (tailnet.MultiAgentConn, error) {
return (*api.TailnetCoordinator.Load()).ServeMultiAgent(uuid.New()), nil
},
@@ -544,6 +550,10 @@ func New(options *Options) *API {
r.Use(apiKeyMiddleware)
r.Get("/regions", api.regions)
})
r.Route("/derp-map", func(r chi.Router) {
// r.Use(apiKeyMiddleware)
r.Get("/", api.derpMapUpdates)
})
r.Route("/deployment", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Get("/config", api.deploymentValues)
@@ -953,6 +963,8 @@ type API struct {
// UserQuietHoursScheduleStore is a pointer to an atomic pointer for the
// same reason as TemplateScheduleStore.
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
// DERPMapper mutates the DERPMap to include workspace proxies.
DERPMapper atomic.Pointer[func(derpMap *tailcfg.DERPMap) *tailcfg.DERPMap]
HTTPAuth *HTTPAuthorizer
@@ -1107,6 +1119,15 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, debounce ti
return proto.NewDRPCProvisionerDaemonClient(clientSession), nil
}
func (api *API) DERPMap() *tailcfg.DERPMap {
fn := api.DERPMapper.Load()
if fn != nil {
return (*fn)(api.Options.BaseDERPMap)
}
return api.Options.BaseDERPMap
}
// nolint:revive
func ReadExperiments(log slog.Logger, raw []string) codersdk.Experiments {
exps := make([]codersdk.Experiment, 0, len(raw))
+2 -1
View File
@@ -385,7 +385,8 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
TLSCertificates: options.TLSCertificates,
TrialGenerator: options.TrialGenerator,
TailnetCoordinator: options.Coordinator,
DERPMap: derpMap,
BaseDERPMap: derpMap,
DERPMapUpdateFrequency: 150 * time.Millisecond,
MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval,
AgentStatsRefreshInterval: options.AgentStatsRefreshInterval,
DeploymentValues: options.DeploymentValues,
+7
View File
@@ -1123,6 +1123,13 @@ func (q *querier) GetQuotaConsumedForUser(ctx context.Context, userID uuid.UUID)
return q.db.GetQuotaConsumedForUser(ctx, userID)
}
func (q *querier) GetReplicaByID(ctx context.Context, id uuid.UUID) (database.Replica, error) {
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
return database.Replica{}, err
}
return q.db.GetReplicaByID(ctx, id)
}
func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.Replica, error) {
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
+22
View File
@@ -1820,6 +1820,19 @@ func (q *FakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUI
return sum, nil
}
func (q *FakeQuerier) GetReplicaByID(_ context.Context, id uuid.UUID) (database.Replica, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
for _, replica := range q.replicas {
if replica.ID == id {
return replica, nil
}
}
return database.Replica{}, sql.ErrNoRows
}
func (q *FakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time.Time) ([]database.Replica, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -3684,6 +3697,7 @@ func (q *FakeQuerier) InsertReplica(_ context.Context, arg database.InsertReplic
RelayAddress: arg.RelayAddress,
Version: arg.Version,
DatabaseLatency: arg.DatabaseLatency,
Primary: arg.Primary,
}
q.replicas = append(q.replicas, replica)
return replica, nil
@@ -4125,10 +4139,14 @@ func (q *FakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.Inser
q.mutex.Lock()
defer q.mutex.Unlock()
lastRegionID := int32(0)
for _, p := range q.workspaceProxies {
if !p.Deleted && p.Name == arg.Name {
return database.WorkspaceProxy{}, errDuplicateKey
}
if p.RegionID > lastRegionID {
lastRegionID = p.RegionID
}
}
p := database.WorkspaceProxy{
@@ -4136,7 +4154,9 @@ func (q *FakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.Inser
Name: arg.Name,
DisplayName: arg.DisplayName,
Icon: arg.Icon,
DerpEnabled: arg.DerpEnabled,
TokenHashedSecret: arg.TokenHashedSecret,
RegionID: lastRegionID + 1,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
Deleted: false,
@@ -4208,6 +4228,7 @@ func (q *FakeQuerier) RegisterWorkspaceProxy(_ context.Context, arg database.Reg
if p.ID == arg.ID {
p.Url = arg.Url
p.WildcardHostname = arg.WildcardHostname
p.DerpEnabled = arg.DerpEnabled
p.UpdatedAt = database.Now()
q.workspaceProxies[i] = p
return p, nil
@@ -4419,6 +4440,7 @@ func (q *FakeQuerier) UpdateReplica(_ context.Context, arg database.UpdateReplic
replica.Version = arg.Version
replica.Error = arg.Error
replica.DatabaseLatency = arg.DatabaseLatency
replica.Primary = arg.Primary
q.replicas[index] = replica
return replica, nil
}
+7
View File
@@ -545,6 +545,13 @@ func (m metricsStore) GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.
return consumed, err
}
func (m metricsStore) GetReplicaByID(ctx context.Context, id uuid.UUID) (database.Replica, error) {
start := time.Now()
replica, err := m.s.GetReplicaByID(ctx, id)
m.queryLatencies.WithLabelValues("GetReplicaByID").Observe(time.Since(start).Seconds())
return replica, err
}
func (m metricsStore) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.Replica, error) {
start := time.Now()
replicas, err := m.s.GetReplicasUpdatedAfter(ctx, updatedAt)
+15
View File
@@ -1076,6 +1076,21 @@ func (mr *MockStoreMockRecorder) GetQuotaConsumedForUser(arg0, arg1 interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQuotaConsumedForUser", reflect.TypeOf((*MockStore)(nil).GetQuotaConsumedForUser), arg0, arg1)
}
// GetReplicaByID mocks base method.
func (m *MockStore) GetReplicaByID(arg0 context.Context, arg1 uuid.UUID) (database.Replica, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetReplicaByID", arg0, arg1)
ret0, _ := ret[0].(database.Replica)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetReplicaByID indicates an expected call of GetReplicaByID.
func (mr *MockStoreMockRecorder) GetReplicaByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicaByID", reflect.TypeOf((*MockStore)(nil).GetReplicaByID), arg0, arg1)
}
// GetReplicasUpdatedAfter mocks base method.
func (m *MockStore) GetReplicasUpdatedAfter(arg0 context.Context, arg1 time.Time) ([]database.Replica, error) {
m.ctrl.T.Helper()
+20 -2
View File
@@ -418,7 +418,8 @@ CREATE TABLE replicas (
relay_address text NOT NULL,
database_latency integer NOT NULL,
version text NOT NULL,
error text DEFAULT ''::text NOT NULL
error text DEFAULT ''::text NOT NULL,
"primary" boolean DEFAULT true NOT NULL
);
CREATE TABLE site_configs (
@@ -862,7 +863,9 @@ CREATE TABLE workspace_proxies (
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
deleted boolean NOT NULL,
token_hashed_secret bytea NOT NULL
token_hashed_secret bytea NOT NULL,
region_id integer NOT NULL,
derp_enabled boolean DEFAULT true NOT NULL
);
COMMENT ON COLUMN workspace_proxies.icon IS 'Expects an emoji character. (/emojis/1f1fa-1f1f8.png)';
@@ -875,6 +878,16 @@ COMMENT ON COLUMN workspace_proxies.deleted IS 'Boolean indicator of a deleted w
COMMENT ON COLUMN workspace_proxies.token_hashed_secret IS 'Hashed secret is used to authenticate the workspace proxy using a session token.';
CREATE SEQUENCE workspace_proxies_region_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE workspace_proxies_region_id_seq OWNED BY workspace_proxies.region_id;
CREATE TABLE workspace_resource_metadata (
workspace_resource_id uuid NOT NULL,
key character varying(1024) NOT NULL,
@@ -927,6 +940,8 @@ ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provi
ALTER TABLE ONLY workspace_agent_startup_logs ALTER COLUMN id SET DEFAULT nextval('workspace_agent_startup_logs_id_seq'::regclass);
ALTER TABLE ONLY workspace_proxies ALTER COLUMN region_id SET DEFAULT nextval('workspace_proxies_region_id_seq'::regclass);
ALTER TABLE ONLY workspace_resource_metadata ALTER COLUMN id SET DEFAULT nextval('workspace_resource_metadata_id_seq'::regclass);
ALTER TABLE ONLY workspace_agent_stats
@@ -1058,6 +1073,9 @@ ALTER TABLE ONLY workspace_builds
ALTER TABLE ONLY workspace_proxies
ADD CONSTRAINT workspace_proxies_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_proxies
ADD CONSTRAINT workspace_proxies_region_id_unique UNIQUE (region_id);
ALTER TABLE ONLY workspace_resource_metadata
ADD CONSTRAINT workspace_resource_metadata_name UNIQUE (workspace_resource_id, key);
@@ -0,0 +1,15 @@
BEGIN;
-- drop any rows that aren't primary replicas
DELETE FROM replicas
WHERE "primary" = false;
ALTER TABLE replicas
DROP COLUMN "primary";
ALTER TABLE workspace_proxies
DROP CONSTRAINT workspace_proxies_region_id_unique,
DROP COLUMN region_id,
DROP COLUMN derp_enabled;
COMMIT;
@@ -0,0 +1,13 @@
BEGIN;
ALTER TABLE replicas
ADD COLUMN "primary" boolean NOT NULL DEFAULT true;
ALTER TABLE workspace_proxies
-- Adding a serial to a table without a default value will be filled as you
-- would expect on versions of Postgres >= 9 AFAIK (which we require).
ADD COLUMN region_id serial NOT NULL,
ADD COLUMN derp_enabled boolean NOT NULL DEFAULT true,
ADD CONSTRAINT workspace_proxies_region_id_unique UNIQUE (region_id);
COMMIT;
+3
View File
@@ -1539,6 +1539,7 @@ type Replica struct {
DatabaseLatency int32 `db:"database_latency" json:"database_latency"`
Version string `db:"version" json:"version"`
Error string `db:"error" json:"error"`
Primary bool `db:"primary" json:"primary"`
}
type SiteConfig struct {
@@ -1936,6 +1937,8 @@ type WorkspaceProxy struct {
Deleted bool `db:"deleted" json:"deleted"`
// Hashed secret is used to authenticate the workspace proxy using a session token.
TokenHashedSecret []byte `db:"token_hashed_secret" json:"token_hashed_secret"`
RegionID int32 `db:"region_id" json:"region_id"`
DerpEnabled bool `db:"derp_enabled" json:"derp_enabled"`
}
type WorkspaceResource struct {
+1
View File
@@ -97,6 +97,7 @@ type sqlcQuerier interface {
GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error)
GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error)
GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.UUID) (int64, error)
GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error)
GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error)
GetServiceBanner(ctx context.Context) (string, error)
GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error)
+73 -16
View File
@@ -2848,7 +2848,7 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a
const getWorkspaceProxies = `-- name: GetWorkspaceProxies :many
SELECT
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled
FROM
workspace_proxies
WHERE
@@ -2875,6 +2875,8 @@ func (q *sqlQuerier) GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy,
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
); err != nil {
return nil, err
}
@@ -2891,7 +2893,7 @@ func (q *sqlQuerier) GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy,
const getWorkspaceProxyByHostname = `-- name: GetWorkspaceProxyByHostname :one
SELECT
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled
FROM
workspace_proxies
WHERE
@@ -2947,13 +2949,15 @@ func (q *sqlQuerier) GetWorkspaceProxyByHostname(ctx context.Context, arg GetWor
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
)
return i, err
}
const getWorkspaceProxyByID = `-- name: GetWorkspaceProxyByID :one
SELECT
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled
FROM
workspace_proxies
WHERE
@@ -2976,13 +2980,15 @@ func (q *sqlQuerier) GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (W
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
)
return i, err
}
const getWorkspaceProxyByName = `-- name: GetWorkspaceProxyByName :one
SELECT
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled
FROM
workspace_proxies
WHERE
@@ -3006,6 +3012,8 @@ func (q *sqlQuerier) GetWorkspaceProxyByName(ctx context.Context, name string) (
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
)
return i, err
}
@@ -3019,13 +3027,14 @@ INSERT INTO
name,
display_name,
icon,
derp_enabled,
token_hashed_secret,
created_at,
updated_at,
deleted
)
VALUES
($1, '', '', $2, $3, $4, $5, $6, $7, false) RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
($1, '', '', $2, $3, $4, $5, $6, $7, $8, false) RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled
`
type InsertWorkspaceProxyParams struct {
@@ -3033,6 +3042,7 @@ type InsertWorkspaceProxyParams struct {
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
Icon string `db:"icon" json:"icon"`
DerpEnabled bool `db:"derp_enabled" json:"derp_enabled"`
TokenHashedSecret []byte `db:"token_hashed_secret" json:"token_hashed_secret"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
@@ -3044,6 +3054,7 @@ func (q *sqlQuerier) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspa
arg.Name,
arg.DisplayName,
arg.Icon,
arg.DerpEnabled,
arg.TokenHashedSecret,
arg.CreatedAt,
arg.UpdatedAt,
@@ -3060,6 +3071,8 @@ func (q *sqlQuerier) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspa
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
)
return i, err
}
@@ -3068,22 +3081,29 @@ const registerWorkspaceProxy = `-- name: RegisterWorkspaceProxy :one
UPDATE
workspace_proxies
SET
url = $1,
wildcard_hostname = $2,
url = $1 :: text,
wildcard_hostname = $2 :: text,
derp_enabled = $3 :: boolean,
updated_at = Now()
WHERE
id = $3
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
id = $4
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled
`
type RegisterWorkspaceProxyParams struct {
Url string `db:"url" json:"url"`
WildcardHostname string `db:"wildcard_hostname" json:"wildcard_hostname"`
DerpEnabled bool `db:"derp_enabled" json:"derp_enabled"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) {
row := q.db.QueryRowContext(ctx, registerWorkspaceProxy, arg.Url, arg.WildcardHostname, arg.ID)
row := q.db.QueryRowContext(ctx, registerWorkspaceProxy,
arg.Url,
arg.WildcardHostname,
arg.DerpEnabled,
arg.ID,
)
var i WorkspaceProxy
err := row.Scan(
&i.ID,
@@ -3096,6 +3116,8 @@ func (q *sqlQuerier) RegisterWorkspaceProxy(ctx context.Context, arg RegisterWor
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
)
return i, err
}
@@ -3118,7 +3140,7 @@ SET
updated_at = Now()
WHERE
id = $5
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled
`
type UpdateWorkspaceProxyParams struct {
@@ -3150,6 +3172,8 @@ func (q *sqlQuerier) UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspa
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
)
return i, err
}
@@ -3230,8 +3254,32 @@ func (q *sqlQuerier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt
return err
}
const getReplicaByID = `-- name: GetReplicaByID :one
SELECT id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary" FROM replicas WHERE id = $1
`
func (q *sqlQuerier) GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) {
row := q.db.QueryRowContext(ctx, getReplicaByID, id)
var i Replica
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.StartedAt,
&i.StoppedAt,
&i.UpdatedAt,
&i.Hostname,
&i.RegionID,
&i.RelayAddress,
&i.DatabaseLatency,
&i.Version,
&i.Error,
&i.Primary,
)
return i, err
}
const getReplicasUpdatedAfter = `-- name: GetReplicasUpdatedAfter :many
SELECT id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error FROM replicas WHERE updated_at > $1 AND stopped_at IS NULL
SELECT id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary" FROM replicas WHERE updated_at > $1 AND stopped_at IS NULL
`
func (q *sqlQuerier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) {
@@ -3255,6 +3303,7 @@ func (q *sqlQuerier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time
&i.DatabaseLatency,
&i.Version,
&i.Error,
&i.Primary,
); err != nil {
return nil, err
}
@@ -3279,8 +3328,9 @@ INSERT INTO replicas (
region_id,
relay_address,
version,
database_latency
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error
database_latency,
"primary"
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary"
`
type InsertReplicaParams struct {
@@ -3293,6 +3343,7 @@ type InsertReplicaParams struct {
RelayAddress string `db:"relay_address" json:"relay_address"`
Version string `db:"version" json:"version"`
DatabaseLatency int32 `db:"database_latency" json:"database_latency"`
Primary bool `db:"primary" json:"primary"`
}
func (q *sqlQuerier) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) {
@@ -3306,6 +3357,7 @@ func (q *sqlQuerier) InsertReplica(ctx context.Context, arg InsertReplicaParams)
arg.RelayAddress,
arg.Version,
arg.DatabaseLatency,
arg.Primary,
)
var i Replica
err := row.Scan(
@@ -3320,6 +3372,7 @@ func (q *sqlQuerier) InsertReplica(ctx context.Context, arg InsertReplicaParams)
&i.DatabaseLatency,
&i.Version,
&i.Error,
&i.Primary,
)
return i, err
}
@@ -3334,8 +3387,9 @@ UPDATE replicas SET
hostname = $7,
version = $8,
error = $9,
database_latency = $10
WHERE id = $1 RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error
database_latency = $10,
"primary" = $11
WHERE id = $1 RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary"
`
type UpdateReplicaParams struct {
@@ -3349,6 +3403,7 @@ type UpdateReplicaParams struct {
Version string `db:"version" json:"version"`
Error string `db:"error" json:"error"`
DatabaseLatency int32 `db:"database_latency" json:"database_latency"`
Primary bool `db:"primary" json:"primary"`
}
func (q *sqlQuerier) UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) {
@@ -3363,6 +3418,7 @@ func (q *sqlQuerier) UpdateReplica(ctx context.Context, arg UpdateReplicaParams)
arg.Version,
arg.Error,
arg.DatabaseLatency,
arg.Primary,
)
var i Replica
err := row.Scan(
@@ -3377,6 +3433,7 @@ func (q *sqlQuerier) UpdateReplica(ctx context.Context, arg UpdateReplicaParams)
&i.DatabaseLatency,
&i.Version,
&i.Error,
&i.Primary,
)
return i, err
}
+5 -3
View File
@@ -7,20 +7,22 @@ INSERT INTO
name,
display_name,
icon,
derp_enabled,
token_hashed_secret,
created_at,
updated_at,
deleted
)
VALUES
($1, '', '', $2, $3, $4, $5, $6, $7, false) RETURNING *;
($1, '', '', $2, $3, $4, $5, $6, $7, $8, false) RETURNING *;
-- name: RegisterWorkspaceProxy :one
UPDATE
workspace_proxies
SET
url = @url,
wildcard_hostname = @wildcard_hostname,
url = @url :: text,
wildcard_hostname = @wildcard_hostname :: text,
derp_enabled = @derp_enabled :: boolean,
updated_at = Now()
WHERE
id = @id
+8 -3
View File
@@ -1,6 +1,9 @@
-- name: GetReplicasUpdatedAfter :many
SELECT * FROM replicas WHERE updated_at > $1 AND stopped_at IS NULL;
-- name: GetReplicaByID :one
SELECT * FROM replicas WHERE id = $1;
-- name: InsertReplica :one
INSERT INTO replicas (
id,
@@ -11,8 +14,9 @@ INSERT INTO replicas (
region_id,
relay_address,
version,
database_latency
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *;
database_latency,
"primary"
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *;
-- name: UpdateReplica :one
UPDATE replicas SET
@@ -24,7 +28,8 @@ UPDATE replicas SET
hostname = $7,
version = $8,
error = $9,
database_latency = $10
database_latency = $10,
"primary" = $11
WHERE id = $1 RETURNING *;
-- name: DeleteReplicasUpdatedBefore :exec
+1
View File
@@ -22,6 +22,7 @@ const (
UniqueWorkspaceBuildParametersWorkspaceBuildIDNameKey UniqueConstraint = "workspace_build_parameters_workspace_build_id_name_key" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_name_key UNIQUE (workspace_build_id, name);
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
UniqueWorkspaceBuildsWorkspaceIDBuildNumberKey UniqueConstraint = "workspace_builds_workspace_id_build_number_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_build_number_key UNIQUE (workspace_id, build_number);
UniqueWorkspaceProxiesRegionIDUnique UniqueConstraint = "workspace_proxies_region_id_unique" // ALTER TABLE ONLY workspace_proxies ADD CONSTRAINT workspace_proxies_region_id_unique UNIQUE (region_id);
UniqueWorkspaceResourceMetadataName UniqueConstraint = "workspace_resource_metadata_name" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_name UNIQUE (workspace_resource_id, key);
UniqueIndexApiKeyName UniqueConstraint = "idx_api_key_name" // CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type);
UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name);
+4 -1
View File
@@ -170,7 +170,10 @@ func (r *DERPNodeReport) derpURL() *url.URL {
derpURL.Scheme = "http"
}
if r.Node.HostName == "" {
derpURL.Host = fmt.Sprintf("%s:%d", r.Node.IPv4, r.Node.DERPPort)
derpURL.Host = r.Node.IPv4
}
if r.Node.DERPPort != 0 {
derpURL.Host = fmt.Sprintf("%s:%d", derpURL.Host, r.Node.DERPPort)
}
return derpURL
+6 -10
View File
@@ -151,11 +151,9 @@ func Write(ctx context.Context, rw http.ResponseWriter, status int, response int
enc := json.NewEncoder(rw)
enc.SetEscapeHTML(true)
err := enc.Encode(response)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
// We can't really do much about these errors, it's probably due to a
// dropped connection.
_ = enc.Encode(response)
}
func WriteIndent(ctx context.Context, rw http.ResponseWriter, status int, response interface{}) {
@@ -169,11 +167,9 @@ func WriteIndent(ctx context.Context, rw http.ResponseWriter, status int, respon
enc.SetEscapeHTML(true)
enc.SetIndent("", "\t")
err := enc.Encode(response)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
// We can't really do much about these errors, it's probably due to a
// dropped connection.
_ = enc.Encode(response)
}
// Read decodes JSON from the HTTP request into the value provided. It uses
@@ -142,7 +142,7 @@ func Workspaces(ctx context.Context, registerer prometheus.Registerer, db databa
}
// Agents tracks the total number of workspaces with labels on status.
func Agents(ctx context.Context, logger slog.Logger, registerer prometheus.Registerer, db database.Store, coordinator *atomic.Pointer[tailnet.Coordinator], derpMap *tailcfg.DERPMap, agentInactiveDisconnectTimeout, duration time.Duration) (func(), error) {
func Agents(ctx context.Context, logger slog.Logger, registerer prometheus.Registerer, db database.Store, coordinator *atomic.Pointer[tailnet.Coordinator], derpMapFn func() *tailcfg.DERPMap, agentInactiveDisconnectTimeout, duration time.Duration) (func(), error) {
if duration == 0 {
duration = 1 * time.Minute
}
@@ -223,6 +223,7 @@ func Agents(ctx context.Context, logger slog.Logger, registerer prometheus.Regis
logger.Debug(ctx, "agent metrics collection is starting")
timer := prometheus.NewTimer(metricsCollectorAgents)
derpMap := derpMapFn()
workspaceRows, err := db.GetWorkspaces(ctx, database.GetWorkspacesParams{
AgentInactiveDisconnectTimeoutSeconds: int64(agentInactiveDisconnectTimeout.Seconds()),
@@ -15,6 +15,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/tailcfg"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
@@ -299,10 +300,13 @@ func TestAgents(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
// given
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
derpMapFn := func() *tailcfg.DERPMap {
return derpMap
}
coordinator := tailnet.NewCoordinator(slogtest.Make(t, nil).Leveled(slog.LevelDebug))
coordinatorPtr := atomic.Pointer[tailnet.Coordinator]{}
coordinatorPtr.Store(&coordinator)
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
agentInactiveDisconnectTimeout := 1 * time.Hour // don't need to focus on this value in tests
registry := prometheus.NewRegistry()
@@ -312,7 +316,7 @@ func TestAgents(t *testing.T) {
// when
closeFunc, err := prometheusmetrics.Agents(ctx, slogtest.Make(t, &slogtest.Options{
IgnoreErrors: true,
}), registry, db, &coordinatorPtr, derpMap, agentInactiveDisconnectTimeout, 50*time.Millisecond)
}), registry, db, &coordinatorPtr, derpMapFn, agentInactiveDisconnectTimeout, 50*time.Millisecond)
require.NoError(t, err)
t.Cleanup(closeFunc)
+1 -1
View File
@@ -149,7 +149,7 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
}
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
+40 -1
View File
@@ -2,10 +2,17 @@ package tracing
import (
"bufio"
"flag"
"fmt"
"log"
"net"
"net/http"
"runtime"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/buildinfo"
)
var (
@@ -22,7 +29,8 @@ type StatusWriter struct {
Hijacked bool
responseBody []byte
wroteHeader bool
wroteHeader bool
wroteHeaderStack string
}
func StatusWriterMiddleware(next http.Handler) http.Handler {
@@ -33,6 +41,20 @@ func StatusWriterMiddleware(next http.Handler) http.Handler {
}
func (w *StatusWriter) WriteHeader(status int) {
if buildinfo.IsDev() || flag.Lookup("test.v") != nil {
if w.wroteHeader {
stack := getStackString(2)
wroteHeaderStack := w.wroteHeaderStack
if wroteHeaderStack == "" {
wroteHeaderStack = "unknown"
}
// It's fine that this logs to stdlib logger since it only happens
// in dev builds and tests.
log.Printf("duplicate call to (*StatusWriter.).WriteHeader(%d):\n\nstack: %s\n\nheader written at: %s", status, stack, wroteHeaderStack)
} else {
w.wroteHeaderStack = getStackString(2)
}
}
if !w.wroteHeader {
w.Status = status
w.wroteHeader = true
@@ -89,3 +111,20 @@ func (w *StatusWriter) Flush() {
}
f.Flush()
}
func getStackString(skip int) string {
// Get up to 5 callers, skipping this one and the skip count.
pcs := make([]uintptr, 5)
got := runtime.Callers(skip+1, pcs)
frames := runtime.CallersFrames(pcs[:got])
callers := []string{}
for {
frame, more := frames.Next()
callers = append(callers, fmt.Sprintf("%s:%v", frame.File, frame.Line))
if !more {
break
}
}
return strings.Join(callers, " -> ")
}
+93 -14
View File
@@ -63,7 +63,7 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
return
}
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
@@ -88,7 +88,7 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request)
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
@@ -163,7 +163,7 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request)
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Manifest{
AgentID: apiAgent.ID,
Apps: convertApps(dbApps),
DERPMap: api.DERPMap,
DERPMap: api.DERPMap(),
GitAuthConfigs: len(api.GitAuthConfigs),
EnvironmentVariables: apiAgent.EnvironmentVariables,
StartupScript: apiAgent.StartupScript,
@@ -192,7 +192,7 @@ func (api *API) postWorkspaceAgentStartup(rw http.ResponseWriter, r *http.Reques
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
@@ -590,7 +590,7 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
workspaceAgent := httpmw.WorkspaceAgentParam(r)
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, nil, api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
@@ -686,9 +686,11 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
// See: https://github.com/coder/coder/issues/8218
func (api *API) _dialWorkspaceAgentTailnet(agentID uuid.UUID) (*codersdk.WorkspaceAgentConn, error) {
clientConn, serverConn := net.Pipe()
derpMap := api.DERPMap()
conn, err := tailnet.NewConn(&tailnet.Options{
Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
DERPMap: api.DERPMap,
DERPMap: api.DERPMap(),
Logger: api.Logger.Named("tailnet"),
BlockEndpoints: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
})
@@ -712,14 +714,35 @@ func (api *API) _dialWorkspaceAgentTailnet(agentID uuid.UUID) (*codersdk.Workspa
return left
})
sendNodes, _ := tailnet.ServeCoordinator(clientConn, func(node []*tailnet.Node) error {
err = conn.UpdateNodes(node, true)
if err != nil {
return xerrors.Errorf("update nodes: %w", err)
}
return nil
sendNodes, _ := tailnet.ServeCoordinator(clientConn, func(nodes []*tailnet.Node) error {
return conn.UpdateNodes(nodes, true)
})
conn.SetNodeCallback(sendNodes)
// Check for updated DERP map every 5 seconds.
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
lastDERPMap := derpMap
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
}
derpMap := api.DERPMap()
if lastDERPMap == nil || tailnet.CompareDERPMaps(lastDERPMap, derpMap) {
conn.SetDERPMap(derpMap)
lastDERPMap = derpMap
}
ticker.Reset(5 * time.Second)
}
}
}()
agentConn := codersdk.NewWorkspaceAgentConn(conn, codersdk.WorkspaceAgentConnOptions{
AgentID: agentID,
AgentIP: codersdk.WorkspaceAgentIP,
@@ -743,6 +766,9 @@ func (api *API) _dialWorkspaceAgentTailnet(agentID uuid.UUID) (*codersdk.Workspa
}()
if !agentConn.AwaitReachable(ctx) {
_ = agentConn.Close()
_ = serverConn.Close()
_ = clientConn.Close()
cancel()
return nil, xerrors.Errorf("agent not reachable")
}
return agentConn, nil
@@ -760,7 +786,7 @@ func (api *API) workspaceAgentConnection(rw http.ResponseWriter, r *http.Request
ctx := r.Context()
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentConnectionInfo{
DERPMap: api.DERPMap,
DERPMap: api.DERPMap(),
DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
})
}
@@ -780,10 +806,63 @@ func (api *API) workspaceAgentConnectionGeneric(rw http.ResponseWriter, r *http.
ctx := r.Context()
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentConnectionInfo{
DERPMap: api.DERPMap,
DERPMap: api.DERPMap(),
DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
})
}
// @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
}
nconn := websocket.NetConn(ctx, ws, websocket.MessageBinary)
defer nconn.Close()
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 {
_ = ws.Close(websocket.StatusInternalError, err.Error())
return
}
lastDERPMap = derpMap
}
select {
case <-ctx.Done():
return
case <-api.ctx.Done():
return
case <-ticker.C:
}
ticker.Reset(api.Options.DERPMapUpdateFrequency)
}
}
// @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.
+103
View File
@@ -9,12 +9,14 @@ import (
"runtime"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/tailcfg"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
@@ -25,6 +27,7 @@ import (
"github.com/coder/coder/codersdk/agentsdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/tailnet/tailnettest"
"github.com/coder/coder/testutil"
)
@@ -1247,3 +1250,103 @@ func TestWorkspaceAgent_Startup(t *testing.T) {
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
}
// TestWorkspaceAgent_UpdatedDERP runs a real coderd server, with a real agent
// and a real client, and updates the DERP map live to ensure connections still
// work.
func TestWorkspaceAgent_UpdatedDERP(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
dv := coderdtest.DeploymentValues(t)
err := dv.DERP.Config.BlockDirect.Set("true")
require.NoError(t, err)
client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
DeploymentValues: dv,
})
defer closer.Close()
user := coderdtest.CreateFirstUser(t, client)
originalDerpMap := api.DERPMap()
require.NotNil(t, originalDerpMap)
// Change the DERP mapper to our custom one.
var currentDerpMap atomic.Pointer[tailcfg.DERPMap]
currentDerpMap.Store(originalDerpMap)
derpMapFn := func(_ *tailcfg.DERPMap) *tailcfg.DERPMap {
return currentDerpMap.Load().Clone()
}
api.DERPMapper.Store(&derpMapFn)
// Start workspace a workspace agent.
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(agentToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(agentToken)
agentCloser := agent.New(agent.Options{
Client: agentClient,
Logger: logger.Named("agent"),
})
defer func() {
_ = agentCloser.Close()
}()
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
agentID := resources[0].Agents[0].ID
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Connect from a client.
conn1, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{
Logger: logger.Named("client1"),
})
require.NoError(t, err)
defer conn1.Close()
ok := conn1.AwaitReachable(ctx)
require.True(t, ok)
// Change the DERP map and change the region ID.
newDerpMap, _ := tailnettest.RunDERPAndSTUN(t)
require.NotNil(t, newDerpMap)
newDerpMap.Regions[2] = newDerpMap.Regions[1]
delete(newDerpMap.Regions, 1)
newDerpMap.Regions[2].RegionID = 2
for _, node := range newDerpMap.Regions[2].Nodes {
node.RegionID = 2
}
currentDerpMap.Store(newDerpMap)
// Wait for the agent's DERP map to be updated.
// TODO: this
// Wait for the DERP map to be updated on the existing client.
require.Eventually(t, func() bool {
regionIDs := conn1.Conn.DERPMap().RegionIDs()
return len(regionIDs) == 1 && regionIDs[0] == 2
}, testutil.WaitLong, testutil.IntervalFast)
// The first client should still be able to reach the agent.
ok = conn1.AwaitReachable(ctx)
require.True(t, ok)
// Connect from a second client.
conn2, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{
Logger: logger.Named("client2"),
})
require.NoError(t, err)
defer conn2.Close()
ok = conn2.AwaitReachable(ctx)
require.True(t, ok)
require.Equal(t, []int{2}, conn2.DERPMap().RegionIDs())
}
+1 -1
View File
@@ -835,7 +835,7 @@ func (api *API) convertWorkspaceBuild(
for _, agent := range agents {
apps := appsByAgentID[agent.ID]
apiAgent, err := convertWorkspaceAgent(
api.DERPMap, *api.TailnetCoordinator.Load(), agent, convertApps(apps), api.AgentInactiveDisconnectTimeout,
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(apps), api.AgentInactiveDisconnectTimeout,
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
)
if err != nil {
+3 -2
View File
@@ -645,7 +645,8 @@ func TestWorkspaceBuildDebugMode(t *testing.T) {
// Create user
deploymentValues := coderdtest.DeploymentValues(t)
deploymentValues.EnableTerraformDebugMode = false
err := deploymentValues.EnableTerraformDebugMode.Set("false")
require.NoError(t, err)
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: deploymentValues})
owner := coderdtest.CreateFirstUser(t, adminClient)
@@ -663,7 +664,7 @@ func TestWorkspaceBuildDebugMode(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, err := adminClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
_, err = adminClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
Transition: codersdk.WorkspaceTransitionStart,
LogLevel: "debug",
+20 -2
View File
@@ -191,8 +191,8 @@ func setupAgent(t *testing.T, manifest agentsdk.Manifest, ptyTimeout time.Durati
_ = conn.Close()
})
go coordinator.ServeClient(serverConn, uuid.New(), manifest.AgentID)
sendNode, _ := tailnet.ServeCoordinator(clientConn, func(node []*tailnet.Node) error {
return conn.UpdateNodes(node, false)
sendNode, _ := tailnet.ServeCoordinator(clientConn, func(nodes []*tailnet.Node) error {
return conn.UpdateNodes(nodes, false)
})
conn.SetNodeCallback(sendNode)
agentConn := codersdk.NewWorkspaceAgentConn(conn, codersdk.WorkspaceAgentConnOptions{
@@ -221,6 +221,24 @@ func (c *client) Manifest(_ context.Context) (agentsdk.Manifest, error) {
return c.manifest, nil
}
type closer struct {
closeFunc func() error
}
func (c *closer) Close() error {
return c.closeFunc()
}
func (*client) DERPMapUpdates(_ context.Context) (<-chan agentsdk.DERPMapUpdate, io.Closer, error) {
closed := make(chan struct{})
return make(<-chan agentsdk.DERPMapUpdate), &closer{
closeFunc: func() error {
close(closed)
return nil
},
}, nil
}
func (c *client) Listen(_ context.Context) (net.Conn, error) {
clientConn, serverConn := net.Pipe()
closed := make(chan struct{})