Skip to content

Commit 73e3c40

Browse files
committed
Add env var and headers support to local development
1 parent 7583db3 commit 73e3c40

File tree

3 files changed

+110
-30
lines changed

3 files changed

+110
-30
lines changed

templates/cli/lib/commands/run.js.twig

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const EventEmitter = require('node:events');
33
const ignore = require("ignore");
44
const tar = require("tar");
55
const fs = require("fs");
6+
const ID = require("../id");
67
const childProcess = require('child_process');
78
const chokidar = require('chokidar');
89
const inquirer = require("inquirer");
@@ -11,6 +12,8 @@ const { Command } = require("commander");
1112
const { localConfig, globalConfig } = require("../config");
1213
const { paginate } = require('../paginate');
1314
const { functionsListVariables } = require('./functions');
15+
const { usersGet, usersCreateJWT } = require('./users');
16+
const { projectsCreateJWT } = require('./projects');
1417
const { questionsRunFunctions } = require("../questions");
1518
const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser");
1619
const { systemHasCommand, isPortTaken, getAllFiles } = require('../utils');
@@ -31,17 +34,67 @@ const systemTools = {
3134
// TODO: Add all runtime needs
3235
};
3336

37+
const JwtManager = {
38+
userJwt: null,
39+
functionJwt: null,
40+
41+
timerWarn: null,
42+
timerError: null,
43+
44+
async setup(userId = null) {
45+
if(this.timerWarn) {
46+
clearTimeout(this.timerWarn);
47+
}
48+
49+
if(this.timerError) {
50+
clearTimeout(this.timerError);
51+
}
52+
53+
this.timerWarn = setTimeout(() => {
54+
log("Warning: Authorized JWT will expire in 5 minutes. Please stop and re-run the command to refresh tokens for 1 hour.");
55+
}, 1000 * 60 * 55); // 55 mins
56+
57+
this.timerError = setTimeout(() => {
58+
log("Warning: Authorized JWT just expired. Please stop and re-run the command to obtain new tokens with 1 hour validity.");
59+
log("Some Appwrite API communication is not authorized now.")
60+
}, 1000 * 60 * 60); // 60 mins
61+
62+
if(userId) {
63+
await usersGet({
64+
userId,
65+
parseOutput: false
66+
});
67+
const userResponse = await usersCreateJWT({
68+
userId,
69+
duration: 60*60,
70+
parseOutput: false
71+
});
72+
this.userJwt = userResponse.jwt;
73+
}
74+
75+
const functionResponse = await projectsCreateJWT({
76+
projectId: localConfig.getProject().projectId,
77+
// TODO: There must be better way to get the list
78+
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"],
79+
duration: 60*60,
80+
parseOutput: false
81+
});
82+
this.functionJwt = functionResponse.jwt;
83+
}
84+
};
85+
3486
const Queue = {
3587
files: [],
3688
locked: false,
3789
events: new EventEmitter(),
90+
debounce: null,
3891
push(file) {
3992
if(!this.files.includes(file)) {
4093
this.files.push(file);
4194
}
4295

4396
if(!this.locked) {
44-
this.events.emit('reload', { files: this.files });
97+
this._trigger();
4598
}
4699
},
47100
lock() {
@@ -51,13 +104,23 @@ const Queue = {
51104
unlock() {
52105
this.locked = false;
53106
if(this.files.length > 0) {
54-
this.events.emit('reload', { files: this.files });
107+
this._trigger();
55108
}
109+
},
110+
_trigger() {
111+
if(this.debounce) {
112+
return;
113+
}
114+
115+
this.debounce = setTimeout(() => {
116+
this.events.emit('reload', { files: this.files });
117+
this.debounce = null;
118+
}, 300);
56119
}
57120
};
58121

59122
async function dockerStop(id) {
60-
delete activeDockerIds[id];
123+
delete activeDockerIds[id];
61124
const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], {
62125
stdio: 'pipe',
63126
});
@@ -92,7 +155,7 @@ async function dockerBuild(func, variables) {
92155

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

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

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

105-
for(const v of variables) {
106-
params.push('-e', `${v.key}=${v.value}`);
168+
for(const k of Object.keys(variables)) {
169+
params.push('-e', `${k}=${variables[k]}`);
107170
}
108171

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

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

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

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

181-
for(const v of variables) {
182-
params.push('-e', `${v.key}=${v.value}`);
244+
for(const k of Object.keys(variables)) {
245+
params.push('-e', `${k}=${variables[k]}`);
183246
}
184247

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

218-
const runFunction = async ({ port, engine, functionId, noVariables, noReload } = {}) => {
281+
const runFunction = async ({ port, engine, functionId, noVariables, noReload, userId } = {}) => {
219282
// Selection
220283
if(!functionId) {
221284
const answers = await inquirer.prompt(questionsRunFunctions[0]);
@@ -265,14 +328,10 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } =
265328
}
266329

267330
if(engine === 'docker') {
268-
log('💡 Hint: Using system is faster, but using Docker simulates the production environment precisely.');
269-
270331
if(!systemHasCommand('docker')) {
271332
return error("Please install Docker first: https://docs.docker.com/engine/install/");
272333
}
273334
} else if(engine === 'system') {
274-
log('💡 Hint: Docker simulates the production environment precisely, but using system is faster');
275-
276335
for(const command of tool.commands) {
277336
if(!systemHasCommand(command.command)) {
278337
return error(`Your system is missing command "${command.command}". Please install it first: ${command.docs}`);
@@ -317,25 +376,45 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } =
317376
fs.writeFileSync(errorsPath, '');
318377
}
319378

320-
let variables = [];
379+
const variables = {};
321380
if(!noVariables) {
322381
if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') {
323382
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.");
324383
} else {
325-
const { variables: remoteVariables } = await paginate(functionsListVariables, {
326-
functionId: func['$id'],
327-
parseOutput: false
328-
}, 100, 'variables');
329-
330-
remoteVariables.forEach((v) => {
331-
variables.push({
332-
key: v.key,
333-
value: v.value
384+
try {
385+
const { variables: remoteVariables } = await paginate(functionsListVariables, {
386+
functionId: func['$id'],
387+
parseOutput: false
388+
}, 100, 'variables');
389+
390+
remoteVariables.forEach((v) => {
391+
variables[v.key] = v.value;
334392
});
335-
});
393+
} catch(err) {
394+
error("Could not fetch remote variables: " + err.message);
395+
error("Function will run locally, but will not have your function's environment variables set.");
396+
}
336397
}
337398
}
338399

400+
variables['APPWRITE_FUNCTION_API_ENDPOINT'] = globalConfig.getFrom('endpoint');
401+
variables['APPWRITE_FUNCTION_ID'] = func.$id;
402+
variables['APPWRITE_FUNCTION_NAME'] = func.name;
403+
variables['APPWRITE_FUNCTION_DEPLOYMENT'] = ''; // TODO: Implement when relevant
404+
variables['APPWRITE_FUNCTION_PROJECT_ID'] = localConfig.getProject().projectId;
405+
variables['APPWRITE_FUNCTION_RUNTIME_NAME'] = ''; // TODO: Implement when relevant
406+
variables['APPWRITE_FUNCTION_RUNTIME_VERSION'] = ''; // TODO: Implement when relevant
407+
408+
await JwtManager.setup(userId);
409+
410+
const headers = {};
411+
headers['x-appwrite-key'] = JwtManager.functionJwt ?? '';
412+
headers['x-appwrite-trigger'] = 'http';
413+
headers['x-appwrite-event'] = '';
414+
headers['x-appwrite-user-id'] = userId ?? '';
415+
headers['x-appwrite-user-jwt'] = JwtManager.userJwt ?? '';
416+
variables['OPEN_RUNTIMES_HEADERS'] = JSON.stringify(headers);
417+
339418
await dockerPull(func);
340419
await dockerBuild(func, variables);
341420
await dockerStart(func, variables, port);
@@ -348,6 +427,7 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } =
348427
});
349428

350429
if(!noReload) {
430+
// TODO: Stop previous job mid-way if new deployment is ready, I think?
351431
chokidar.watch('.', {
352432
cwd: path.join(process.cwd(), func.path),
353433
ignoreInitial: true,
@@ -460,6 +540,7 @@ run
460540
.option(`--functionId <functionId>`, `Function ID`)
461541
.option(`--port <port>`, `Local port`)
462542
.option(`--engine <engine>`, `Local engine, "system" or "docker"`)
543+
.option(`--userId <userId>`, `ID of user to impersonate`)
463544
.option(`--noVariables`, `Prevent pulling variables from function settings`)
464545
.option(`--noReload`, `Prevent live reloading of server when changes are made to function files`)
465546
.action(actionRunner(runFunction));

templates/cli/lib/config.js.twig

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ class Global extends Config {
419419
return this.get(Global.PREFERENCE_CURRENT);
420420
}
421421

422-
setCurrentLogin(endpoint) {
422+
setCurrentLogin(id) {
423423
this.set(Global.PREFERENCE_CURRENT, endpoint);
424424
}
425425

@@ -516,7 +516,6 @@ class Global extends Config {
516516
this.setTo(Global.PREFERENCE_KEY, key);
517517
}
518518

519-
520519
hasFrom(key) {
521520
const current = this.getCurrentLogin();
522521

templates/cli/lib/questions.js.twig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -820,11 +820,11 @@ const questionsRunFunctions = [
820820
message: "Which engine would you like to use?",
821821
choices: [
822822
{
823-
name: "Docker",
824-
value: "docker",
823+
name: "Docker (recommended, simulates production precisely)",
824+
value: "docker",
825825
},
826826
{
827-
name: "System",
827+
name: "System (faster and easier to debug)",
828828
value: "system",
829829
},
830830
],

0 commit comments

Comments
 (0)