Skip to content

Commit 17b9e3f

Browse files
committed
chore: Refactor Enterprise code to layer on top of AGPL
This is an experiment to invert the import order of the Enterprise code to layer on top of AGPL.
1 parent a209825 commit 17b9e3f

29 files changed

+1083
-1951
lines changed

cli/root.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ func Core() []*cobra.Command {
9696
}
9797

9898
func AGPL() []*cobra.Command {
99-
all := append(Core(), Server(coderd.New))
99+
all := append(Core(), Server(func(_ context.Context, o *coderd.Options) (*coderd.API, error) {
100+
return coderd.New(o), nil
101+
}))
100102
return all
101103
}
102104

cli/server.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import (
7070
)
7171

7272
// nolint:gocyclo
73-
func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
73+
func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error)) *cobra.Command {
7474
var (
7575
accessURL string
7676
address string
@@ -506,7 +506,10 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
506506
), promAddress, "prometheus")()
507507
}
508508

509-
coderAPI := newAPI(options)
509+
coderAPI, err := newAPI(ctx, options)
510+
if err != nil {
511+
return err
512+
}
510513
defer coderAPI.Close()
511514

512515
client := codersdk.New(localURL)
@@ -553,7 +556,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
553556
// These errors are typically noise like "TLS: EOF". Vault does similar:
554557
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
555558
ErrorLog: log.New(io.Discard, "", 0),
556-
Handler: coderAPI.Handler,
559+
Handler: coderAPI.RootHandler,
557560
BaseContext: func(_ net.Listener) context.Context {
558561
return shutdownConnsCtx
559562
},

coderd/audit/request.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@ import (
1212

1313
"cdr.dev/slog"
1414
"github.com/coder/coder/coderd/database"
15-
"github.com/coder/coder/coderd/features"
1615
"github.com/coder/coder/coderd/httpapi"
1716
"github.com/coder/coder/coderd/httpmw"
1817
)
1918

2019
type RequestParams struct {
21-
Features features.Service
22-
Log slog.Logger
20+
Audit Auditor
21+
Log slog.Logger
2322

2423
Request *http.Request
2524
Action database.AuditAction
@@ -102,15 +101,6 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
102101
params: p,
103102
}
104103

105-
feats := struct {
106-
Audit Auditor
107-
}{}
108-
err := p.Features.Get(&feats)
109-
if err != nil {
110-
p.Log.Error(p.Request.Context(), "unable to get auditor interface", slog.Error(err))
111-
return req, func() {}
112-
}
113-
114104
return req, func() {
115105
ctx := context.Background()
116106
logCtx := p.Request.Context()
@@ -120,15 +110,15 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
120110
return
121111
}
122112

123-
diff := Diff(feats.Audit, req.Old, req.New)
113+
diff := Diff(p.Audit, req.Old, req.New)
124114
diffRaw, _ := json.Marshal(diff)
125115

126116
ip, err := parseIP(p.Request.RemoteAddr)
127117
if err != nil {
128118
p.Log.Warn(logCtx, "parse ip", slog.Error(err))
129119
}
130120

