1- import {
2- type GrantedLicense ,
3- LicenseManagerClient ,
4- ListReceivedLicensesCommand ,
5- } from '@aws-sdk/client-license-manager'
1+ import { CheckoutLicenseCommand , LicenseManagerClient } from '@aws-sdk/client-license-manager'
62import { createLogger } from '@sim/logger'
73import { getErrorMessage } from '@sim/utils/errors'
4+ import { generateId } from '@sim/utils/id'
85import { randomInt } from '@sim/utils/random'
96import { awsRequestHandler } from '@/lib/core/network/proxy-agent.server'
107
118/**
129 * Enterprise entitlement for Sim installed from the AWS Marketplace container listing.
1310 *
1411 * The listing sells an annual contract whose one dimension is `enterprise_license`. AWS Marketplace
15- * grants the buyer a license in AWS License Manager, and the running product reads it. While the
16- * license is active, the Enterprise suite is on (`ENTERPRISE_ENABLED`). Without one, Sim runs as the
17- * open-source core, so a lapsed renewal or an unreachable License Manager never locks a buyer out
18- * of their deployment.
19- *
20- * The check reads the license rather than checking a unit out of it. A contract usually covers the
21- * whole deployment with one unit, so checking it out from every replica would make them compete
22- * for it, and a unit left checked out (a failed check-in holds it for an hour) would read exactly
23- * like an expired license.
12+ * grants the buyer a license in AWS License Manager, and the running product checks it the way AWS
13+ * documents for a contract that unlocks a tier of features rather than metering a quantity: a
14+ * provisional `CheckoutLicense` of the entitlement with unit `None`. Such a checkout is not counted
15+ * against the contract, so every replica can check at once, and it needs only the permission AWS's
16+ * managed `AWSLicenseManagerConsumptionPolicy` grants. While the license is active, the Enterprise
17+ * suite is on (`ENTERPRISE_ENABLED`). Without one, Sim runs as the open-source core, so a lapsed
18+ * renewal or an unreachable License Manager never locks a buyer out of their deployment.
2419 *
2520 * Opt-in: nothing here runs unless {@link PRODUCT_SKU_ENV} is set, which only the Marketplace
2621 * Helm profile does. Enterprise flags are read once at startup by both the server and the
@@ -34,8 +29,8 @@ const logger = createLogger('AwsMarketplaceContainerLicense')
3429const PRODUCT_SKU_ENV = 'AWS_MARKETPLACE_LICENSE_PRODUCT_SKU'
3530
3631/**
37- * Optional override for where the license is read . License Manager replicates a granted license to
38- * other Regions, so the default is the pod's own Region, the same chain AWS's EKS walkthrough uses.
32+ * Optional override for where the license is checked . License Manager replicates a granted license
33+ * to other Regions, so the default is the pod's own Region, the same chain AWS's EKS walkthrough uses.
3934 */
4035const LICENSE_REGION_ENV = 'AWS_MARKETPLACE_LICENSE_REGION'
4136
@@ -48,21 +43,26 @@ const ENTERPRISE_ENTITLEMENT = 'enterprise_license'
4843/** AWS recommends re-checking every 15 minutes to pick up renewals, upgrades, and cancellations. */
4944const RECHECK_INTERVAL_MS = 15 * 60 * 1000
5045
51- /**
52- * Replicas start together, so their re-checks line up; each waits a random part of this before
53- * restarting, so a renewal or lapse never takes every replica down at once.
54- */
55- const RESTART_SPREAD_MS = 10 * 60 * 1000
46+ /** Replicas start together; a random part of this added to each re-check keeps them apart. */
47+ const RECHECK_JITTER_MS = 2 * 60 * 1000
5648
5749/**
58- * How the buyer's account holds the license. A delegate grant only shows licenses distributed to
59- * other accounts, and a grant still awaiting acceptance, or rejected, disabled, or deleted by the
60- * license administrator, entitles nothing. No status at all is a license the account bought itself.
50+ * Replicas see a change together; each waits a random part of this before restarting, so a renewal
51+ * or lapse never takes every replica down at once.
6152 */
62- const USABLE_RECEIVED_STATUSES = new Set ( [ 'ACTIVE' , 'WORKFLOW_COMPLETED' ] )
53+ const RESTART_SPREAD_MS = 10 * 60 * 1000
6354
6455const ENTERPRISE_FLAGS = [ 'ENTERPRISE_ENABLED' , 'NEXT_PUBLIC_ENTERPRISE_ENABLED' ] as const
6556
57+ /** License Manager's answers when the account holds no active license with the entitlement. */
58+ const NO_ENTITLEMENT_ERRORS = new Set ( [
59+ 'NoEntitlementsAllowedException' ,
60+ 'ResourceNotFoundException' ,
61+ ] )
62+
63+ /** The ServiceAccount's IAM role does not allow the check: a setup problem, never a verdict. */
64+ const PERMISSION_ERRORS = new Set ( [ 'AccessDeniedException' , 'AuthorizationException' ] )
65+
6666/** `unknown` is a failed check (network, permissions, throttling), never a verdict on the license. */
6767type LicenseState = 'entitled' | 'not-entitled' | 'unknown'
6868
@@ -78,58 +78,62 @@ function createClient(): LicenseManagerClient {
7878 } )
7979}
8080
81- function isUsableGrant ( { ReceivedMetadata : received } : GrantedLicense ) : boolean {
82- if ( received ?. GrantType === 'DelegateGrant' ) return false
83- return ! received ?. ReceivedStatus || USABLE_RECEIVED_STATUSES . has ( received . ReceivedStatus )
84- }
85-
86- /**
87- * An AWS Marketplace license this product's buyer holds is current when License Manager reports it
88- * `AVAILABLE`, its issuer is AWS Marketplace, it carries the Enterprise entitlement, and the account
89- * holds it through a grant it may use. License Manager moves an ended, cancelled, or suspended
90- * license out of `AVAILABLE` itself.
91- */
92- function isActiveEnterpriseLicense ( license : GrantedLicense ) : boolean {
93- return (
94- license . Status === 'AVAILABLE' &&
95- isUsableGrant ( license ) &&
96- license . Issuer ?. KeyFingerprint === MARKETPLACE_KEY_FINGERPRINT &&
97- ( license . Entitlements ?? [ ] ) . some ( ( entitlement ) => entitlement . Name === ENTERPRISE_ENTITLEMENT )
98- )
81+ function classifyFailure ( error : unknown ) : LicenseState {
82+ const name = error instanceof Error ? error . name : undefined
83+ if ( name && NO_ENTITLEMENT_ERRORS . has ( name ) ) {
84+ logger . info ( 'AWS License Manager reports no active Enterprise entitlement' , { error : name } )
85+ return 'not-entitled'
86+ }
87+ if ( name && PERMISSION_ERRORS . has ( name ) ) {
88+ logger . error (
89+ 'The IAM role cannot check the AWS Marketplace license; allow license-manager:CheckoutLicense' ,
90+ { error : name }
91+ )
92+ return 'unknown'
93+ }
94+ logger . warn ( 'Could not check the AWS Marketplace Enterprise license' , {
95+ error : name ,
96+ message : getErrorMessage ( error , 'Unknown error' ) ,
97+ } )
98+ return 'unknown'
9999}
100100
101101async function checkLicense (
102102 client : LicenseManagerClient ,
103103 productSku : string
104104) : Promise < LicenseState > {
105105 try {
106- let nextToken : string | undefined
107- do {
108- const page = await client . send (
109- new ListReceivedLicensesCommand ( {
110- Filters : [
111- { Name : 'ProductSKU' , Values : [ productSku ] } ,
112- { Name : 'Fingerprint' , Values : [ MARKETPLACE_KEY_FINGERPRINT ] } ,
113- ] ,
114- NextToken : nextToken ,
115- } )
116- )
117- if ( ( page . Licenses ?? [ ] ) . some ( isActiveEnterpriseLicense ) ) return 'entitled'
118- nextToken = page . NextToken
119- } while ( nextToken )
120- return 'not-entitled'
106+ const checkout = await client . send (
107+ new CheckoutLicenseCommand ( {
108+ ProductSKU : productSku ,
109+ CheckoutType : 'PROVISIONAL' ,
110+ KeyFingerprint : MARKETPLACE_KEY_FINGERPRINT ,
111+ Entitlements : [ { Name : ENTERPRISE_ENTITLEMENT , Unit : 'None' } ] ,
112+ ClientToken : generateId ( ) ,
113+ } )
114+ )
115+ const allowed = ( checkout . EntitlementsAllowed ?? [ ] ) . some (
116+ ( entitlement ) => entitlement . Name === ENTERPRISE_ENTITLEMENT
117+ )
118+ return allowed ? 'entitled' : 'not-entitled'
121119 } catch ( error ) {
122- logger . warn ( 'Could not check the AWS Marketplace Enterprise license' , {
123- message : getErrorMessage ( error , 'Unknown error' ) ,
124- } )
125- return 'unknown'
120+ return classifyFailure ( error )
126121 }
127122}
128123
129124function setEnterpriseFlags ( enabled : boolean ) : void {
130125 for ( const flag of ENTERPRISE_FLAGS ) process . env [ flag ] = enabled ? 'true' : 'false'
131126}
132127
128+ function restartToApply ( state : LicenseState ) : void {
129+ const delayMs = randomInt ( 0 , RESTART_SPREAD_MS )
130+ logger . info ( 'AWS Marketplace Enterprise license changed; restarting to apply it' , {
131+ state,
132+ delayMs,
133+ } )
134+ setTimeout ( ( ) => process . kill ( process . pid , 'SIGTERM' ) , delayMs )
135+ }
136+
133137/**
134138 * Resolves the Enterprise license before the server loads, so every Enterprise gate reads the
135139 * answer through `ENTERPRISE_ENABLED`, then keeps re-checking. Returns without doing anything on a
@@ -150,17 +154,16 @@ export async function applyAwsMarketplaceContainerLicense(): Promise<void> {
150154 { state : bootState }
151155 )
152156
153- const recheck = setInterval ( async ( ) => {
157+ const scheduleRecheck = ( ) => {
158+ setTimeout ( recheck , RECHECK_INTERVAL_MS + randomInt ( 0 , RECHECK_JITTER_MS ) ) . unref ( )
159+ }
160+ const recheck = async ( ) => {
154161 const state = await checkLicense ( client , productSku )
155- const entitledNow = state === 'entitled'
156- if ( state === 'unknown' || entitledNow === enterpriseAtBoot ) return
157- clearInterval ( recheck )
158- const delayMs = randomInt ( 0 , RESTART_SPREAD_MS )
159- logger . info ( 'AWS Marketplace Enterprise license changed; restarting to apply it' , {
160- state,
161- delayMs,
162- } )
163- setTimeout ( ( ) => process . kill ( process . pid , 'SIGTERM' ) , delayMs )
164- } , RECHECK_INTERVAL_MS )
165- recheck . unref ( )
162+ // A failed check keeps the current mode; only a definite answer that differs restarts.
163+ const changed =
164+ ( state === 'entitled' && ! enterpriseAtBoot ) || ( state === 'not-entitled' && enterpriseAtBoot )
165+ if ( changed ) return restartToApply ( state )
166+ scheduleRecheck ( )
167+ }
168+ scheduleRecheck ( )
166169}
0 commit comments