@appliance.sh/api-server 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/services/build.service.ts +104 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appliance.sh/api-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "Eliot Lim",
|
|
6
6
|
"repository": "https://github.com/appliance-sh/appliance.sh",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"test:e2e": "vitest run --config vitest.e2e.config.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@appliance.sh/infra": "1.
|
|
23
|
-
"@appliance.sh/sdk": "1.
|
|
22
|
+
"@appliance.sh/infra": "1.21.0",
|
|
23
|
+
"@appliance.sh/sdk": "1.21.0",
|
|
24
24
|
"@aws-sdk/client-ecr": "^3.1005.0",
|
|
25
25
|
"@aws-sdk/client-s3": "^3.750.0",
|
|
26
26
|
"express": "^5.2.1"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
1
|
+
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
2
2
|
import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr';
|
|
3
3
|
import { applianceBaseConfig, applianceInput } from '@appliance.sh/sdk';
|
|
4
|
-
import type { ApplianceContainer } from '@appliance.sh/sdk';
|
|
4
|
+
import type { ApplianceContainer, ApplianceFrameworkApp } from '@appliance.sh/sdk';
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
6
|
import * as fs from 'node:fs';
|
|
7
7
|
import * as path from 'node:path';
|
|
@@ -10,6 +10,11 @@ import * as os from 'node:os';
|
|
|
10
10
|
export interface ResolvedBuild {
|
|
11
11
|
imageUri?: string;
|
|
12
12
|
codeS3Key?: string;
|
|
13
|
+
runtime?: string;
|
|
14
|
+
handler?: string;
|
|
15
|
+
layers?: string[];
|
|
16
|
+
architectures?: string[];
|
|
17
|
+
environment?: Record<string, string>;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
function getBaseConfig() {
|
|
@@ -18,6 +23,23 @@ function getBaseConfig() {
|
|
|
18
23
|
return applianceBaseConfig.parse(JSON.parse(raw));
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
const LAMBDA_ADAPTER_LAYER: Record<string, string> = {
|
|
27
|
+
'linux/amd64': 'arn:aws:lambda:${region}:753240598075:layer:LambdaAdapterLayerX86:26',
|
|
28
|
+
'linux/arm64': 'arn:aws:lambda:${region}:753240598075:layer:LambdaAdapterLayerArm64:26',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const FRAMEWORK_RUNTIMES: Record<string, string> = {
|
|
32
|
+
node: 'nodejs22.x',
|
|
33
|
+
python: 'python3.13',
|
|
34
|
+
auto: 'nodejs22.x',
|
|
35
|
+
other: 'nodejs22.x',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const FRAMEWORK_ARCHITECTURES: Record<string, string> = {
|
|
39
|
+
'linux/amd64': 'x86_64',
|
|
40
|
+
'linux/arm64': 'arm64',
|
|
41
|
+
};
|
|
42
|
+
|
|
21
43
|
export class BuildService {
|
|
22
44
|
async resolve(buildId: string, tag: string): Promise<ResolvedBuild> {
|
|
23
45
|
const config = getBaseConfig();
|
|
@@ -55,8 +77,9 @@ export class BuildService {
|
|
|
55
77
|
|
|
56
78
|
if (manifest.type === 'container') {
|
|
57
79
|
return this.resolveContainer(tmpDir, manifest, tag, config);
|
|
80
|
+
} else if (manifest.type === 'framework') {
|
|
81
|
+
return this.resolveFramework(tmpDir, manifest, tag, config);
|
|
58
82
|
} else {
|
|
59
|
-
// For framework/other, the zip is already in S3 — just reference it
|
|
60
83
|
return { codeS3Key: s3Key };
|
|
61
84
|
}
|
|
62
85
|
} finally {
|
|
@@ -64,6 +87,84 @@ export class BuildService {
|
|
|
64
87
|
}
|
|
65
88
|
}
|
|
66
89
|
|
|
90
|
+
private async resolveFramework(
|
|
91
|
+
tmpDir: string,
|
|
92
|
+
manifest: ApplianceFrameworkApp,
|
|
93
|
+
tag: string,
|
|
94
|
+
config: ReturnType<typeof getBaseConfig>
|
|
95
|
+
): Promise<ResolvedBuild> {
|
|
96
|
+
if (!config.aws.dataBucketName) throw new Error('Data bucket not configured');
|
|
97
|
+
|
|
98
|
+
const port = manifest.port ?? 8080;
|
|
99
|
+
const framework = manifest.framework ?? 'auto';
|
|
100
|
+
const detectedFramework = framework === 'auto' ? this.detectFramework(tmpDir) : framework;
|
|
101
|
+
const runtime = FRAMEWORK_RUNTIMES[detectedFramework] ?? FRAMEWORK_RUNTIMES['node'];
|
|
102
|
+
|
|
103
|
+
// Generate a run.sh that starts the web server
|
|
104
|
+
const startCommand = manifest.scripts?.start ?? this.defaultStartCommand(detectedFramework, tmpDir);
|
|
105
|
+
const runSh = ['#!/bin/bash', `export PORT=${port}`, `exec ${startCommand}`].join('\n');
|
|
106
|
+
fs.writeFileSync(path.join(tmpDir, 'run.sh'), runSh, { mode: 0o755 });
|
|
107
|
+
|
|
108
|
+
// Repackage as a new zip (excluding the original zip and manifest-only artifacts)
|
|
109
|
+
const repackagedKey = `builds/${tag}.zip`;
|
|
110
|
+
const repackagedZip = path.join(tmpDir, 'repackaged.zip');
|
|
111
|
+
execSync(`cd "${tmpDir}" && zip -r "${repackagedZip}" . -x "appliance.zip" -x "repackaged.zip"`, { stdio: 'pipe' });
|
|
112
|
+
|
|
113
|
+
// Upload the repackaged zip
|
|
114
|
+
const s3 = new S3Client({ region: config.aws.region });
|
|
115
|
+
await s3.send(
|
|
116
|
+
new PutObjectCommand({
|
|
117
|
+
Bucket: config.aws.dataBucketName,
|
|
118
|
+
Key: repackagedKey,
|
|
119
|
+
Body: fs.readFileSync(repackagedZip),
|
|
120
|
+
ContentType: 'application/zip',
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Resolve the Lambda Web Adapter layer ARN and architecture for this region
|
|
125
|
+
const platform = manifest.platform;
|
|
126
|
+
const layerArn = LAMBDA_ADAPTER_LAYER[platform]?.replace('${region}', config.aws.region);
|
|
127
|
+
if (!layerArn) throw new Error(`No Lambda Web Adapter layer for platform: ${platform}`);
|
|
128
|
+
const architecture = FRAMEWORK_ARCHITECTURES[platform];
|
|
129
|
+
if (!architecture) throw new Error(`No Lambda architecture for platform: ${platform}`);
|
|
130
|
+
|
|
131
|
+
// The Web Adapter layer uses AWS_LAMBDA_EXEC_WRAPPER to intercept the
|
|
132
|
+
// Lambda runtime bootstrap. It starts run.sh (the web server) and proxies
|
|
133
|
+
// Lambda invocations to it as HTTP requests. The handler value is not
|
|
134
|
+
// called directly but must point to a valid file to pass validation.
|
|
135
|
+
return {
|
|
136
|
+
codeS3Key: repackagedKey,
|
|
137
|
+
runtime,
|
|
138
|
+
handler: 'run.sh',
|
|
139
|
+
layers: [layerArn],
|
|
140
|
+
architectures: [architecture],
|
|
141
|
+
environment: {
|
|
142
|
+
AWS_LAMBDA_EXEC_WRAPPER: '/opt/bootstrap',
|
|
143
|
+
AWS_LWA_PORT: String(port),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private detectFramework(tmpDir: string): string {
|
|
149
|
+
if (fs.existsSync(path.join(tmpDir, 'package.json'))) return 'node';
|
|
150
|
+
if (fs.existsSync(path.join(tmpDir, 'requirements.txt'))) return 'python';
|
|
151
|
+
if (fs.existsSync(path.join(tmpDir, 'Pipfile'))) return 'python';
|
|
152
|
+
if (fs.existsSync(path.join(tmpDir, 'pyproject.toml'))) return 'python';
|
|
153
|
+
return 'node';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private defaultStartCommand(framework: string, tmpDir: string): string {
|
|
157
|
+
if (framework === 'python') {
|
|
158
|
+
return 'python app.py';
|
|
159
|
+
}
|
|
160
|
+
// Node default
|
|
161
|
+
if (fs.existsSync(path.join(tmpDir, 'package.json'))) {
|
|
162
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8'));
|
|
163
|
+
if (pkg.scripts?.start) return 'npm start';
|
|
164
|
+
}
|
|
165
|
+
return 'node index.js';
|
|
166
|
+
}
|
|
167
|
+
|
|
67
168
|
private async resolveContainer(
|
|
68
169
|
tmpDir: string,
|
|
69
170
|
manifest: ApplianceContainer,
|