131-
err = feats.Audit.Export(ctx, database.AuditLog{
121+
err = p.Audit.Export(ctx, database.AuditLog{
132122
ID: uuid.New(),
133123
Time: database.Now(),
134124
UserID: httpmw.APIKey(p.Request).UserID,

coderd/authorize.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type HTTPAuthorizer struct {
4242
// return
4343
// }
4444
func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
45-
return api.httpAuth.Authorize(r, action, object)
45+
return api.HTTPAuth.Authorize(r, action, object)
4646
}
4747

4848
// Authorize will return false if the user is not authorized to do the action.

coderd/coderd.go

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/url"
88
"path/filepath"
99
"sync"
10+
"sync/atomic"
1011
"time"
1112

1213
"github.com/andybalholm/brotli"
@@ -25,9 +26,9 @@ import (
2526

2627
"cdr.dev/slog"
2728
"github.com/coder/coder/buildinfo"
29+
"github.com/coder/coder/coderd/audit"
2830
"github.com/coder/coder/coderd/awsidentity"
2931
"github.com/coder/coder/coderd/database"
30-
"github.com/coder/coder/coderd/features"
3132
"github.com/coder/coder/coderd/gitsshkey"
3233
"github.com/coder/coder/coderd/httpapi"
3334
"github.com/coder/coder/coderd/httpmw"
@@ -52,6 +53,7 @@ type Options struct {
5253
// CacheDir is used for caching files served by the API.
5354
CacheDir string
5455

56+
Auditor audit.Auditor
5557
AgentConnectionUpdateFrequency time.Duration
5658
AgentInactiveDisconnectTimeout time.Duration
5759
// APIRateLimit is the minutely throughput rate limit per user or ip.
@@ -72,8 +74,6 @@ type Options struct {
7274
TURNServer *turnconn.Server
7375
TracerProvider *sdktrace.TracerProvider
7476
AutoImportTemplates []AutoImportTemplate
75-
LicenseHandler http.Handler
76-
FeaturesService features.Service
7777

7878
TailscaleEnable bool
7979
TailnetCoordinator *tailnet.Coordinator
@@ -85,6 +85,9 @@ type Options struct {
8585

8686
// New constructs a Coder API handler.
8787
func New(options *Options) *API {
88+
if options == nil {
89+
options = &Options{}
90+
}
8891
if options.AgentConnectionUpdateFrequency == 0 {
8992
options.AgentConnectionUpdateFrequency = 3 * time.Second
9093
}
@@ -110,11 +113,8 @@ func New(options *Options) *API {
110113
if options.TailnetCoordinator == nil {
111114
options.TailnetCoordinator = tailnet.NewCoordinator()
112115
}
113-
if options.LicenseHandler == nil {
114-
options.LicenseHandler = licenses()
115-
}
116-
if options.FeaturesService == nil {
117-
options.FeaturesService = &featuresService{}
116+
if options.Auditor == nil {
117+
options.Auditor = audit.NewNop()
118118
}
119119

120120
siteCacheDir := options.CacheDir
@@ -135,14 +135,17 @@ func New(options *Options) *API {
135135
r := chi.NewRouter()
136136
api := &API{
137137
Options: options,
138-
Handler: r,
138+
RootHandler: r,
139139
siteHandler: site.Handler(site.FS(), binFS),
140-
httpAuth: &HTTPAuthorizer{
140+
HTTPAuth: &HTTPAuthorizer{
141141
Authorizer: options.Authorizer,
142142
Logger: options.Logger,
143143
},
144144
metricsCache: metricsCache,
145+
Auditor: atomic.Pointer[audit.Auditor]{},
145146
}
147+
api.Auditor.Store(&options.Auditor)
148+
146149
if options.TailscaleEnable {
147150
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0)
148151
} else {
@@ -194,6 +197,8 @@ func New(options *Options) *API {
194197
})
195198

196199
r.Route("/api/v2", func(r chi.Router) {
200+
api.APIHandler = r
201+
197202
r.NotFound(func(rw http.ResponseWriter, r *http.Request) {
198203
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
199204
Message: "Route not found.",
@@ -460,12 +465,9 @@ func New(options *Options) *API {
460465
})
461466
r.Route("/entitlements", func(r chi.Router) {
462467
r.Use(apiKeyMiddleware)
463-
r.Get("/", api.FeaturesService.EntitlementsAPI)
464-
})
465-
r.Route("/licenses", func(r chi.Router) {
466-
r.Use(apiKeyMiddleware)
467-
r.Mount("/", options.LicenseHandler)
468+
r.Get("/", entitlements)
468469
})
470+
r.HandleFunc("/licenses", unsupported)
469471
})
470472

471473
r.NotFound(compressHandler(http.HandlerFunc(api.siteHandler.ServeHTTP)).ServeHTTP)
@@ -477,12 +479,14 @@ type API struct {
477479

478480
derpServer *derp.Server
479481

480-
Handler chi.Router
482+
Auditor atomic.Pointer[audit.Auditor]
483+
RootHandler chi.Router
484+
APIHandler chi.Router
481485
siteHandler http.Handler
482486
websocketWaitMutex sync.Mutex
483487
websocketWaitGroup sync.WaitGroup
484488
workspaceAgentCache *wsconncache.Cache
485-
httpAuth *HTTPAuthorizer
489+
HTTPAuth *HTTPAuthorizer
486490

487491
metricsCache *metricscache.Cache
488492
}
@@ -517,3 +521,26 @@ func compressHandler(h http.Handler) http.Handler {
517521

518522
return cmp.Handler(h)
519523
}
524+
525+
func entitlements(rw http.ResponseWriter, _ *http.Request) {
526+
feats := make(map[string]codersdk.Feature)
527+
for _, f := range codersdk.FeatureNames {
528+
feats[f] = codersdk.Feature{
529+
Entitlement: codersdk.EntitlementNotEntitled,
530+
Enabled: false,
531+
}
532+
}
533+
httpapi.Write(rw, http.StatusOK, codersdk.Entitlements{
534+
Features: feats,
535+
Warnings: []string{},
536+
HasLicense: false,
537+
})
538+
}
539+
540+
func unsupported(rw http.ResponseWriter, _ *http.Request) {
541+
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
542+
Message: "Unsupported",
543+
Detail: "These endpoints are not supported in AGPL-licensed Coder",
544+
Validations: nil,
545+
})
546+
}

coderd/coderd_test.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/coder/coder/buildinfo"
1919
"github.com/coder/coder/coderd/coderdtest"
20+
"github.com/coder/coder/codersdk"
2021
"github.com/coder/coder/tailnet"
2122
"github.com/coder/coder/testutil"
2223
)
@@ -38,16 +39,6 @@ func TestBuildInfo(t *testing.T) {
3839
require.Equal(t, buildinfo.Version(), buildInfo.Version, "version")
3940
}
4041

41-
// TestAuthorizeAllEndpoints will check `authorize` is called on every endpoint registered.
42-
func TestAuthorizeAllEndpoints(t *testing.T) {
43-
t.Parallel()
44-
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
45-
defer cancel()
46-
a := coderdtest.NewAuthTester(ctx, t, nil)
47-
skipRoutes, assertRoute := coderdtest.AGPLRoutes(a)
48-
a.Test(ctx, assertRoute, skipRoutes)
49-
}
50-
5142
func TestDERP(t *testing.T) {
5243
t.Parallel()
5344
client := coderdtest.New(t, nil)
@@ -124,3 +115,22 @@ func TestDERPLatencyCheck(t *testing.T) {
124115
defer res.Body.Close()
125116
require.Equal(t, http.StatusOK, res.StatusCode)
126117
}
118+
119+
func TestEntitlements(t *testing.T) {
120+
t.Parallel()
121+
t.Run("GET", func(t *testing.T) {
122+
t.Parallel()
123+
client := coderdtest.New(t, nil)
124+
_ = coderdtest.CreateFirstUser(t, client)
125+
result, err := client.Entitlements(context.Background())
126+
require.NoError(t, err)
127+
assert.False(t, result.HasLicense)
128+
assert.Empty(t, result.Warnings)
129+
for _, f := range codersdk.FeatureNames {
130+
require.Contains(t, result.Features, f)
131+
fe := result.Features[f]
132+
assert.False(t, fe.Enabled)
133+
assert.Equal(t, codersdk.EntitlementNotEntitled, fe.Entitlement)
134+
}
135+
})
136+
}

coderd/coderdtest/coderdtest.go

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ type Options struct {
7979

8080
// IncludeProvisionerDaemon when true means to start an in-memory provisionerD
8181
IncludeProvisionerDaemon bool
82-
APIBuilder func(*coderd.Options) *coderd.API
8382
MetricsCacheRefreshInterval time.Duration
8483
AgentStatsRefreshInterval time.Duration
8584
}
@@ -115,10 +114,8 @@ func newWithCloser(t *testing.T, options *Options) (*codersdk.Client, io.Closer)
115114
return client, closer
116115
}
117116

118-
// newWithAPI constructs an in-memory API instance and returns a client to talk to it.
119-
// Most tests never need a reference to the API, but AuthorizationTest in this module uses it.
120-
// Do not expose the API or wrath shall descend upon thee.
121-
func newWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *coderd.API) {
117+
// NewOptions constructs a new set of
118+
func NewOptions(t *testing.T, options *Options) (*httptest.Server, *coderd.Options) {
122119
if options == nil {
123120
options = &Options{}
124121
}
@@ -139,9 +136,6 @@ func newWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
139136
close(options.AutobuildStats)
140137
})
141138
}
142-
if options.APIBuilder == nil {
143-
options.APIBuilder = coderd.New
144-
}
145139

146140
// This can be hotswapped for a live database instance.
147141
db := databasefake.New()
@@ -199,13 +193,7 @@ func newWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
199193
_ = turnServer.Close()
200194
})
201195

202-
features := coderd.DisabledImplementations
203-
if options.Auditor != nil {
204-
features.Auditor = options.Auditor
205-
}
206-
207-
// We set the handler after server creation for the access URL.
208-
coderAPI := options.APIBuilder(&coderd.Options{
196+
return srv, &coderd.Options{
209197
AgentConnectionUpdateFrequency: 150 * time.Millisecond,
210198
// Force a long disconnection timeout to ensure
211199
// agents are not marked as disconnected during slow tests.
@@ -216,6 +204,7 @@ func newWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
216204
Database: db,
217205
Pubsub: pubsub,
218206

207+
Auditor: options.Auditor,
219208
AWSCertificates: options.AWSCertificates,
220209
AzureCertificates: options.AzureCertificates,
221210
GithubOAuth2Config: options.GithubOAuth2Config,
@@ -247,22 +236,31 @@ func newWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
247236
AutoImportTemplates: options.AutoImportTemplates,
248237
MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval,
249238
AgentStatsRefreshInterval: options.AgentStatsRefreshInterval,
250-
FeaturesService: coderd.NewMockFeaturesService(features),
251-
})
239+
}
240+
}
241+
242+
// newWithAPI constructs an in-memory API instance and returns a client to talk to it.
243+
// Most tests never need a reference to the API, but AuthorizationTest in this module uses it.
244+
// Do not expose the API or wrath shall descend upon thee.
245+
func newWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *coderd.API) {
246+
if options == nil {
247+
options = &Options{}
248+
}
249+
srv, newOptions := NewOptions(t, options)
250+
// We set the handler after server creation for the access URL.
251+
coderAPI := coderd.New(newOptions)
252252
t.Cleanup(func() {
253253
_ = coderAPI.Close()
254254
})
255-
srv.Config.Handler = coderAPI.Handler
256-
255+
srv.Config.Handler = coderAPI.RootHandler
257256
var provisionerCloser io.Closer = nopcloser{}
258257
if options.IncludeProvisionerDaemon {
259258
provisionerCloser = NewProvisionerDaemon(t, coderAPI)
260259
}
261260
t.Cleanup(func() {
262261
_ = provisionerCloser.Close()
263262
})
264-
265-
return codersdk.New(serverURL), provisionerCloser, coderAPI
263+
return codersdk.New(coderAPI.AccessURL), provisionerCloser, coderAPI
266264
}
267265

268266
// NewProvisionerDaemon launches a provisionerd instance configured to work

0 commit comments

Comments
 (0)