-
Notifications
You must be signed in to change notification settings - Fork 904
chore: Refactor Enterprise code to layer on top of AGPL #4034
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
17b9e3f
to
db6eed8
Compare
This is an experiment to invert the import order of the Enterprise code to layer on top of AGPL.
db6eed8
to
fd05de9
Compare
24cf747
to
04c5d1d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This organization is clearer to me and I had similar ideas when browsing this code for the first time. Overall improvement
enterprise/coderd/coderd.go
Outdated
api.AGPL.APIHandler.Group(func(r chi.Router) { | ||
r.Get("/entitlements", api.entitlements) | ||
r.Route("/licenses", func(r chi.Router) { | ||
r.Use(apiKeyMiddleware) | ||
r.Post("/", api.postLicense) | ||
r.Get("/", api.licenses) | ||
r.Delete("/{id}", api.deleteLicense) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this pattern a lot
skipRoutes, assertRoute := coderdtest.AGPLRoutes(a) | ||
a.Test(ctx, assertRoute, skipRoutes) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without this there are no AGPL tests of authorization on endpoints, which is a loss for AGPL code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I'll fix this up.
enterprise/coderd/coderd.go
Outdated
api.mutex.RLock() | ||
hasLicense := api.hasLicense | ||
activeUsers := api.activeUsers | ||
auditLogs := api.auditLogs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You've replaced something that will scale to a large number of features (entitlements struct) with something that will not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand how wrapping this in a struct enables scale. If we had 10 of these options, I'd agree we should make a struct... but this seems like an easy to solve problem once we approach it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not just the struct itself, but the way the code used to work was that there was a function that computed the struct, which completely specifies and encapsulates the relevant license state. A change in license state resulted in a new struct. Here it's spread across three fields that are mutated in place, so it's conceptually less clear that these are the only things that change when license state changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, fair point. I can encapsulate these inside a struct so the mental model is simpler to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still pending @kylecarbs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, forgot about this... fixed!
enterprise/coderd/coderd.go
Outdated
return nil, xerrors.Errorf("update entitlements: %w", err) | ||
} | ||
api.closeLicenseSubscribe, err = api.Pubsub.Subscribe(pubSubEventLicenses, func(ctx context.Context, message []byte) { | ||
_ = api.updateEntitlements(ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This construction drops errors without logging them, and doesn't retry if we fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Will fix.
enterprise/coderd/coderd.go
Outdated
select { | ||
case <-ctx.Done(): | ||
return | ||
case <-ticker.C: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this time-only loop is simpler, it means we lose retries on error. So, a momentary hiccup in DB connectivity means we don't update for 10 minutes. This kind of thing makes software a pain to operate, and I don't see the value of stripping out something of higher sophistication considering it had good unit tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, good point. I'll revert this.
I think the layering here is easier to conceptualize for me. I like the changes 👍 |
@spikecurtis and I made some good progress discussing the current pitfalls of the approach:
Spike feel free to leave additional improvements if any come to mind! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I like the the composability of Spike's implementation however I'm not a big fan of "injecting" enterprise logic into the core (aka free) API.
I think we should strive for a consistent developer experience for core and enterprise features, and it seems that developing enterprise features today deviates from the way we've developed features in the past. To that end I also think it's important that we try to avoid leaking enterprise-specific logic (specifically routes) into core Coder as much as possible (some leakage may be inevitable).
The aspect of Kyle's PR that I really like is that it wraps the coderd API and injects the enterprise routes on top. I know @spikecurtis you have some reservations about injecting routes into the handler in enterprise/coderd
so I'd love to hear more of your thoughts on that because to me it feels like a cleaner separation of concerns and mirrors the way routes are added in coderd
.
However, I do agree with Spike that there's some improvements that can be made to how we're currently getting features in this PR.The limited surface area of Spike's featureService
implementation is really clean but adding features in the features.go.setImplementation
is somewhat intimidating. It's possible the complexity is simply a result of ensuring correctness but I think it's worth trying to simplify the devex in this regard which is why I lean more towards something akin to the atomic pointer.
coderd/coderd.go
Outdated
RootHandler chi.Router | ||
APIHandler chi.Router |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand why this needs to occur but it looks really sketchy and deserves a comment.
I think it's fine to inject new routes into the handler, but I don't like rewriting existing routes, since this makes it hard for someone reading AGPL code to realize that there are multiple implementations of some routes. |
This uses a context to ensure the same value persists through multiple executions to `Load()`.
🥳 @spikecurtis if we find anything after merge that's questionable, I'm always happy to fix! |
7b6fe85
to
fb35e01
Compare
fb35e01
to
7f9ed39
Compare
This is an experiment to invert the import order of the Enterprise code to layer on top of AGPL.
This change was motivated by a desire to have AGPL Coder be more of a root in the codebase that we can layer on top of. I'm not attached to any patterns here, and I mostly took precedent from testing in
coderd
. These reflect integration tests much more closely than unit tests, but I don't believe this pattern excludes us from unit testing in the future.Colin also needed to call
coderd.API.createUser
, but he couldn't since Enterprise didn't have an instantiatedAPI
for the handlers. Ideally, our solution is to expose that function rather than abstract it further, which this now enables.