Skip to content

Commit 186a5e2

Browse files
committed
Add warnings for HA
1 parent 557b390 commit 186a5e2

File tree

10 files changed

+156
-36
lines changed

10 files changed

+156
-36
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"derphttp",
2020
"derpmap",
2121
"devel",
22+
"dflags",
2223
"drpc",
2324
"drpcconn",
2425
"drpcmux",
@@ -88,6 +89,7 @@
8889
"replicasync",
8990
"retrier",
9091
"rpty",
92+
"SCIM",
9193
"sdkproto",
9294
"sdktrace",
9395
"Signup",

cli/deployment/flags.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,6 @@ func Flags() *codersdk.DeploymentFlags {
130130
Description: "The bind address to serve pprof.",
131131
Default: "127.0.0.1:6060",
132132
},
133-
HighAvailability: &codersdk.BoolFlag{
134-
Name: "High Availability",
135-
Flag: "high-availability",
136-
EnvVar: "CODER_HIGH_AVAILABILITY",
137-
Description: "Specifies whether high availability is enabled.",
138-
Default: true,
139-
Enterprise: true,
140-
},
141133
CacheDir: &codersdk.StringFlag{
142134
Name: "Cache Directory",
143135
Flag: "cache-dir",

codersdk/deployment.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package codersdk
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
type DeploymentInfo struct {
10+
Replicas []Replica `json:"replicas"`
11+
}
12+
13+
type Replica struct {
14+
// ID is the unique identifier for the replica.
15+
ID uuid.UUID `json:"id"`
16+
// Hostname is the hostname of the replica.
17+
Hostname string `json:"hostname"`
18+
// CreatedAt is when the replica was first seen.
19+
CreatedAt time.Time `json:"created_at"`
20+
// Active determines whether the replica is online.
21+
Active bool `json:"active"`
22+
// RelayAddress is the accessible address to relay DERP connections.
23+
RelayAddress string `json:"relay_address"`
24+
// Error is the error.
25+
Error string `json:"error"`
26+
}

codersdk/flags.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ type DeploymentFlags struct {
6060
Verbose *BoolFlag `json:"verbose" typescript:",notnull"`
6161
AuditLogging *BoolFlag `json:"audit_logging" typescript:",notnull"`
6262
BrowserOnly *BoolFlag `json:"browser_only" typescript:",notnull"`
63-
HighAvailability *BoolFlag `json:"high_availability" typescript:",notnull"`
6463
SCIMAuthHeader *StringFlag `json:"scim_auth_header" typescript:",notnull"`
6564
UserWorkspaceQuota *IntFlag `json:"user_workspace_quota" typescript:",notnull"`
6665
}

enterprise/cli/server.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package cli
22

33
import (
44
"context"
5+
"net/url"
56

67
"github.com/google/uuid"
78
"github.com/spf13/cobra"
9+
"golang.org/x/xerrors"
810

911
"cdr.dev/slog"
1012

@@ -20,22 +22,35 @@ func server() *cobra.Command {
2022
dflags := deployment.Flags()
2123
cmd := agpl.Server(dflags, func(ctx context.Context, cfg config.Root, options *agplcoderd.Options) (*agplcoderd.API, error) {
2224
replicaIDRaw, err := cfg.ReplicaID().Read()
25+
generatedReplicaID := false
2326
if err != nil {
2427
replicaIDRaw = uuid.NewString()
28+
generatedReplicaID = true
2529
}
2630
replicaID, err := uuid.Parse(replicaIDRaw)
2731
if err != nil {
2832
options.Logger.Warn(ctx, "failed to parse replica id", slog.Error(err), slog.F("replica_id", replicaIDRaw))
2933
replicaID = uuid.New()
34+
generatedReplicaID = true
35+
}
36+
if generatedReplicaID {
37+
// Make sure we save it to be reused later!
38+
_ = cfg.ReplicaID().Write(replicaID.String())
39+
}
40+
41+
if dflags.DerpServerRelayAddress.Value != "" {
42+
_, err := url.Parse(dflags.DerpServerRelayAddress.Value)
43+
if err != nil {
44+
return nil, xerrors.Errorf("derp-server-relay-address must be a valid HTTP URL: %w", err)
45+
}
3046
}
31-
o := &coderd.Options{
32-
AuditLogging: dflags.AuditLogging.Value,
33-
BrowserOnly: dflags.BrowserOnly.Value,
34-
SCIMAPIKey: []byte(dflags.SCIMAuthHeader.Value),
35-
UserWorkspaceQuota: dflags.UserWorkspaceQuota.Value,
36-
RBAC: true,
37-
HighAvailability: dflags.HighAvailability.Value,
3847

48+
o := &coderd.Options{
49+
AuditLogging: dflags.AuditLogging.Value,
50+
BrowserOnly: dflags.BrowserOnly.Value,
51+
SCIMAPIKey: []byte(dflags.SCIMAuthHeader.Value),
52+
UserWorkspaceQuota: dflags.UserWorkspaceQuota.Value,
53+
RBAC: true,
3954
ReplicaID: replicaID,
4055
DERPServerRelayAddress: dflags.DerpServerRelayAddress.Value,
4156
DERPServerRegionID: dflags.DerpServerRegionID.Value,
@@ -50,6 +65,5 @@ func server() *cobra.Command {
5065
})
5166

5267
deployment.AttachFlags(cmd.Flags(), dflags, true)
53-
5468
return cmd
5569
}

enterprise/coderd/coderd.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ type Options struct {
146146
BrowserOnly bool
147147
SCIMAPIKey []byte
148148
UserWorkspaceQuota int
149-
HighAvailability bool
150149

151150
// Used for high availability.
152151
DERPServerRelayAddress string
@@ -182,12 +181,12 @@ func (api *API) updateEntitlements(ctx context.Context) error {
182181
api.entitlementsMu.Lock()
183182
defer api.entitlementsMu.Unlock()
184183

185-
entitlements, err := license.Entitlements(ctx, api.Database, api.Logger, api.Keys, map[string]bool{
184+
entitlements, err := license.Entitlements(ctx, api.Database, api.Logger, len(api.replicaManager.All()), api.Keys, map[string]bool{
186185
codersdk.FeatureAuditLog: api.AuditLogging,
187186
codersdk.FeatureBrowserOnly: api.BrowserOnly,
188187
codersdk.FeatureSCIM: len(api.SCIMAPIKey) != 0,
189188
codersdk.FeatureWorkspaceQuota: api.UserWorkspaceQuota != 0,
190-
codersdk.FeatureHighAvailability: api.HighAvailability,
189+
codersdk.FeatureHighAvailability: api.DERPServerRelayAddress != "",
191190
codersdk.FeatureTemplateRBAC: api.RBAC,
192191
})
193192
if err != nil {

enterprise/coderd/coderdenttest/coderdenttest.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,12 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
6363
}
6464
srv, cancelFunc, oop := coderdtest.NewOptions(t, options.Options)
6565
coderAPI, err := coderd.New(context.Background(), &coderd.Options{
66-
RBAC: true,
67-
AuditLogging: options.AuditLogging,
68-
BrowserOnly: options.BrowserOnly,
69-
SCIMAPIKey: options.SCIMAPIKey,
70-
// TODO: Kyle change this before merge!
71-
DERPServerRelayAddress: oop.AccessURL.String() + "/derp",
66+
RBAC: true,
67+
AuditLogging: options.AuditLogging,
68+
BrowserOnly: options.BrowserOnly,
69+
SCIMAPIKey: options.SCIMAPIKey,
70+
DERPServerRelayAddress: oop.AccessURL.String(),
7271
DERPServerRegionID: 1,
73-
HighAvailability: true,
7472
ReplicaID: uuid.New(),
7573
UserWorkspaceQuota: options.UserWorkspaceQuota,
7674
Options: oop,

enterprise/coderd/license/license.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ func Entitlements(
2121
ctx context.Context,
2222
db database.Store,
2323
logger slog.Logger,
24+
replicaCount int,
2425
keys map[string]ed25519.PublicKey,
2526
enablements map[string]bool,
2627
) (codersdk.Entitlements, error) {
@@ -144,6 +145,10 @@ func Entitlements(
144145
if featureName == codersdk.FeatureUserLimit {
145146
continue
146147
}
148+
// High availability has it's own warnings based on replica count!
149+
if featureName == codersdk.FeatureHighAvailability {
150+
continue
151+
}
147152
feature := entitlements.Features[featureName]
148153
if !feature.Enabled {
149154
continue
@@ -161,6 +166,24 @@ func Entitlements(
161166
}
162167
}
163168

169+
if replicaCount > 1 {
170+
feature := entitlements.Features[codersdk.FeatureHighAvailability]
171+
172+
switch feature.Entitlement {
173+
case codersdk.EntitlementNotEntitled:
174+
if entitlements.HasLicense {
175+
entitlements.Warnings = append(entitlements.Warnings,
176+
"You have multiple replicas but your license is not entitled to high availability.")
177+
} else {
178+
entitlements.Warnings = append(entitlements.Warnings,
179+
"You have multiple replicas but high availability is an Enterprise feature. Contact sales to get a license.")
180+
}
181+
case codersdk.EntitlementGracePeriod:
182+
entitlements.Warnings = append(entitlements.Warnings,
183+
"You have multiple replicas but your license for high availability is expired.")
184+
}
185+
}
186+
164187
for _, featureName := range codersdk.FeatureNames {
165188
feature := entitlements.Features[featureName]
166189
if feature.Entitlement == codersdk.EntitlementNotEntitled {

enterprise/coderd/license/license_test.go

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestEntitlements(t *testing.T) {
3131
t.Run("Defaults", func(t *testing.T) {
3232
t.Parallel()
3333
db := databasefake.New()
34-
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, coderdenttest.Keys, all)
34+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, all)
3535
require.NoError(t, err)
3636
require.False(t, entitlements.HasLicense)
3737
require.False(t, entitlements.Trial)
@@ -47,7 +47,7 @@ func TestEntitlements(t *testing.T) {
4747
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{}),
4848
Exp: time.Now().Add(time.Hour),
4949
})
50-
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, coderdenttest.Keys, map[string]bool{})
50+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, map[string]bool{})
5151
require.NoError(t, err)
5252
require.True(t, entitlements.HasLicense)
5353
require.False(t, entitlements.Trial)
@@ -71,7 +71,7 @@ func TestEntitlements(t *testing.T) {
7171
}),
7272
Exp: time.Now().Add(time.Hour),
7373
})
74-
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, coderdenttest.Keys, map[string]bool{})
74+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, map[string]bool{})
7575
require.NoError(t, err)
7676
require.True(t, entitlements.HasLicense)
7777
require.False(t, entitlements.Trial)
@@ -96,14 +96,17 @@ func TestEntitlements(t *testing.T) {
9696
}),
9797
Exp: time.Now().Add(time.Hour),
9898
})
99-
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, coderdenttest.Keys, all)
99+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, all)
100100
require.NoError(t, err)
101101
require.True(t, entitlements.HasLicense)
102102
require.False(t, entitlements.Trial)
103103
for _, featureName := range codersdk.FeatureNames {
104104
if featureName == codersdk.FeatureUserLimit {
105105
continue
106106
}
107+
if featureName == codersdk.FeatureHighAvailability {
108+
continue
109+
}
107110
niceName := strings.Title(strings.ReplaceAll(featureName, "_", " "))
108111
require.Equal(t, codersdk.EntitlementGracePeriod, entitlements.Features[featureName].Entitlement)
109112
require.Contains(t, entitlements.Warnings, fmt.Sprintf("%s is enabled but your license for this feature is expired.", niceName))
@@ -116,14 +119,17 @@ func TestEntitlements(t *testing.T) {
116119
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{}),
117120
Exp: time.Now().Add(time.Hour),
118121
})
119-
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, coderdenttest.Keys, all)
122+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, all)
120123
require.NoError(t, err)
121124
require.True(t, entitlements.HasLicense)
122125
require.False(t, entitlements.Trial)
123126
for _, featureName := range codersdk.FeatureNames {
124127
if featureName == codersdk.FeatureUserLimit {
125128
continue
126129
}
130+
if featureName == codersdk.FeatureHighAvailability {
131+
continue
132+
}
127133
niceName := strings.Title(strings.ReplaceAll(featureName, "_", " "))
128134
// Ensures features that are not entitled are properly disabled.
129135
require.False(t, entitlements.Features[featureName].Enabled)
@@ -142,7 +148,7 @@ func TestEntitlements(t *testing.T) {
142148
}),
143149
Exp: time.Now().Add(time.Hour),
144150
})
145-
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, coderdenttest.Keys, map[string]bool{})
151+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, map[string]bool{})
146152
require.NoError(t, err)
147153
require.True(t, entitlements.HasLicense)
148154
require.Contains(t, entitlements.Warnings, "Your deployment has 2 active users but is only licensed for 1.")
@@ -164,7 +170,7 @@ func TestEntitlements(t *testing.T) {
164170
}),
165171
Exp: time.Now().Add(time.Hour),
166172
})
167-
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, coderdenttest.Keys, map[string]bool{})
173+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, map[string]bool{})
168174
require.NoError(t, err)
169175
require.True(t, entitlements.HasLicense)
170176
require.Empty(t, entitlements.Warnings)
@@ -187,7 +193,7 @@ func TestEntitlements(t *testing.T) {
187193
}),
188194
})
189195

