Skip to content

Commit 1b8f6c5

Browse files
feat(compute-providers): add MicroVM webhook routing
1 parent bac71c6 commit 1b8f6c5

7 files changed

Lines changed: 183 additions & 19 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { expect, it, vi } from 'vitest';
2+
3+
import { provider as controlPlaneProvider } from './control-plane';
4+
import { provider as webhookProvider } from './webhook';
5+
6+
it('exposes every MicroVM compute provider capability from its compute-provider entry point', () => {
7+
const controlPlanePlugin = controlPlaneProvider.createPlugin(vi.fn(async () => []));
8+
const webhookPlugin = webhookProvider.createPlugin();
9+
10+
expect(controlPlanePlugin.type).toBe('microvm');
11+
expect(controlPlanePlugin.capabilities.pool()).toEqual({
12+
listRunners: expect.any(Function),
13+
countAvailableRunners: expect.any(Function),
14+
createRunners: expect.any(Function),
15+
});
16+
expect(controlPlanePlugin.capabilities.scaleUp()).toEqual({
17+
resolveLabelsForRunners: expect.any(Function),
18+
getCurrentRunners: expect.any(Function),
19+
createRunners: expect.any(Function),
20+
});
21+
expect(controlPlanePlugin.capabilities.scaleDown()).toEqual({
22+
list: expect.any(Function),
23+
bootTimeExceeded: expect.any(Function),
24+
markOrphan: expect.any(Function),
25+
unmarkOrphan: expect.any(Function),
26+
terminate: expect.any(Function),
27+
});
28+
expect(webhookPlugin.type).toBe('microvm');
29+
expect(webhookPlugin.capabilities.dynamicLabels.getViolations).toEqual(expect.any(Function));
30+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import type { RunnerMatcherConfig } from '../../../../contracts';
4+
import { microvmDynamicLabelProvider } from './dynamic-labels';
5+
6+
const imageArn = 'arn:aws:lambda:eu-west-1:123456789012:microvm-image:runner-large';
7+
const egressConnectorArn = 'arn:aws:lambda:eu-west-1:123456789012:network-connector:github-runner-private-egress';
8+
9+
describe('microvmDynamicLabelProvider', () => {
10+
it('accepts supported MicroVM overrides', () => {
11+
const queue = microvmQueue();
12+
const dynamicLabels = [
13+
`ghr-microvm-egress-network-connectors:${egressConnectorArn}`,
14+
`ghr-microvm-image-arn:${imageArn}`,
15+
'ghr-microvm-image-version:3.0',
16+
'ghr-microvm-maximum-duration-in-seconds:7200',
17+
];
18+
19+
expect(getViolations(queue, dynamicLabels)).toEqual([]);
20+
});
21+
22+
it('rejects unsupported MicroVM resource overrides', () => {
23+
expect(getViolations(microvmQueue(), ['ghr-microvm-memory:8192'])).toEqual([
24+
{
25+
label: 'ghr-microvm-memory:8192',
26+
reason: "key 'memory' is not a supported MicroVM override",
27+
},
28+
]);
29+
});
30+
31+
it('enforces the AWS dynamic-label policy', () => {
32+
const queue = microvmQueue();
33+
queue.matcherConfig.awsDynamicLabelsPolicy = {
34+
restricted_keys: { 'maximum-duration-in-seconds': { max: 3600 } },
35+
};
36+
37+
expect(getViolations(queue, ['ghr-microvm-maximum-duration-in-seconds:7200'])).toEqual([
38+
{
39+
label: 'ghr-microvm-maximum-duration-in-seconds:7200',
40+
reason: "value '7200' exceeds max '3600'",
41+
},
42+
]);
43+
});
44+
45+
it('applies allowed patterns to the complete image ARN', () => {
46+
const queue = microvmQueue();
47+
queue.matcherConfig.awsDynamicLabelsPolicy = {
48+
restricted_keys: {
49+
'image-arn': {
50+
allowed: ['arn:aws:lambda:eu-west-1:123456789012:microvm-image:approved-*'],
51+
},
52+
},
53+
};
54+
55+
expect(
56+
getViolations(queue, [
57+
'ghr-microvm-image-arn:arn:aws:lambda:eu-west-1:123456789012:microvm-image:approved-large',
58+
]),
59+
).toEqual([]);
60+
expect(
61+
getViolations(queue, ['ghr-microvm-image-arn:arn:aws:lambda:eu-west-1:123456789012:microvm-image:unapproved']),
62+
).toHaveLength(1);
63+
});
64+
65+
it('applies the policy to each egress connector label', () => {
66+
const queue = microvmQueue();
67+
queue.matcherConfig.awsDynamicLabelsPolicy = {
68+
restricted_keys: {
69+
'egress-network-connectors': {
70+
allowed: ['arn:aws:lambda:eu-west-1:123456789012:network-connector:approved-*'],
71+
},
72+
},
73+
};
74+
75+
expect(
76+
getViolations(queue, [
77+
'ghr-microvm-egress-network-connectors:arn:aws:lambda:eu-west-1:123456789012:network-connector:approved-private',
78+
]),
79+
).toEqual([]);
80+
expect(
81+
getViolations(queue, [
82+
'ghr-microvm-egress-network-connectors:arn:aws:lambda:eu-west-1:123456789012:network-connector:unapproved',
83+
]),
84+
).toHaveLength(1);
85+
});
86+
});
87+
88+
function getViolations(queue: RunnerMatcherConfig, labels: string[]) {
89+
return microvmDynamicLabelProvider.getViolations({ queue, labels });
90+
}
91+
92+
function microvmQueue(): RunnerMatcherConfig {
93+
return {
94+
id: 'microvm',
95+
arn: 'arn:aws:sqs:eu-west-1:123456789012:microvm',
96+
computeProvider: 'microvm',
97+
matcherConfig: {
98+
labelMatchers: [['self-hosted', 'linux', 'arm64', 'microvm']],
99+
exactMatch: false,
100+
enableDynamicLabels: true,
101+
},
102+
};
103+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { violationsAgainstAwsDynamicLabelsPolicy } from '../../../dynamic-labels-policy';
2+
import type { DynamicLabelProvider } from '../../../../contracts';
3+
import { MICROVM_DYNAMIC_LABEL_PREFIX, parseMicrovmDynamicLabels } from '../dynamic-labels';
4+
5+
export const microvmDynamicLabelProvider: DynamicLabelProvider = {
6+
getViolations: ({ queue, labels }) => {
7+
const parsedLabels = parseMicrovmDynamicLabels(labels);
8+
const policyViolations = violationsAgainstAwsDynamicLabelsPolicy(
9+
labels,
10+
queue.matcherConfig.awsDynamicLabelsPolicy,
11+
MICROVM_DYNAMIC_LABEL_PREFIX,
12+
);
13+
14+
return [...parsedLabels.violations, ...policyViolations];
15+
},
16+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { ComputeProviderPlugin } from '../../core';
2+
3+
import type { WebhookProviderCapabilities, WebhookProviderModule } from '../../contracts';
4+
import { microvmDynamicLabelProvider } from './src/webhook/dynamic-labels';
5+
6+
export function createMicrovmWebhookPlugin(): ComputeProviderPlugin<WebhookProviderCapabilities, 'microvm'> {
7+
return {
8+
type: 'microvm',
9+
capabilities: { dynamicLabels: microvmDynamicLabelProvider },
10+
};
11+
}
12+
13+
export const provider = {
14+
type: 'microvm',
15+
createPlugin: createMicrovmWebhookPlugin,
16+
} satisfies WebhookProviderModule<'microvm'>;

lambdas/libs/compute-providers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"./aws/ec2/control-plane": "./aws/ec2/control-plane.ts",
1313
"./aws/ec2/control-plane/runners": "./aws/ec2/src/control-plane/runners.ts",
1414
"./aws/ec2/control-plane/runner-config": "./aws/ec2/src/control-plane/runner-config.ts",
15+
"./aws/microvm/webhook": "./aws/microvm/webhook.ts",
1516
"./aws/microvm/control-plane": "./aws/microvm/control-plane.ts"
1617
},
1718
"type": "module",
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { provider as ec2 } from './aws/ec2/webhook';
2+
import { provider as microvm } from './aws/microvm/webhook';
23
import type { WebhookProviderModule } from './contracts';
34

45
/** Provider plugins included in the webhook bundle. */
5-
export const enabledWebhookProviders = [ec2] as const satisfies readonly WebhookProviderModule[];
6+
export const enabledWebhookProviders = [ec2, microvm] as const satisfies readonly WebhookProviderModule[];

lambdas/libs/compute-providers/webhook.test.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { describe, expect, it, vi } from 'vitest';
22

33
import type { DynamicLabelProvider, DynamicLabelViolation, RunnerMatcherConfig } from './contracts';
4-
import type { ComputeProviderType } from './provider-types';
54
import { createDynamicLabelQueueSelector } from './webhook';
65

6+
type TestProvider = 'provider-a' | 'provider-b';
7+
78
describe('createDynamicLabelQueueSelector', () => {
89
it('returns the first queue accepted by its provider', () => {
910
const queue = runnerQueue('accepted');
@@ -55,41 +56,37 @@ describe('createDynamicLabelQueueSelector', () => {
5556
expect(selectQueue([queue], ['self-hosted'], ['ghr-test-size:large'])).toBeUndefined();
5657
});
5758

58-
/* TODO: Re-enable this scenario when the MicroVM provider is added.
59-
it('skips EC2 and selects the MicroVM queue for MicroVM override labels', () => {
60-
const ec2Queue = runnerQueue('ec2');
61-
const microvmQueue = runnerQueue('microvm');
62-
const imageVersionLabel = 'ghr-microvm-image-version:3.0';
59+
it('skips queues when labels target another provider', () => {
60+
const firstQueue = runnerQueue('first');
61+
const secondQueue = runnerQueue('second');
6362
const { getViolations, selectQueue } = selector({
64-
providerByQueue: { ec2: 'ec2', microvm: 'microvm' },
65-
labelsForOtherProvider: (labels, provider) =>
66-
provider === 'ec2' ? labels.filter((label) => label.startsWith('ghr-microvm-')) : [],
63+
providerByQueue: { first: 'provider-a', second: 'provider-b' },
64+
labelsForOtherProvider: (_labels, provider) => (provider === 'provider-a' ? ['ghr-provider-b-size:large'] : []),
6765
});
6866

69-
expect(selectQueue([ec2Queue, microvmQueue], ['self-hosted', 'linux'], [imageVersionLabel])).toEqual({
70-
queue: microvmQueue,
71-
labels: ['self-hosted', 'linux', imageVersionLabel],
67+
expect(selectQueue([firstQueue, secondQueue], ['self-hosted'], ['ghr-provider-b-size:large'])).toEqual({
68+
queue: secondQueue,
69+
labels: ['self-hosted', 'ghr-provider-b-size:large'],
7270
});
7371
expect(getViolations).toHaveBeenCalledOnce();
74-
expect(getViolations).toHaveBeenCalledWith({ queue: microvmQueue, labels: [imageVersionLabel] });
72+
expect(getViolations).toHaveBeenCalledWith({ queue: secondQueue, labels: ['ghr-provider-b-size:large'] });
7573
});
76-
*/
7774
});
7875

7976
function selector(options?: {
80-
providerByQueue?: Record<string, ComputeProviderType>;
77+
providerByQueue?: Record<string, TestProvider>;
8178
violationsByQueue?: Record<string, DynamicLabelViolation[]>;
82-
labelsForOtherProvider?: (labels: string[], provider: ComputeProviderType) => string[];
79+
labelsForOtherProvider?: (labels: string[], provider: TestProvider) => string[];
8380
}) {
8481
const getViolations = vi.fn<DynamicLabelProvider['getViolations']>(({ queue }) => {
8582
return options?.violationsByQueue?.[queue.id] ?? [];
8683
});
8784

8885
return {
8986
getViolations,
90-
selectQueue: createDynamicLabelQueueSelector<ComputeProviderType>({
87+
selectQueue: createDynamicLabelQueueSelector<TestProvider>({
9188
resolveProvider: (queue) => ({
92-
type: options?.providerByQueue?.[queue.id] ?? 'ec2',
89+
type: options?.providerByQueue?.[queue.id] ?? 'provider-a',
9390
dynamicLabels: { getViolations },
9491
}),
9592
dynamicLabelsForOtherProvider: options?.labelsForOtherProvider ?? (() => []),

0 commit comments

Comments
 (0)