@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appliance.sh/api-server",
3
- "version": "1.21.0",
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.21.0",
23
- "@appliance.sh/sdk": "1.21.0",
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, PutObjectCommand } from '@aws-sdk/client-s3';
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(tmpDir, manifest, tag, config);
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
- private async resolveFramework(
91
- tmpDir: string,
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
- tag: string,
97
+ s3Key: string,
94
98
  config: ReturnType<typeof getBaseConfig>
95
- ): Promise<ResolvedBuild> {
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 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 });
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: repackagedKey,
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
- 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
-
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 user's original image into Docker and capture the loaded image reference
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 the Lambda-wrapped image
160
+ // Tag and push
221
161
  const remoteTag = `${ecrRepositoryUrl}:${tag}`;
222
- execSync(`docker tag ${lambdaImageName} ${remoteTag}`, { stdio: 'pipe' });
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 {