@appliance.sh/api-server 1.21.0 → 1.22.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 +21 -81
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appliance.sh/api-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.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.22.0",
|
|
23
|
+
"@appliance.sh/sdk": "1.22.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,4 +1,4 @@
|
|
|
1
|
-
import { S3Client, GetObjectCommand
|
|
1
|
+
import { S3Client, GetObjectCommand } 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
4
|
import type { ApplianceContainer, ApplianceFrameworkApp } from '@appliance.sh/sdk';
|
|
@@ -78,7 +78,7 @@ export class BuildService {
|
|
|
78
78
|
if (manifest.type === 'container') {
|
|
79
79
|
return this.resolveContainer(tmpDir, manifest, tag, config);
|
|
80
80
|
} else if (manifest.type === 'framework') {
|
|
81
|
-
return this.resolveFramework(
|
|
81
|
+
return this.resolveFramework(manifest, s3Key, config);
|
|
82
82
|
} else {
|
|
83
83
|
return { codeS3Key: s3Key };
|
|
84
84
|
}
|
|
@@ -87,53 +87,28 @@ export class BuildService {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Framework builds are fully pre-processed by the CLI (dependencies installed,
|
|
92
|
+
* run.sh generated). The server just resolves Lambda-specific params from the
|
|
93
|
+
* manifest metadata and points at the original uploaded zip.
|
|
94
|
+
*/
|
|
95
|
+
private resolveFramework(
|
|
92
96
|
manifest: ApplianceFrameworkApp,
|
|
93
|
-
|
|
97
|
+
s3Key: string,
|
|
94
98
|
config: ReturnType<typeof getBaseConfig>
|
|
95
|
-
):
|
|
96
|
-
if (!config.aws.dataBucketName) throw new Error('Data bucket not configured');
|
|
97
|
-
|
|
99
|
+
): ResolvedBuild {
|
|
98
100
|
const port = manifest.port ?? 8080;
|
|
99
101
|
const framework = manifest.framework ?? 'auto';
|
|
100
|
-
const
|
|
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 });
|
|
102
|
+
const runtime = FRAMEWORK_RUNTIMES[framework] ?? FRAMEWORK_RUNTIMES['node'];
|
|
107
103
|
|
|
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
104
|
const platform = manifest.platform;
|
|
126
105
|
const layerArn = LAMBDA_ADAPTER_LAYER[platform]?.replace('${region}', config.aws.region);
|
|
127
106
|
if (!layerArn) throw new Error(`No Lambda Web Adapter layer for platform: ${platform}`);
|
|
128
107
|
const architecture = FRAMEWORK_ARCHITECTURES[platform];
|
|
129
108
|
if (!architecture) throw new Error(`No Lambda architecture for platform: ${platform}`);
|
|
130
109
|
|
|
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
110
|
return {
|
|
136
|
-
codeS3Key:
|
|
111
|
+
codeS3Key: s3Key,
|
|
137
112
|
runtime,
|
|
138
113
|
handler: 'run.sh',
|
|
139
114
|
layers: [layerArn],
|
|
@@ -145,26 +120,10 @@ export class BuildService {
|
|
|
145
120
|
};
|
|
146
121
|
}
|
|
147
122
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Container builds are fully pre-processed by the CLI (Lambda Web Adapter
|
|
125
|
+
* already injected). The server just loads the image and pushes it to ECR.
|
|
126
|
+
*/
|
|
168
127
|
private async resolveContainer(
|
|
169
128
|
tmpDir: string,
|
|
170
129
|
manifest: ApplianceContainer,
|
|
@@ -177,31 +136,12 @@ export class BuildService {
|
|
|
177
136
|
const imageTarPath = path.join(tmpDir, 'image.tar');
|
|
178
137
|
if (!fs.existsSync(imageTarPath)) throw new Error('Build missing image.tar');
|
|
179
138
|
|
|
180
|
-
// Load the
|
|
139
|
+
// Load the pre-built Lambda-ready image
|
|
181
140
|
const loadOutput = execSync(`docker load -i "${imageTarPath}"`, { encoding: 'utf-8' });
|
|
182
|
-
// Output is like "Loaded image: name:tag" or "Loaded image ID: sha256:abc..."
|
|
183
141
|
const loadedMatch = loadOutput.match(/Loaded image(?: ID)?:\s*(.+)/);
|
|
184
142
|
if (!loadedMatch) throw new Error(`Failed to parse docker load output: ${loadOutput}`);
|
|
185
143
|
const loadedImage = loadedMatch[1].trim();
|
|
186
144
|
|
|
187
|
-
// Wrap the image with the Lambda Web Adapter so the same plain HTTP
|
|
188
|
-
// container works on both Lambda and ECS/Fargate without any changes.
|
|
189
|
-
const lambdaImageName = `${manifest.name}-lambda`;
|
|
190
|
-
const wrapperDockerfile = path.join(tmpDir, 'Dockerfile.lambda');
|
|
191
|
-
fs.writeFileSync(
|
|
192
|
-
wrapperDockerfile,
|
|
193
|
-
[
|
|
194
|
-
`FROM --platform=${manifest.platform} public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 AS adapter`,
|
|
195
|
-
`FROM ${loadedImage}`,
|
|
196
|
-
`COPY --from=adapter /lambda-adapter /opt/extensions/lambda-adapter`,
|
|
197
|
-
`ENV AWS_LWA_PORT=${manifest.port}`,
|
|
198
|
-
].join('\n')
|
|
199
|
-
);
|
|
200
|
-
execSync(
|
|
201
|
-
`docker build --platform ${manifest.platform} --provenance=false -f "${wrapperDockerfile}" -t "${lambdaImageName}" "${tmpDir}"`,
|
|
202
|
-
{ stdio: 'pipe' }
|
|
203
|
-
);
|
|
204
|
-
|
|
205
145
|
// Auth with ECR
|
|
206
146
|
const ecr = new ECRClient({ region: config.aws.region });
|
|
207
147
|
const authResult = await ecr.send(new GetAuthorizationTokenCommand({}));
|
|
@@ -217,15 +157,15 @@ export class BuildService {
|
|
|
217
157
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
218
158
|
});
|
|
219
159
|
|
|
220
|
-
// Tag and push
|
|
160
|
+
// Tag and push
|
|
221
161
|
const remoteTag = `${ecrRepositoryUrl}:${tag}`;
|
|
222
|
-
execSync(`docker tag ${
|
|
223
|
-
execSync(`docker push ${remoteTag}`, { stdio: 'pipe' });
|
|
162
|
+
execSync(`docker tag "${loadedImage}" "${remoteTag}"`, { stdio: 'pipe' });
|
|
163
|
+
execSync(`docker push "${remoteTag}"`, { stdio: 'pipe' });
|
|
224
164
|
|
|
225
165
|
// Get digest
|
|
226
166
|
let imageUri: string;
|
|
227
167
|
try {
|
|
228
|
-
imageUri = execSync(`docker inspect --format='{{index .RepoDigests 0}}' ${remoteTag}`, {
|
|
168
|
+
imageUri = execSync(`docker inspect --format='{{index .RepoDigests 0}}' "${remoteTag}"`, {
|
|
229
169
|
encoding: 'utf-8',
|
|
230
170
|
}).trim();
|
|
231
171
|
} catch {
|