Skip to content

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

Merged
merged 25 commits into from
Sep 20, 2022
Merged

Conversation

kylecarbs
Copy link
Member

@kylecarbs kylecarbs commented Sep 13, 2022

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 instantiated API for the handlers. Ideally, our solution is to expose that function rather than abstract it further, which this now enables.

@kylecarbs kylecarbs force-pushed the entfactor branch 3 times, most recently from 17b9e3f to db6eed8 Compare September 13, 2022 03:04
This is an experiment to invert the import order of the Enterprise
code to layer on top of AGPL.
Copy link
Contributor

@f0ssel f0ssel left a 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

Comment on lines 49 to 57
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)
})
})
Copy link
Contributor

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)
}

Copy link
Contributor

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.

Copy link
Member Author

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.

api.mutex.RLock()
hasLicense := api.hasLicense
activeUsers := api.activeUsers
auditLogs := api.auditLogs
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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

Copy link
Member Author

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!

return nil, xerrors.Errorf("update entitlements: %w", err)
}
api.closeLicenseSubscribe, err = api.Pubsub.Subscribe(pubSubEventLicenses, func(ctx context.Context, message []byte) {
_ = api.updateEntitlements(ctx)
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Will fix.

select {
case <-ctx.Done():
return
case <-ticker.C:
Copy link
Contributor

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.

Copy link
Member Author

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.

@deansheather deansheather self-requested a review September 13, 2022 18:11
@coadler
Copy link
Contributor

coadler commented Sep 13, 2022

I think the layering here is easier to conceptualize for me. I like the changes 👍

@kylecarbs
Copy link
Member Author

kylecarbs commented Sep 13, 2022

@spikecurtis and I made some good progress discussing the current pitfalls of the approach:

  1. The /entitlements route has an ambiguous implementation point. Contributors could easily look at our AGPL coderd and be misled by the implementation without it being clear where the real implementation is.
  2. The entitlements HTTP handler and feature replacement code should be in a single, well-tested unit. Right now it's baked into the Enterprise coderd implementation which might be fine but should be at high test coverage like before.
  3. We should use a context.Context to get a feature interface like Auditor. It should be stored for the lifecycle of the context too, so that future retrievals return the same pointer. This prevents an implementation swap mid-way through a request, which would cause oddities.
  4. Calling Store() shouldn't be permitted inside the AGPL code. It's a privileged operation and could easily be misused.

Spike feel free to leave additional improvements if any come to mind!

Copy link
Collaborator

@sreya sreya left a 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
Comment on lines 484 to 485
RootHandler chi.Router
APIHandler chi.Router
Copy link
Collaborator

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.

@spikecurtis
Copy link
Contributor

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.

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.

@kylecarbs kylecarbs marked this pull request as ready for review September 19, 2022 19:25
@kylecarbs kylecarbs requested a review from a team as a code owner September 19, 2022 19:25
@kylecarbs kylecarbs requested review from Kira-Pilot and removed request for a team September 19, 2022 19:25
@kylecarbs
Copy link
Member Author

🥳 @spikecurtis if we find anything after merge that's questionable, I'm always happy to fix!

@kylecarbs kylecarbs force-pushed the entfactor branch 3 times, most recently from 7b6fe85 to fb35e01 Compare September 20, 2022 03:22
@kylecarbs kylecarbs merged commit db0ba85 into main Sep 20, 2022
@kylecarbs kylecarbs deleted the entfactor branch September 20, 2022 04:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants