Skip to content

Commit

Permalink
Add env var and headers support to local development
Browse files Browse the repository at this point in the history
  • Loading branch information
Meldiron committed Jun 5, 2024
1 parent 7583db3 commit 73e3c40
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 30 deletions.
131 changes: 106 additions & 25 deletions templates/cli/lib/commands/run.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const EventEmitter = require('node:events');
const ignore = require("ignore");
const tar = require("tar");
const fs = require("fs");
const ID = require("../id");
const childProcess = require('child_process');
const chokidar = require('chokidar');
const inquirer = require("inquirer");
Expand All @@ -11,6 +12,8 @@ const { Command } = require("commander");
const { localConfig, globalConfig } = require("../config");
const { paginate } = require('../paginate');
const { functionsListVariables } = require('./functions');
const { usersGet, usersCreateJWT } = require('./users');
const { projectsCreateJWT } = require('./projects');
const { questionsRunFunctions } = require("../questions");
const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser");
const { systemHasCommand, isPortTaken, getAllFiles } = require('../utils');
Expand All @@ -31,17 +34,67 @@ const systemTools = {
// TODO: Add all runtime needs
};

const JwtManager = {
userJwt: null,
functionJwt: null,

timerWarn: null,
timerError: null,

async setup(userId = null) {
if(this.timerWarn) {
clearTimeout(this.timerWarn);
}

if(this.timerError) {
clearTimeout(this.timerError);
}

this.timerWarn = setTimeout(() => {
log("Warning: Authorized JWT will expire in 5 minutes. Please stop and re-run the command to refresh tokens for 1 hour.");
}, 1000 * 60 * 55); // 55 mins

this.timerError = setTimeout(() => {
log("Warning: Authorized JWT just expired. Please stop and re-run the command to obtain new tokens with 1 hour validity.");
log("Some Appwrite API communication is not authorized now.")
}, 1000 * 60 * 60); // 60 mins

if(userId) {
await usersGet({
userId,
parseOutput: false
});
const userResponse = await usersCreateJWT({
userId,
duration: 60*60,
parseOutput: false
});
this.userJwt = userResponse.jwt;
}

const functionResponse = await projectsCreateJWT({
projectId: localConfig.getProject().projectId,
// TODO: There must be better way to get the list
scopes: ["sessions.write","users.read","users.write","teams.read","teams.write","databases.read","databases.write","collections.read","collections.write","attributes.read","attributes.write","indexes.read","indexes.write","documents.read","documents.write","files.read","files.write","buckets.read","buckets.write","functions.read","functions.write","execution.read","execution.write","locale.read","avatars.read","health.read","providers.read","providers.write","messages.read","messages.write","topics.read","topics.write","subscribers.read","subscribers.write","targets.read","targets.write","rules.read","rules.write","migrations.read","migrations.write","vcs.read","vcs.write","assistant.read"],
duration: 60*60,
parseOutput: false
});
this.functionJwt = functionResponse.jwt;
}
};

