fix: allow coderd to start with an empty DERP map when built-in DERP is disabled (#24544)

Allow coderd to start with an empty base DERP map when built-in DERP
is disabled and no static DERP map is configured, so DERP can come from
workspace proxies after startup.

Also add a DERP healthcheck warning when no DERP servers are currently
available at runtime.

Related to: https://linear.app/codercom/issue/PLAT-43/bug-coderd-unable-to-be-started-if-built-in-derp-server-disabled-and
Related to: https://github.com/coder/coder/issues/22324
This commit is contained in:
George K
2026-04-28 09:17:08 -07:00
committed by GitHub
parent 1926b7e658
commit 3f0e015fe5
13 changed files with 125 additions and 70 deletions
+2
View File
@@ -23736,6 +23736,7 @@ const docTemplate = `{
"EACS04",
"EDERP01",
"EDERP02",
"EDERP03",
"EPD01",
"EPD02",
"EPD03"
@@ -23756,6 +23757,7 @@ const docTemplate = `{
"CodeAccessURLNotOK",
"CodeDERPNodeUsesWebsocket",
"CodeDERPOneNodeUnhealthy",
"CodeDERPNoNodes",
"CodeProvisionerDaemonsNoProvisionerDaemons",
"CodeProvisionerDaemonVersionMismatch",
"CodeProvisionerDaemonAPIMajorVersionDeprecated"
+2
View File
@@ -21863,6 +21863,7 @@
"EACS04",
"EDERP01",
"EDERP02",
"EDERP03",
"EPD01",
"EPD02",
"EPD03"
@@ -21883,6 +21884,7 @@
"CodeAccessURLNotOK",
"CodeDERPNodeUsesWebsocket",
"CodeDERPOneNodeUnhealthy",
"CodeDERPNoNodes",
"CodeProvisionerDaemonsNoProvisionerDaemons",
"CodeProvisionerDaemonVersionMismatch",
"CodeProvisionerDaemonAPIMajorVersionDeprecated"
+13 -6
View File
@@ -560,12 +560,19 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
if !options.DeploymentValues.DERP.Server.Enable.Value() {
region = nil
}
derpMap, err := tailnet.NewDERPMap(ctx, region, stunAddresses,
options.DeploymentValues.DERP.Config.URL.Value(),
options.DeploymentValues.DERP.Config.Path.Value(),
options.DeploymentValues.DERP.Config.BlockDirect.Value(),
)
require.NoError(t, err)
derpConfigURL := options.DeploymentValues.DERP.Config.URL.Value()
derpConfigPath := options.DeploymentValues.DERP.Config.Path.Value()
var derpMap *tailcfg.DERPMap
if region == nil && derpConfigURL == "" && derpConfigPath == "" {
derpMap = &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{}}
} else {
derpMap, err = tailnet.NewDERPMap(
ctx, region, stunAddresses,
derpConfigURL, derpConfigPath,
options.DeploymentValues.DERP.Config.BlockDirect.Value(),
)
require.NoError(t, err)
}
return func(h http.Handler) {
mutex.Lock()
+16
View File
@@ -34,6 +34,7 @@ const (
oneNodeUnhealthy = "Region is operational, but performance might be degraded as one node is unhealthy."
missingNodeReport = "Missing node health report, probably a developer error."
noSTUN = "No STUN servers are available."
noDERP = "No DERP servers are available."
stunMapVaryDest = "STUN returned different addresses; you may be behind a hard NAT."
)
@@ -69,11 +70,20 @@ func (r *Report) Run(ctx context.Context, opts *ReportOptions) {
r.Regions = map[int]*healthsdk.DERPRegionReport{}
// Track whether the map contains any DERP nodes so we can warn if
// it does not.
hasDERP := false
wg := &sync.WaitGroup{}
mu := sync.Mutex{}
wg.Add(len(opts.DERPMap.Regions))
for _, region := range opts.DERPMap.Regions {
for _, node := range region.Nodes {
if !node.STUNOnly {
hasDERP = true
break
}
}
var (
region = region
regionReport = RegionReport{
@@ -103,6 +113,12 @@ func (r *Report) Run(ctx context.Context, opts *ReportOptions) {
mu.Unlock()
}()
}
if !hasDERP {
r.Severity = health.SeverityWarning
r.Warnings = append(r.Warnings, health.Messagef(
health.CodeDERPNoNodes, noDERP,
))
}
ncLogf := func(format string, args ...interface{}) {
mu.Lock()
+37 -5
View File
@@ -64,6 +64,9 @@ func TestDERP(t *testing.T) {
report.Run(ctx, opts)
assert.True(t, report.Healthy)
for _, warning := range report.Warnings {
assert.NotEqual(t, health.CodeDERPNoNodes, warning.Code)
}
for _, region := range report.Regions {
assert.True(t, region.Healthy)
for _, node := range region.NodeReports {
@@ -361,7 +364,7 @@ func TestDERP(t *testing.T) {
}
})
t.Run("STUNOnly/OK", func(t *testing.T) {
t.Run("STUNOnly/WarnsNoDERP", func(t *testing.T) {
t.Parallel()
var (
@@ -389,7 +392,9 @@ func TestDERP(t *testing.T) {
report.Run(ctx, opts)
assert.True(t, report.Healthy)
assert.Equal(t, health.SeverityOK, report.Severity)
assert.Equal(t, health.SeverityWarning, report.Severity)
require.Len(t, report.Warnings, 1)
assert.Equal(t, health.CodeDERPNoNodes, report.Warnings[0].Code)
for _, region := range report.Regions {
assert.True(t, region.Healthy)
assert.Equal(t, health.SeverityOK, region.Severity)
@@ -405,6 +410,27 @@ func TestDERP(t *testing.T) {
}
})
t.Run("NoDERP/EmptyMap", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
report = derphealth.Report{}
opts = &derphealth.ReportOptions{
DERPMap: &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{},
},
}
)
report.Run(ctx, opts)
assert.Equal(t, health.SeverityWarning, report.Severity)
require.Len(t, report.Warnings, 1)
assert.Equal(t, health.CodeDERPNoNodes, report.Warnings[0].Code)
assert.Empty(t, report.Regions)
})
t.Run("STUNOnly/OneBadOneGood", func(t *testing.T) {
t.Parallel()
@@ -443,9 +469,15 @@ func TestDERP(t *testing.T) {
report.Run(ctx, opts)
assert.True(t, report.Healthy)
assert.Equal(t, health.SeverityWarning, report.Severity)
if assert.Len(t, report.Warnings, 1) {
assert.Equal(t, health.CodeDERPOneNodeUnhealthy, report.Warnings[0].Code)
}
assert.Len(t, report.Warnings, 2)
assert.Contains(t, []health.Code{
report.Warnings[0].Code,
report.Warnings[1].Code,
}, health.CodeDERPOneNodeUnhealthy)
assert.Contains(t, []health.Code{
report.Warnings[0].Code,
report.Warnings[1].Code,
}, health.CodeDERPNoNodes)
for _, region := range report.Regions {
assert.True(t, region.Healthy)
assert.Equal(t, health.SeverityWarning, region.Severity)
+1
View File
@@ -36,6 +36,7 @@ const (
CodeDERPNodeUsesWebsocket Code = `EDERP01`
CodeDERPOneNodeUnhealthy Code = `EDERP02`
CodeDERPNoNodes Code = `EDERP03`
CodeSTUNNoNodes = `ESTUN01`
CodeSTUNMapVaryDest = `ESTUN02`