Skip to content

Commit e5fa7c0

Browse files
committed
fix(aws-marketplace): check the container license with CheckoutLicense so AWS's standard buyer policy works
A buyer who follows AWS's generated launch steps gets the managed AWSLicenseManagerConsumptionPolicy, which does not allow ListReceivedLicenses, so the check failed and Sim stayed on the open-source core. Check out a provisional unit of the entitlement and return it at once, the integration AWS documents for contract-priced containers. License Manager answers a unit held by another replica, or one a crashed replica never returned, the same way as a missing entitlement. Startup retries that answer, and a running Enterprise deployment only restarts without Enterprise once it has held longer than an unreturned unit can last.
1 parent d8d7c8c commit e5fa7c0

4 files changed

Lines changed: 287 additions & 74 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
const { mockSend } = vi.hoisted(() => ({ mockSend: vi.fn() }))
4+
5+
vi.mock('@aws-sdk/client-license-manager', () => ({
6+
LicenseManagerClient: class {
7+
send = mockSend
8+
},
9+
CheckoutLicenseCommand: class {
10+
constructor(public input: { ProductSKU: string }) {}
11+
},
12+
CheckInLicenseCommand: class {
13+
constructor(public input: { LicenseConsumptionToken: string }) {}
14+
},
15+
}))
16+
vi.mock('@/lib/core/network/proxy-agent.server', () => ({ awsRequestHandler: () => undefined }))
17+
vi.mock('@sim/utils/random', () => ({ randomInt: (min: number) => min }))
18+
19+
import { CheckInLicenseCommand } from '@aws-sdk/client-license-manager'
20+
import { applyAwsMarketplaceContainerLicense } from '@/lib/billing/aws-marketplace/container-license'
21+
22+
const MINUTE = 60 * 1000
23+
const TOKEN = 'consumption-token-placeholder'
24+
25+
type Answer = 'entitled' | 'NoEntitlementsAllowedException' | 'AccessDeniedException'
26+
27+
/** License Manager's checkout answers in order; the last one repeats once the list runs out. */
28+
let answers: Answer[] = []
29+
let checkInFails = false
30+
const checkIns: string[] = []
31+
32+
function awsError(name: string): Error {
33+
return Object.assign(new Error(name), { name })
34+
}
35+
36+
function enterpriseEnabled(): boolean {
37+
return process.env.ENTERPRISE_ENABLED === 'true'
38+
}
39+
40+
async function boot(): Promise<void> {
41+
const started = applyAwsMarketplaceContainerLicense()
42+
await vi.advanceTimersByTimeAsync(30_000)
43+
await started
44+
}
45+
46+
describe('applyAwsMarketplaceContainerLicense', () => {
47+
let kill: ReturnType<typeof vi.spyOn>
48+
49+
beforeEach(() => {
50+
vi.useFakeTimers({ toFake: ['setTimeout', 'Date'] })
51+
answers = []
52+
checkInFails = false
53+
checkIns.length = 0
54+
vi.stubEnv('AWS_MARKETPLACE_LICENSE_PRODUCT_SKU', 'product-placeholder')
55+
kill = vi.spyOn(process, 'kill').mockImplementation(() => true)
56+
mockSend.mockReset()
57+
mockSend.mockImplementation(async (command: unknown) => {
58+
if (command instanceof CheckInLicenseCommand) {
59+
checkIns.push(command.input.LicenseConsumptionToken)
60+
if (checkInFails) throw awsError('ServerInternalException')
61+
return {}
62+
}
63+
const answer = answers.length > 1 ? answers.shift() : answers[0]
64+
if (answer !== 'entitled') throw awsError(answer ?? 'ServerInternalException')
65+
return {
66+
LicenseConsumptionToken: TOKEN,
67+
EntitlementsAllowed: [{ Name: 'enterprise_license', Unit: 'Count', Value: '1' }],
68+
}
69+
})
70+
})
71+
72+
afterEach(() => {
73+
vi.clearAllTimers()
74+
vi.useRealTimers()
75+
kill.mockRestore()
76+
vi.unstubAllEnvs()
77+
Reflect.deleteProperty(process.env, 'ENTERPRISE_ENABLED')
78+
Reflect.deleteProperty(process.env, 'NEXT_PUBLIC_ENTERPRISE_ENABLED')
79+
})
80+
81+
it('does nothing outside a Marketplace install', async () => {
82+
vi.stubEnv('AWS_MARKETPLACE_LICENSE_PRODUCT_SKU', undefined)
83+
await boot()
84+
expect(mockSend).not.toHaveBeenCalled()
85+
})
86+
87+
it('turns Enterprise on and returns the unit it checked out', async () => {
88+
answers = ['entitled']
89+
await boot()
90+
expect(enterpriseEnabled()).toBe(true)
91+
expect(checkIns).toEqual([TOKEN])
92+
})
93+
94+
it('keeps an active license when returning the unit fails', async () => {
95+
answers = ['entitled']
96+
checkInFails = true
97+
await boot()
98+
expect(enterpriseEnabled()).toBe(true)
99+
})
100+
101+
it('retries a unit another replica is holding at startup', async () => {
102+
answers = ['NoEntitlementsAllowedException', 'entitled']
103+
await boot()
104+
expect(enterpriseEnabled()).toBe(true)
105+
})
106+
107+
it('runs without Enterprise when no unit is allowed at startup', async () => {
108+
answers = ['NoEntitlementsAllowedException']
109+
await boot()
110+
expect(enterpriseEnabled()).toBe(false)
111+
})
112+
113+
it('runs without Enterprise when its IAM role cannot check out', async () => {
114+
answers = ['AccessDeniedException']
115+
await boot()
116+
expect(enterpriseEnabled()).toBe(false)
117+
})
118+
119+
it('keeps Enterprise on through a unit held elsewhere for up to an hour', async () => {
120+
answers = ['entitled', ...Array<Answer>(5).fill('NoEntitlementsAllowedException'), 'entitled']
121+
await boot()
122+
await vi.advanceTimersByTimeAsync(180 * MINUTE)
123+
expect(kill).not.toHaveBeenCalled()
124+
})
125+
126+
it('restarts without Enterprise once no entitlement outlasts any held unit', async () => {
127+
answers = ['entitled', 'NoEntitlementsAllowedException']
128+
await boot()
129+
await vi.advanceTimersByTimeAsync(85 * MINUTE)
130+
expect(kill).not.toHaveBeenCalled()
131+
await vi.advanceTimersByTimeAsync(10 * MINUTE)
132+
expect(kill).toHaveBeenCalledWith(process.pid, 'SIGTERM')
133+
})
134+
135+
it('never restarts a running Enterprise deployment over a permission failure', async () => {
136+
answers = ['entitled', 'AccessDeniedException']
137+
await boot()
138+
await vi.advanceTimersByTimeAsync(6 * 60 * MINUTE)
139+
expect(kill).not.toHaveBeenCalled()
140+
})
141+
142+
it('restarts into Enterprise once the license becomes active', async () => {
143+
answers = [...Array<Answer>(3).fill('NoEntitlementsAllowedException'), 'entitled']
144+
await boot()
145+
expect(enterpriseEnabled()).toBe(false)
146+
await vi.advanceTimersByTimeAsync(16 * MINUTE)
147+
expect(kill).toHaveBeenCalledWith(process.pid, 'SIGTERM')
148+
})
149+
})

0 commit comments

Comments
 (0)