const Queue = {
files: [],
locked: false,
events: new EventEmitter(),
debounce: null,
push(file) {
if(!this.files.includes(file)) {
this.files.push(file);
}

if(!this.locked) {
this.events.emit('reload', { files: this.files });
this._trigger();
}
},
lock() {
Expand All @@ -51,13 +104,23 @@ const Queue = {
unlock() {
this.locked = false;
if(this.files.length > 0) {
this.events.emit('reload', { files: this.files });
this._trigger();
}
},
_trigger() {
if(this.debounce) {
return;
}

this.debounce = setTimeout(() => {
this.events.emit('reload', { files: this.files });
this.debounce = null;
}, 300);
}
};

async function dockerStop(id) {
delete activeDockerIds[id];
delete activeDockerIds[id];
const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], {
stdio: 'pipe',
});
Expand Down Expand Up @@ -92,7 +155,7 @@ async function dockerBuild(func, variables) {

const functionDir = path.join(process.cwd(), func.path);

const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}`;
const id = ID.unique();

const params = [ 'run' ];
params.push('--name', id);
Expand All @@ -102,8 +165,8 @@ async function dockerBuild(func, variables) {
params.push('-e', 'OPEN_RUNTIMES_SECRET=');
params.push('-e', `OPEN_RUNTIMES_ENTRYPOINT=${func.entrypoint}`);

for(const v of variables) {
params.push('-e', `${v.key}=${v.value}`);
for(const k of Object.keys(variables)) {
params.push('-e', `${k}=${variables[k]}`);
}

params.push(imageName, 'sh', '-c', `helpers/build.sh "${func.commands}"`);
Expand Down Expand Up @@ -167,7 +230,7 @@ async function dockerStart(func, variables, port) {

const functionDir = path.join(process.cwd(), func.path);

const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}`;
const id = ID.unique();

const params = [ 'run' ];
params.push('--rm');
Expand All @@ -178,8 +241,8 @@ async function dockerStart(func, variables, port) {
params.push('-e', 'OPEN_RUNTIMES_ENV=development');
params.push('-e', 'OPEN_RUNTIMES_SECRET=');

for(const v of variables) {
params.push('-e', `${v.key}=${v.value}`);
for(const k of Object.keys(variables)) {
params.push('-e', `${k}=${variables[k]}`);
}

params.push('-v', `${functionDir}/.appwrite/logs.txt:/mnt/logs/dev_logs.log:rw`);
Expand Down Expand Up @@ -215,7 +278,7 @@ async function dockerCleanup() {
}
}

const runFunction = async ({ port, engine, functionId, noVariables, noReload } = {}) => {
const runFunction = async ({ port, engine, functionId, noVariables, noReload, userId } = {}) => {
// Selection
if(!functionId) {
const answers = await inquirer.prompt(questionsRunFunctions[0]);
Expand Down Expand Up @@ -265,14 +328,10 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } =
}

if(engine === 'docker') {
log('💡 Hint: Using system is faster, but using Docker simulates the production environment precisely.');

if(!systemHasCommand('docker')) {
return error("Please install Docker first: https://docs.docker.com/engine/install/");
}
} else if(engine === 'system') {
log('💡 Hint: Docker simulates the production environment precisely, but using system is faster');

for(const command of tool.commands) {
if(!systemHasCommand(command.command)) {
return error(`Your system is missing command "${command.command}". Please install it first: ${command.docs}`);
Expand Down Expand Up @@ -317,25 +376,45 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } =
fs.writeFileSync(errorsPath, '');
}

let variables = [];
const variables = {};
if(!noVariables) {
if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') {
error("No user is signed in. To sign in, run: appwrite login. Function will run locally, but will not have your function's environment variables set.");
} else {
const { variables: remoteVariables } = await paginate(functionsListVariables, {
functionId: func['$id'],
parseOutput: false
}, 100, 'variables');

remoteVariables.forEach((v) => {
variables.push({
key: v.key,
value: v.value
try {
const { variables: remoteVariables } = await paginate(functionsListVariables, {
functionId: func['$id'],
parseOutput: false
}, 100, 'variables');

remoteVariables.forEach((v) => {
variables[v.key] = v.value;
});
});
} catch(err) {
error("Could not fetch remote variables: " + err.message);
error("Function will run locally, but will not have your function's environment variables set.");
}
}
}

variables['APPWRITE_FUNCTION_API_ENDPOINT'] = globalConfig.getFrom('endpoint');
variables['APPWRITE_FUNCTION_ID'] = func.$id;
variables['APPWRITE_FUNCTION_NAME'] = func.name;
variables['APPWRITE_FUNCTION_DEPLOYMENT'] = ''; // TODO: Implement when relevant
variables['APPWRITE_FUNCTION_PROJECT_ID'] = localConfig.getProject().projectId;
variables['APPWRITE_FUNCTION_RUNTIME_NAME'] = ''; // TODO: Implement when relevant
variables['APPWRITE_FUNCTION_RUNTIME_VERSION'] = ''; // TODO: Implement when relevant

await JwtManager.setup(userId);

const headers = {};
headers['x-appwrite-key'] = JwtManager.functionJwt ?? '';
headers['x-appwrite-trigger'] = 'http';
headers['x-appwrite-event'] = '';
headers['x-appwrite-user-id'] = userId ?? '';
headers['x-appwrite-user-jwt'] = JwtManager.userJwt ?? '';
variables['OPEN_RUNTIMES_HEADERS'] = JSON.stringify(headers);

await dockerPull(func);
await dockerBuild(func, variables);
await dockerStart(func, variables, port);
Expand All @@ -348,6 +427,7 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } =
});

if(!noReload) {
// TODO: Stop previous job mid-way if new deployment is ready, I think?
chokidar.watch('.', {
cwd: path.join(process.cwd(), func.path),
ignoreInitial: true,
Expand Down Expand Up @@ -460,6 +540,7 @@ run
.option(`--functionId <functionId>`, `Function ID`)
.option(`--port <port>`, `Local port`)
.option(`--engine <engine>`, `Local engine, "system" or "docker"`)
.option(`--userId <userId>`, `ID of user to impersonate`)
.option(`--noVariables`, `Prevent pulling variables from function settings`)
.option(`--noReload`, `Prevent live reloading of server when changes are made to function files`)
.action(actionRunner(runFunction));
Expand Down
3 changes: 1 addition & 2 deletions templates/cli/lib/config.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ class Global extends Config {
return this.get(Global.PREFERENCE_CURRENT);
}

setCurrentLogin(endpoint) {
setCurrentLogin(id) {
this.set(Global.PREFERENCE_CURRENT, endpoint);
}

Expand Down Expand Up @@ -516,7 +516,6 @@ class Global extends Config {
this.setTo(Global.PREFERENCE_KEY, key);
}


hasFrom(key) {
const current = this.getCurrentLogin();

Expand Down
6 changes: 3 additions & 3 deletions templates/cli/lib/questions.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -820,11 +820,11 @@ const questionsRunFunctions = [
message: "Which engine would you like to use?",
choices: [
{
name: "Docker",
value: "docker",
name: "Docker (recommended, simulates production precisely)",
value: "docker",
},
{
name: "System",
name: "System (faster and easier to debug)",
value: "system",
},
],
Expand Down

0 comments on commit 73e3c40

Please sign in to comment.