Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/backend/db/prisma/schema/custom-server/lambda.prisma
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
enum LambdaServerInstanceRuntime {
deno_deploy_v1
deno_self_hosted_v1
python_self_hosted_v1
aws_lambda_nodejs_24_x
aws_lambda_nodejs_22_x
aws_lambda_python_3_9
Expand All @@ -13,6 +14,7 @@ enum LambdaServerInstanceRuntime {
enum LambdaServerInstanceProvider {
deno_deploy
deno_self_hosted
python_self_hosted
aws_lambda
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {
CodeBucket,
CustomServer,
CustomServerDeployment,
db,
Instance,
LambdaServerInstance
} from '@metorial/db';
import { delay } from '@metorial/delay';
import { joinPaths } from '@metorial/join-paths';
import axios from 'axios';
import { env } from '../../../env';
import { DeploymentError } from '../../base/error';
import { getPythonFs } from '../fs';

axios.defaults.headers.common['Accept-Encoding'] = 'gzip';

// Determine deployment mode
export let isPythonLocalEnabled = () => env.python.PYTHON_RUNNER_ADDRESS;

export let createPythonLambdaDeployment = async (config: {
lambdaServerInstance: LambdaServerInstance & {
immutableCodeBucket: CodeBucket;
instance: Instance;
};
customServer: CustomServer;
deployment: CustomServerDeployment;
}) => {
if (!isPythonLocalEnabled()) {
throw new Error('Python Local deployment is not enabled in the environment variables.');
}

let lambdaServerInstance = config.lambdaServerInstance;

let deployment = await Promise.race([
delay(1000 * 60 * 2).then(() => {
throw new DeploymentError({
code: 'deployment_timeout',
message: 'Python deployment timed out after 5 minutes'
});
}),
(async () => {
let fs = await getPythonFs(lambdaServerInstance);

let deploymentPayload = {
entryPointUrl: fs.entrypoint,
envVars: {
...fs.env,
METORIAL_AUTH_TOKEN_SECRET: lambdaServerInstance.securityToken
},
description: `CSRV ${config.customServer.id} / DEPL ${config.deployment.id}`,
permissions: {
net: ['*']
},
assets: Object.fromEntries(
Array.from(fs.files.entries()).map(([k, v]) => [
k,
{
kind: 'file',
encoding: 'utf-8',
content: v
}
])
)
};

let deploymentId: string;
let providerResourceAccessIdentifier: string;

let runnerDeployment = await axios.post<{ id: string }>(
`${env.python.PYTHON_RUNNER_ADDRESS}/deployments`,
deploymentPayload
);

deploymentId = runnerDeployment.data.id;
providerResourceAccessIdentifier = `${env.python.PYTHON_RUNNER_ADDRESS}/${deploymentId}`;

return await db.lambdaServerInstance.update({
where: { oid: lambdaServerInstance.oid },
data: {
status: 'deploying',
providerInfo: { id: deploymentId },
providerResourceId: deploymentId,
providerResourceAccessIdentifier,
runtime: 'python_self_hosted_v1',
provider: 'python_self_hosted',
platform: 'metorial_stellar_v1',
protocol: 'metorial_stellar_over_websocket_v1'
}
});
})()
]);

let serverUrl = { current: deployment.providerResourceAccessIdentifier || '' };

return {
pollDeploymentStatus: async () => {
return {
status: 'success' as const,
logs: [] as { type: 'info' | 'error'; lines: string[] }[]
};
},

discoverServer: async () => {
let discoverUrl = new URL(serverUrl.current);
discoverUrl.pathname = joinPaths(discoverUrl.pathname, '/discover');
let discoverRes = await axios.get<any>(discoverUrl.toString(), {
headers: {
'metorial-stellar-token': lambdaServerInstance.securityToken
},
timeout: 5000
});

let oauthUrl = new URL(serverUrl.current);
oauthUrl.pathname = joinPaths(oauthUrl.pathname, '/oauth');
let oauthRes = await axios.get<{ enabled: boolean; hasForm: boolean }>(
oauthUrl.toString(),
{
headers: {
'metorial-stellar-token': lambdaServerInstance.securityToken
},
timeout: 5000
}
);

let callbacksUrl = new URL(serverUrl.current);
callbacksUrl.pathname = joinPaths(callbacksUrl.pathname, '/callbacks');
let callbacksRes = await axios.get<{
enabled: boolean;
type: 'webhook' | 'polling' | 'manual';
}>(callbacksUrl.toString(), {
headers: {
'metorial-stellar-token': lambdaServerInstance.securityToken
},
timeout: 5000
});

return {
capabilities: discoverRes.data,
oauth: oauthRes.data,
callbacks: callbacksRes.data
};
},

get httpEndpoint() {
return serverUrl.current;
}
};
};

export type PythonDeployment = Awaited<ReturnType<typeof createPythonLambdaDeployment>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { CodeBucket, Instance, LambdaServerInstance } from '@metorial/db';
import { codeBucketService } from '@metorial/module-code-bucket';
import { DeploymentError } from '../../base/error';

let commonEntryPoints = ['index', 'app', 'main', 'server', 'boot', 'mcp'].flatMap(name => [
`${name}.ts`,
`${name}.js`,
`${name}.cjs`,
`${name}.mjs`
]);

export let getPythonFs = async (
lambda: LambdaServerInstance & {
instance: Instance;
immutableCodeBucket: CodeBucket;
}
) => {
let files = new Map(
Object.entries({
// TODO: @RahmeKarim add boot loader files for python here
// 'boot.ts': bootTs,
// 'delay.ts': delayTs,
// 'discover.ts': discoverTs,
// 'error.ts': errorTs,
// 'logs.ts': logsTs,
// 'promise.ts': promiseTs,
// 'server.ts': serverTs,
// 'transport.ts': transportTs,
// 'lib/index.ts': libIndexTs,
// 'lib/args.ts': libArgsTs,
// 'lib/oauth.ts': libOauthTs,
// 'lib/callbacks.ts': libCallbacksTs,
// 'config.ts': configTs,
// 'oauth.ts': oauthTs,
// 'callbacks.ts': callbacksTs
})
);

let bucketFiles = await codeBucketService.getCodeBucketFilesWithContent({
codeBucket: lambda.immutableCodeBucket
});

for (let file of bucketFiles) {
let path = `app/${file.path}`;
if (files.has(path)) {
throw new DeploymentError({
code: 'invalid_file',
message: `File ${file.path} is reserved and cannot be used in the code bucket`
});
}
files.set(path, new TextDecoder().decode(file.content));
}

let entrypoint: string | undefined;
if (!entrypoint) {
let found = commonEntryPoints.find(name => bucketFiles.some(f => f.path === name));
if (found) entrypoint = found;
}
if (!entrypoint) {
throw new DeploymentError({
code: 'missing_entry_point',
message: `Could not determine entry point. Please specify a "main" field in your package.json file or add one of the following files to your code bucket: ${commonEntryPoints.join(
', '
)}`
});
}

let metorialDeploymentContent = JSON.stringify(
{
entrypoint,
lambda: {
id: lambda.id,
createdAt: lambda.createdAt
},
immutableCodeBucket: {
id: lambda.immutableCodeBucket.id,
createdAt: lambda.immutableCodeBucket.createdAt
},
instance: {
id: lambda.instance.id,
slug: lambda.instance.slug,
name: lambda.instance.name,
type: lambda.instance.type,
createdAt: lambda.instance.createdAt
}
},
null,
2
);
files.set('mtdpl.json', metorialDeploymentContent);

return {
entrypoint: 'boot.py', // TODO: @RahmeKarim change to what the entrypoint should be
env: {
CUSTOM_SERVER_ENTRYPOINT: entrypoint
},
files
};
};
Loading
Loading