190-
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, coderdenttest.Keys, map[string]bool{})
196+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, map[string]bool{})
191197
require.NoError(t, err)
192198
require.True(t, entitlements.HasLicense)
193199
require.False(t, entitlements.Trial)
@@ -202,7 +208,7 @@ func TestEntitlements(t *testing.T) {
202208
AllFeatures: true,
203209
}),
204210
})
205-
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, coderdenttest.Keys, all)
211+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, all)
206212
require.NoError(t, err)
207213
require.True(t, entitlements.HasLicense)
208214
require.False(t, entitlements.Trial)
@@ -214,4 +220,52 @@ func TestEntitlements(t *testing.T) {
214220
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[featureName].Entitlement)
215221
}
216222
})
223+
224+
t.Run("MultipleReplicasNoLicense", func(t *testing.T) {
225+
t.Parallel()
226+
db := databasefake.New()
227+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, coderdenttest.Keys, all)
228+
require.NoError(t, err)
229+
require.False(t, entitlements.HasLicense)
230+
require.Len(t, entitlements.Warnings, 1)
231+
require.Equal(t, "You have multiple replicas but high availability is an Enterprise feature. Contact sales to get a license.", entitlements.Warnings[0])
232+
})
233+
234+
t.Run("MultipleReplicasNotEntitled", func(t *testing.T) {
235+
t.Parallel()
236+
db := databasefake.New()
237+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
238+
Exp: time.Now().Add(time.Hour),
239+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
240+
AuditLog: true,
241+
}),
242+
})
243+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, coderdenttest.Keys, map[string]bool{
244+
codersdk.FeatureHighAvailability: true,
245+
})
246+
require.NoError(t, err)
247+
require.True(t, entitlements.HasLicense)
248+
require.Len(t, entitlements.Warnings, 1)
249+
require.Equal(t, "You have multiple replicas but your license is not entitled to high availability.", entitlements.Warnings[0])
250+
})
251+
252+
t.Run("MultipleReplicasGrace", func(t *testing.T) {
253+
t.Parallel()
254+
db := databasefake.New()
255+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
256+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
257+
HighAvailability: true,
258+
GraceAt: time.Now().Add(-time.Hour),
259+
ExpiresAt: time.Now().Add(time.Hour),
260+
}),
261+
Exp: time.Now().Add(time.Hour),
262+
})
263+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, coderdenttest.Keys, map[string]bool{
264+
codersdk.FeatureHighAvailability: true,
265+
})
266+
require.NoError(t, err)
267+
require.True(t, entitlements.HasLicense)
268+
require.Len(t, entitlements.Warnings, 1)
269+
require.Equal(t, "You have multiple replicas but your license for high availability is expired.", entitlements.Warnings[0])
270+
})
217271
}

enterprise/derpmesh/derpmesh.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package derpmesh
22

33
import (
44
"context"
5+
"net/url"
56
"sync"
67

78
"golang.org/x/xerrors"
@@ -40,6 +41,18 @@ type Mesh struct {
4041
func (m *Mesh) SetAddresses(addresses []string) {
4142
total := make(map[string]struct{}, 0)
4243
for _, address := range addresses {
44+
addressURL, err := url.Parse(address)
45+
if err != nil {
46+
m.logger.Error(m.ctx, "invalid address", slog.F("address", err), slog.Error(err))
47+
continue
48+
}
49+
derpURL, err := addressURL.Parse("/derp")
50+
if err != nil {
51+
m.logger.Error(m.ctx, "parse derp", slog.F("address", err), slog.Error(err))
52+
continue
53+
}
54+
address = derpURL.String()
55+
4356
total[address] = struct{}{}
4457
added, err := m.addAddress(address)
4558
if err != nil {

0 commit comments

Comments
 (0)