@@ -15,6 +15,7 @@ import (
1515"github.com/coder/coder/v2/buildinfo"
1616"github.com/coder/coder/v2/coderd/appearance"
1717"github.com/coder/coder/v2/coderd/database"
18+ "github.com/coder/coder/v2/coderd/entitlements"
1819agplportsharing"github.com/coder/coder/v2/coderd/portsharing"
1920"github.com/coder/coder/v2/coderd/rbac/policy"
2021"github.com/coder/coder/v2/enterprise/coderd/portsharing"
@@ -103,19 +104,26 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
103104}
104105return nil ,xerrors .Errorf ("init database encryption: %w" ,err )
105106}
107+
108+ entitlementsSet := entitlements .New ()
106109options .Database = cryptDB
107110api := & API {
108- ctx :ctx ,
109- cancel :cancelFunc ,
110- Options :options ,
111+ ctx :ctx ,
112+ cancel :cancelFunc ,
113+ Options :options ,
114+ entitlements :entitlementsSet ,
111115provisionerDaemonAuth :& provisionerDaemonAuth {
112116psk :options .ProvisionerDaemonPSK ,
113117authorizer :options .Authorizer ,
114118db :options .Database ,
115119},
120+ licenseMetricsCollector :& license.MetricsCollector {
121+ Entitlements :entitlementsSet ,
122+ },
116123}
117124// This must happen before coderd initialization!
118125options .PostAuthAdditionalHeadersFunc = api .writeEntitlementWarningsHeader
126+ options .Options .Entitlements = api .entitlements
119127api .AGPL = coderd .New (options .Options )
120128defer func () {
121129if err != nil {
@@ -493,7 +501,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
493501}
494502api .AGPL .WorkspaceProxiesFetchUpdater .Store (& fetchUpdater )
495503
496- err = api .PrometheusRegistry .Register (& api .licenseMetricsCollector )
504+ err = api .PrometheusRegistry .Register (api .licenseMetricsCollector )
497505if err != nil {
498506return nil ,xerrors .Errorf ("unable to register license metrics collector" )
499507}
@@ -553,13 +561,11 @@ type API struct {
553561// ProxyHealth checks the reachability of all workspace proxies.
554562ProxyHealth * proxyhealth.ProxyHealth
555563
556- entitlementsUpdateMu sync.Mutex
557- entitlementsMu sync.RWMutex
558- entitlements codersdk.Entitlements
564+ entitlements * entitlements.Set
559565
560566provisionerDaemonAuth * provisionerDaemonAuth
561567
562- licenseMetricsCollector license.MetricsCollector
568+ licenseMetricsCollector * license.MetricsCollector
563569tailnetService * tailnet.ClientService
564570}
565571
@@ -588,11 +594,8 @@ func (api *API) writeEntitlementWarningsHeader(a rbac.Subject, header http.Heade
588594// has no roles. This is a normal user!
589595return
590596}
591- api .entitlementsMu .RLock ()
592- defer api .entitlementsMu .RUnlock ()
593- for _ ,warning := range api .entitlements .Warnings {
594- header .Add (codersdk .EntitlementsWarningHeader ,warning )
595- }
597+
598+ api .entitlements .WriteEntitlementWarningHeaders (header )
596599}
597600
598601func (api * API )Close ()error {
@@ -614,9 +617,6 @@ func (api *API) Close() error {
614617}
615618
616619func (api * API )updateEntitlements (ctx context.Context )error {
617- api .entitlementsUpdateMu .Lock ()
618- defer api .entitlementsUpdateMu .Unlock ()
619-
620620replicas := api .replicaManager .AllPrimary ()
621621agedReplicas := make ([]database.Replica ,0 ,len (replicas ))
622622for _ ,replica := range replicas {
@@ -632,7 +632,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
632632agedReplicas = append (agedReplicas ,replica )
633633}
634634
635- entitlements ,err := license .Entitlements (
635+ reloadedEntitlements ,err := license .Entitlements (
636636ctx ,api .Database ,
637637len (agedReplicas ),len (api .ExternalAuthConfigs ),api .LicenseKeys ,map [codersdk.FeatureName ]bool {
638638codersdk .FeatureAuditLog :api .AuditLogging ,
@@ -652,29 +652,24 @@ func (api *API) updateEntitlements(ctx context.Context) error {
652652return err
653653}
654654
655- if entitlements .RequireTelemetry && ! api .DeploymentValues .Telemetry .Enable .Value () {
655+ if reloadedEntitlements .RequireTelemetry && ! api .DeploymentValues .Telemetry .Enable .Value () {
656656// We can't fail because then the user couldn't remove the offending
657657// license w/o a restart.
658658//
659659// We don't simply append to entitlement.Errors since we don't want any
660660// enterprise features enabled.
661- api .entitlements .Errors = []string {
662- "License requires telemetry but telemetry is disabled" ,
663- }
661+ api .entitlements .Update (func (entitlements * codersdk.Entitlements ) {
662+ entitlements .Errors = []string {
663+ "License requires telemetry but telemetry is disabled" ,
664+ }
665+ })
666+
664667api .Logger .Error (ctx ,"license requires telemetry enabled" )
665668return nil
666669}
667670
668671featureChanged := func (featureName codersdk.FeatureName ) (initial ,changed ,enabled bool ) {
669- if api .entitlements .Features == nil {
670- return true ,false ,entitlements .Features [featureName ].Enabled
671- }
672- oldFeature := api .entitlements .Features [featureName ]
673- newFeature := entitlements .Features [featureName ]
674- if oldFeature .Enabled != newFeature .Enabled {
675- return false ,true ,newFeature .Enabled
676- }
677- return false ,false ,newFeature .Enabled
672+ return api .entitlements .FeatureChanged (featureName ,reloadedEntitlements .Features [featureName ])
678673}
679674
680675shouldUpdate := func (initial ,changed ,enabled bool )bool {
@@ -831,20 +826,16 @@ func (api *API) updateEntitlements(ctx context.Context) error {
831826}
832827
833828// External token encryption is soft-enforced
834- featureExternalTokenEncryption := entitlements .Features [codersdk .FeatureExternalTokenEncryption ]
829+ featureExternalTokenEncryption := reloadedEntitlements .Features [codersdk .FeatureExternalTokenEncryption ]
835830featureExternalTokenEncryption .Enabled = len (api .ExternalTokenEncryption )> 0
836831if featureExternalTokenEncryption .Enabled && featureExternalTokenEncryption .Entitlement != codersdk .EntitlementEntitled {
837832msg := fmt .Sprintf ("%s is enabled (due to setting external token encryption keys) but your license is not entitled to this feature." ,codersdk .FeatureExternalTokenEncryption .Humanize ())
838833api .Logger .Warn (ctx ,msg )
839- entitlements .Warnings = append (entitlements .Warnings ,msg )
834+ reloadedEntitlements .Warnings = append (reloadedEntitlements .Warnings ,msg )
840835}
841- entitlements .Features [codersdk .FeatureExternalTokenEncryption ]= featureExternalTokenEncryption
836+ reloadedEntitlements .Features [codersdk .FeatureExternalTokenEncryption ]= featureExternalTokenEncryption
842837
843- api .entitlementsMu .Lock ()
844- defer api .entitlementsMu .Unlock ()
845- api .entitlements = entitlements
846- api .licenseMetricsCollector .Entitlements .Store (& entitlements )
847- api .AGPL .SiteHandler .Entitlements .Store (& entitlements )
838+ api .entitlements .Replace (reloadedEntitlements )
848839return nil
849840}
850841
@@ -1024,10 +1015,7 @@ func derpMapper(logger slog.Logger, proxyHealth *proxyhealth.ProxyHealth) func(*
10241015// @Router /entitlements [get]
10251016func (api * API )serveEntitlements (rw http.ResponseWriter ,r * http.Request ) {
10261017ctx := r .Context ()
1027- api .entitlementsMu .RLock ()
1028- entitlements := api .entitlements
1029- api .entitlementsMu .RUnlock ()
1030- httpapi .Write (ctx ,rw ,http .StatusOK ,entitlements )
1018+ httpapi .Write (ctx ,rw ,http .StatusOK ,api .entitlements .AsJSON ())
10311019}
10321020
10331021func (api * API )runEntitlementsLoop (ctx context.Context ) {