diff --git a/deployment-service/snakeapi_service/dockerfile b/deployment-service/snakeapi_service/dockerfile index 8095003..bac1efc 100644 --- a/deployment-service/snakeapi_service/dockerfile +++ b/deployment-service/snakeapi_service/dockerfile @@ -5,6 +5,7 @@ RUN pip install --no-cache-dir jupyter flask awscli flask_cors nbconvert nbforma COPY entrypoint.sh . COPY notebooks ./notebooks +COPY snakeapi_server.py . RUN chmod +x entrypoint.sh diff --git a/deployment-service/snakeapi_service/entrypoint.sh b/deployment-service/snakeapi_service/entrypoint.sh index 38688b5..cbb5f65 100644 --- a/deployment-service/snakeapi_service/entrypoint.sh +++ b/deployment-service/snakeapi_service/entrypoint.sh @@ -1,17 +1,16 @@ #!/usr/bin/env bash - NOTEBOOK_DIR="notebooks" -mkdir -p ${NOTEBOOK_DIR} +mkdir -p "${NOTEBOOK_DIR}" -while true; do - aws --endpoint-url "$AWS_ENDPOINT_URL_S3" --region "$AWS_REGION" \ - s3 sync "s3://$BUCKET_NAME/$INSTANCE_PREFIX/notebooks/" "${NOTEBOOK_DIR}/" +# fetch latest notebook +aws --endpoint-url "$AWS_ENDPOINT_URL_S3" --region "$AWS_REGION" \ + s3 sync "s3://$BUCKET_NAME/$INSTANCE_PREFIX/notebooks/" "${NOTEBOOK_DIR}/" - latest_notebook=$(ls -t ${NOTEBOOK_DIR}/*.ipynb | head -1) +# convert to Python script for dynamic import +latest_ipynb=$(ls -t "${NOTEBOOK_DIR}"/*.ipynb | head -1) +if [ -n "$latest_ipynb" ]; then + jupyter nbconvert --to script "$latest_ipynb" --output "${NOTEBOOK_DIR}/notebook.py" +fi - if [ -n "$latest_notebook" ]; then - jupyter nbconvert --to notebook --execute --inplace --ExecutePreprocessor.timeout=0 "$latest_notebook" - fi - - sleep 5 -done +# start the Flask server +python snakeapi_server.py diff --git a/deployment-service/snakeapi_service/snakeapi_server.py b/deployment-service/snakeapi_service/snakeapi_server.py new file mode 100644 index 0000000..c81317a --- /dev/null +++ b/deployment-service/snakeapi_service/snakeapi_server.py @@ -0,0 +1,44 @@ +# snakeapi_service/snakeapi_server.py +import os +import importlib.util +from flask import Flask, request +from flask_cors import CORS + +# load notebook code as module +spec = importlib.util.spec_from_file_location( + "nb_module", os.path.join("notebooks", "notebook.py") +) +nb = importlib.util.module_from_spec(spec) +spec.loader.exec_module(nb) + +handlers = { + "info": nb.info, + "start": nb.start, + "move": nb.move, + "end": nb.end +} + +app = Flask(__name__) +CORS(app) + +@app.route("/", methods=["GET"]) +def on_info(): + return handlers["info"]() + +@app.route("/start", methods=["POST"]) +def on_start(): + handlers["start"](request.get_json()) + return "ok" + +@app.route("/move", methods=["POST"]) +def on_move(): + return handlers["move"](request.get_json()) + +@app.route("/end", methods=["POST"]) +def on_end(): + handlers["end"](request.get_json()) + return "ok" + +if __name__ == "__main__": + port = int(os.environ.get("PORT", "3006")) + app.run(host="0.0.0.0", port=port) diff --git a/deployment-service/src/index.js b/deployment-service/src/index.js index ffeca4c..9f53171 100644 --- a/deployment-service/src/index.js +++ b/deployment-service/src/index.js @@ -3,7 +3,6 @@ const fs = require('fs'); const path = require('path'); const AWS = require('aws-sdk'); const axios = require('axios'); -const tar = require('tar'); const { FLY_ORG, @@ -12,7 +11,8 @@ const { AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL_S3, AWS_REGION, - FLY_ACCESS_TOKEN + FLY_ACCESS_TOKEN, + IMAGE_REF } = process.env; const s3 = new AWS.S3({ @@ -62,7 +62,6 @@ app.post('/deploy', async (req, res) => { }); const notebookFile = path.join(__dirname, '../snakeapi_service/notebooks', notebookName); - if (!fs.existsSync(notebookFile)) { throw new Error(`Notebook file ${notebookName} not found.`); } @@ -70,27 +69,37 @@ app.post('/deploy', async (req, res) => { const notebookData = fs.readFileSync(notebookFile); const timestamp = Date.now(); + const notebookKey = `${appName}/notebooks/${timestamp}-notebook.ipynb`; + await s3.putObject({ Bucket: COMMON_BUCKET, - Key: `${appName}/notebooks/${timestamp}-notebook.ipynb`, + Key: notebookKey, Body: notebookData, ContentType: 'application/json' }).promise(); - const tarFilePath = `/tmp/${appName}.tar.gz`; - await tar.c( - { - gzip: true, - file: tarFilePath, - cwd: path.join(__dirname, '../snakeapi_service') - }, - ['.'] - ); + const machineConfig = { + name: `${appName}-machine`, + config: { + image: IMAGE_REF, + env: { + INSTANCE_PREFIX: appName, + NOTEBOOK_KEY: notebookKey, + BUCKET_NAME: COMMON_BUCKET, + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, + AWS_ENDPOINT_URL_S3, + AWS_REGION + }, + services: [{ + ports: [{ port: 3006, handlers: ["http"] }], + protocol: "tcp", + internal_port: 3006 + }] + } + }; - const tarData = fs.readFileSync(tarFilePath); - await fly.post(`/apps/${appName}/deploys`, tarData, { - headers: { 'Content-Type': 'application/gzip' } - }); + await fly.post(`/apps/${appName}/machines`, machineConfig); res.json({ status: 'created', @@ -102,5 +111,50 @@ app.post('/deploy', async (req, res) => { } }); +app.post('/upload', async (req, res) => { + const { appName, notebookName } = req.body; + if (!appName || !notebookName) { + return res.status(400).json({ error: 'appName and notebookName required' }); + } + + try { + const notebookFile = path.join(__dirname, '../snakeapi_service/notebooks', notebookName); + if (!fs.existsSync(notebookFile)) { + throw new Error(`Notebook file ${notebookName} not found.`); + } + + const notebookData = fs.readFileSync(notebookFile); + const timestamp = Date.now(); + + const notebookKey = `${appName}/notebooks/${timestamp}-notebook.ipynb`; + + await s3.putObject({ + Bucket: COMMON_BUCKET, + Key: notebookKey, + Body: notebookData, + ContentType: 'application/json' + }).promise(); + + res.json({ status: 'uploaded', notebookKey }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.delete('/delete/:appName', async (req, res) => { + const appName = req.params.appName; + if (!appName) { + return res.status(400).json({ error: 'appName required' }); + } + + try { + const fly = createFlyClient(); + await fly.delete(`/apps/${appName}`); + res.json({ status: 'deleted', app: appName }); + } catch (error) { + res.status(500).json({ error: error.response?.data || error.message }); + } +}); + const port = process.env.PORT || 3006; app.listen(port, '0.0.0.0', () => console.log(`Listening on port ${port}`));