mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add derpserver to wsproxy, add proxies to derpmap (#7311)
This commit is contained in:
Generated
+85
-9
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+83
-9
@@ -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
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Generated
+20
-2
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{})
|
||||
|
||||
Reference in New Issue
Block a user