genezio
Version:
Command line utility to interact with Genezio infrastructure.
389 lines (310 loc) • 12.5 kB
JavaScript
import crypto from "crypto";
import { writeToFile } from "../../utils/file.js";
import path from "path";
const streamifyOverrideFileContent = `
const METADATA_PRELUDE_CONTENT_TYPE = 'application/vnd.awslambda.http-integration-response';
const DELIMITER_LEN = 8;
global.awslambda = {
streamifyResponse: function (handler) {
return async (event, context) => {
await handler(event, event.responseStream, context);
}
},
HttpResponseStream: {
from: function (underlyingStream, prelude) {
underlyingStream.setContentType(METADATA_PRELUDE_CONTENT_TYPE);
// JSON.stringify is required. NULL byte is not allowed in metadataPrelude.
const metadataPrelude = JSON.stringify(prelude);
underlyingStream._onBeforeFirstWrite = (write) => {
write(metadataPrelude);
// Write 8 null bytes after the JSON prelude.
write(new Uint8Array(DELIMITER_LEN));
};
return underlyingStream;
}
}
};`;
const streamifyOverrideFileContentPython = `
import json
METADATA_PRELUDE_CONTENT_TYPE = 'application/vnd.awslambda.http-integration-response'
DELIMITER_LEN = 8
class AwsLambda:
@staticmethod
async def streamify_response(handler):
async def wrapper(event, context):
await handler(event, event['response_stream'], context)
return wrapper
class HttpResponseStream:
@staticmethod
def from_stream(underlying_stream, prelude):
underlying_stream.set_content_type(METADATA_PRELUDE_CONTENT_TYPE)
metadata_prelude = json.dumps(prelude)
def on_before_first_write(write):
write(metadata_prelude)
write(bytearray(DELIMITER_LEN))
underlying_stream._on_before_first_write = on_before_first_write
return underlying_stream
`;
export class AwsFunctionHandlerProvider {
async write(outputPath, handlerFileName, functionConfiguration) {
const randomFileId = crypto.randomBytes(8).toString("hex");
const handlerContent = `import './setupLambdaGlobals_${randomFileId}.mjs';
import { ${functionConfiguration.handler} as genezioDeploy } from "./${functionConfiguration.entry}";
import { isUtf8 } from "buffer";
function formatTimestamp(timestamp) {
const date = new Date(timestamp);
const day = String(date.getUTCDate()).padStart(2, "0");
const monthNames = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
];
const month = monthNames[date.getUTCMonth()];
const year = date.getUTCFullYear();
const hours = String(date.getUTCHours()).padStart(2, "0");
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
const formattedDate = ` +
"`${day}/${month}/${year}:${hours}:${minutes}:${seconds} +0000`" +
`;
return formattedDate;
}
const handler = async function(event) {
const http2CompliantHeaders = {};
for (const header in event.headers) {
http2CompliantHeaders[header.toLowerCase()] = event.headers[header];
}
const isBinary = !isUtf8(event.body);
const req = {
version: "2.0",
routeKey: "$default",
rawPath: event.url.pathname,
rawQueryString: event.url.search,
headers: http2CompliantHeaders,
queryStringParameters: Object.fromEntries(event.url.searchParams),
requestContext: {
accountId: "anonymous",
apiId: event.headers.Host.split(".")[0],
domainName: event.headers.Host,
domainPrefix: event.headers.Host.split(".")[0],
http: {
method: event.http.method,
path: event.http.path,
protocol: event.http.protocol,
sourceIp: event.http.sourceIp,
userAgent: event.http.userAgent
},
requestId: "undefined",
routeKey: "$default",
stage: "$default",
time: formatTimestamp(event.requestTimestampMs),
timeEpoch: event.requestTimestampMs
},
body: event.body.toString(/* encoding= */ isBinary ? "base64" : "utf8"),
isBase64Encoded: isBinary,
responseStream: event.responseStream,
};
const result = await genezioDeploy(req)
// Ensure result and headers exist
if (!result) {
return { headers: {} };
}
if (!result.headers) {
result.headers = {};
}
// Only process cookies if they exist
if (result.cookies && Array.isArray(result.cookies)) {
for (const cookie of result.cookies) {
result.headers["Set-Cookie"] = cookie;
}
}
return result;
};
export { handler };`;
await writeToFile(outputPath, handlerFileName, handlerContent);
await writeToFile(outputPath, `setupLambdaGlobals_${randomFileId}.mjs`, streamifyOverrideFileContent);
}
async getLocalFunctionWrapperCode(handler, functionConfiguration) {
return `import { ${handler} as userHandler } from "./${functionConfiguration.entry}";
import http from "http";
const port = process.argv[2];
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
res.writeHead(200, {'Content-Type': 'text/plain'});
const jsonParsedBody = JSON.parse(body);
userHandler(jsonParsedBody).then((response) => {
res.end(JSON.stringify(response));
})
});
} else {
res.writeHead(404, {'Content-Type': 'text/plain'});
res.end('404 Not Found');
}
});
server.listen(port, () => {
});
`;
}
}
export class AwsPythonFunctionHandlerProvider {
async write(outputPath, handlerFileName, functionConfiguration) {
const randomFileId = crypto.randomBytes(8).toString("hex");
const nameModule = path
.join(functionConfiguration.path, functionConfiguration.entry)
.replace(/\\/g, ".") // Convert backslashes to dots (Windows)
.replace(/\//g, ".") // Convert slashes to dots (Unix)
.replace(/^\.+/, "") // Remove leading dots
.replace(/\.+/g, ".") // Remove duplicate dots
.replace(/\.py$/, "") // Remove extension
.replace(/-/g, "_"); // Convert hyphens to underscores for Python module compatibility
const handlerContent = `
from setupLambdaGlobals_${randomFileId} import AwsLambda
from ${nameModule} import ${functionConfiguration.handler} as genezio_deploy
import codecs
def format_timestamp(timestamp):
from datetime import datetime
return datetime.utcfromtimestamp(timestamp / 1000).strftime('%d/%b/%Y:%H:%M:%S +0000')
def is_utf8(data):
try:
if isinstance(data, bytes):
codecs.decode(data, 'utf-8')
return True
except UnicodeDecodeError:
return False
def handler(event):
headers = event.get('headers', {})
http2_compliant_headers = {header.lower(): value for header, value in headers.items()}
body = event.get('body', None)
is_binary = body is not None and not is_utf8(body)
req = {
"version": "2.0",
"routeKey": "$default",
"rawPath": event.get('path', '/'),
"rawQueryString": event.get('query', ''),
"headers": http2_compliant_headers,
"queryStringParameters": {
key: value for key, value in [param.split('=') for param in event.get('query', '').split('&') if '=' in param]
},
"requestContext": {
"accountId": "anonymous",
"apiId": headers.get('host', '').split('.')[0],
"domainName": headers.get('host', ''),
"domainPrefix": headers.get('host', '').split('.')[0],
"http": {
"method": event.get('http', {}).get('method', 'GET'),
"path": event.get('path', '/'),
"protocol": f"HTTP/{event.get('http', {}).get('protocol', ['1', '1'])[0]}.{event.get('http', {}).get('protocol', ['1', '1'])[1]}",
"sourceIp": event.get('http', {}).get('sourceIp', '0.0.0.0'),
"userAgent": event.get('http', {}).get('userAgent', 'unknown')
},
"requestId": "undefined",
"routeKey": "$default",
"stage": "$default",
"time": format_timestamp(event.get('requestTimestampMs', 0)),
"timeEpoch": event.get('requestTimestampMs', 0)
},
"body": (body if isinstance(body, str) else body.decode('utf-8') if body else None) if not is_binary else (body.decode('latin-1') if body else None),
"isBase64Encoded": is_binary,
"response_stream": event.get('responseStream', None),
}
result = genezio_deploy(req)
# Ensure we always have a dict result with headers
if result is None:
result = {"headers": {}}
elif not isinstance(result, dict):
result = {"headers": {}, "body": str(result)}
elif "headers" not in result:
result["headers"] = {}
if 'cookies' in result:
result['headers']['Set-Cookie'] = result['cookies']
return result`;
await writeToFile(outputPath, handlerFileName, handlerContent);
await writeToFile(outputPath, `setupLambdaGlobals_${randomFileId}.py`, streamifyOverrideFileContentPython);
}
async getLocalFunctionWrapperCode(handler, functionConfiguration) {
const nameModule = path
.join(functionConfiguration.path, functionConfiguration.entry)
.replace(/\\/g, ".") // Convert backslashes to dots (Windows)
.replace(/\//g, ".") // Convert slashes to dots (Unix)
.replace(/^\.+/, "") // Remove leading dots
.replace(/\.+/g, ".") // Remove duplicate dots
.replace(/\.py$/, "") // Remove extension
.replace(/-/g, "_"); // Convert hyphens to underscores for Python module compatibility;
return `
import sys
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from ${nameModule} import ${handler} as userHandler
class RequestHandler(BaseHTTPRequestHandler):
def log_error(self, format, *args):
# Disable default error logging since we handle it ourselves
pass
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
try:
jsonParsedBody = json.loads(post_data)
response = userHandler(jsonParsedBody)
# Ensure we always have a dict result with headers
if response is None:
response = {"headers": {}}
elif not isinstance(response, dict):
response = {"headers": {}, "body": str(response)}
elif "headers" not in response:
response["headers"] = {}
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response).encode('utf-8'))
except Exception as e:
import traceback
error_trace = traceback.format_exc()
error_response = {
"statusCode": 500,
"body": {
"message": str(e),
"stacktrace": error_trace
}
}
self.send_response(500)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(error_response).encode('utf-8'))
# Log the HTTP access first
self.log_message('"%s" %s %s',
self.requestline, str(500), '-')
# Then print the error details
print("Traceback:", error_trace, file=sys.stderr)
print("Error occurred:", str(e), file=sys.stderr)
sys.stderr.flush()
finally:
sys.stdout.flush()
sys.stderr.flush()
def run():
port = int(sys.argv[1])
server_address = ('', port)
httpd = HTTPServer(server_address, RequestHandler)
try:
httpd.serve_forever()
except KeyboardInterrupt:
sys.stdout.flush()
httpd.server_close()
if __name__ == "__main__":
run()
`;
}
}