Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support --env option for the runtime build #1090

Merged
merged 1 commit into from
Jan 22, 2025
Merged
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
169 changes: 169 additions & 0 deletions integration-tests/cli/env.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import test from 'brittle';
import { EnvParser } from '../../src/env.js';

test('EnvParser should parse single key-value pair', function (t) {
const parser = new EnvParser();
parser.parse('NODE_ENV=production');

t.alike(parser.getEnv(), {
NODE_ENV: 'production',
});
});

test('EnvParser should parse multiple comma-separated values', function (t) {
const parser = new EnvParser();
parser.parse('NODE_ENV=production,DEBUG=true,PORT=3000');

t.alike(parser.getEnv(), {
NODE_ENV: 'production',
DEBUG: 'true',
PORT: '3000',
});
});

test('EnvParser should merge multiple parse calls', function (t) {
const parser = new EnvParser();
parser.parse('NODE_ENV=production');
parser.parse('DEBUG=true');

t.alike(parser.getEnv(), {
NODE_ENV: 'production',
DEBUG: 'true',
});
});

test('EnvParser should inherit existing environment variables', function (t) {
const parser = new EnvParser();

// Set up some test environment variables
process.env.TEST_VAR1 = 'value1';
process.env.TEST_VAR2 = 'value2';

parser.parse('TEST_VAR1,TEST_VAR2');

t.alike(parser.getEnv(), {
TEST_VAR1: 'value1',
TEST_VAR2: 'value2',
});

// Cleanup
delete process.env.TEST_VAR1;
delete process.env.TEST_VAR2;
});

test('EnvParser should handle mixed inheritance and setting', function (t) {
const parser = new EnvParser();

process.env.TEST_VAR = 'inherited';

parser.parse('TEST_VAR,NEW_VAR=set');

t.alike(parser.getEnv(), {
TEST_VAR: 'inherited',
NEW_VAR: 'set',
});

// Cleanup
delete process.env.TEST_VAR;
});

test('EnvParser should handle values with spaces', function (t) {
const parser = new EnvParser();
parser.parse('MESSAGE=Hello World');

t.alike(parser.getEnv(), {
MESSAGE: 'Hello World',
});
});

test('EnvParser should handle values with equals signs', function (t) {
const parser = new EnvParser();
parser.parse('DATABASE_URL=postgres://user:pass@localhost:5432/db');

t.alike(parser.getEnv(), {
DATABASE_URL: 'postgres://user:pass@localhost:5432/db',
});
});

test('EnvParser should handle empty values', function (t) {
const parser = new EnvParser();
parser.parse('EMPTY=');

t.alike(parser.getEnv(), {
EMPTY: '',
});
});

test('EnvParser should handle whitespace', function (t) {
const parser = new EnvParser();
parser.parse(' KEY = value with spaces ');

t.alike(parser.getEnv(), {
KEY: ' value with spaces', // Leading whitespace preserved, trailing removed
});

// Test multiple values with whitespace
parser.parse(' KEY2 = value2 , KEY3 = value3 ');

t.alike(parser.getEnv(), {
KEY: ' value with spaces',
KEY2: ' value2',
KEY3: ' value3',
});
});

test('EnvParser should merge and override values', function (t) {
const parser = new EnvParser();
parser.parse('KEY=first');
parser.parse('KEY=second');

t.alike(parser.getEnv(), {
KEY: 'second',
});
});

test('EnvParser should throw on missing equal sign', function (t) {
const parser = new EnvParser();

t.exception(
() => parser.parse('INVALID_FORMAT'),
'Invalid environment variable format: INVALID_FORMAT\nMust be in format KEY=VALUE',
);
});

test('EnvParser should throw on empty key', function (t) {
const parser = new EnvParser();

t.exception(
() => parser.parse('=value'),
'Invalid environment variable format: =value\nMust be in format KEY=VALUE',
);
});

test('EnvParser should handle empty constructor', function (t) {
const parser = new EnvParser();

t.alike(parser.getEnv(), {});
});

test('EnvParser should handle multiple commas and whitespace', function (t) {
const parser = new EnvParser();
parser.parse('KEY1=value1, KEY2=value2,,,KEY3=value3');

t.alike(parser.getEnv(), {
KEY1: 'value1',
KEY2: 'value2',
KEY3: 'value3',
});
});

test('EnvParser should handle values containing escaped characters', function (t) {
const parser = new EnvParser();

// This is how Node.js argv will receive it after shell processing
parser.parse('A=VERBATIM CONTENTS\\, GO HERE'); // Users will type: --env 'A=VERBATIM CONTENTS\, GO HERE'

t.alike(parser.getEnv(), {
A: 'VERBATIM CONTENTS, GO HERE', // Comma should be unescaped in final value
});
});
72 changes: 0 additions & 72 deletions integration-tests/cli/help.test.js

This file was deleted.

2 changes: 1 addition & 1 deletion integration-tests/js-compute/fixtures/app/fastly.toml.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ name = "js-test-app"
service_id = ""

[scripts]
build = "node ../../../../js-compute-runtime-cli.js --enable-experimental-high-resolution-time-methods src/index.js"
build = "node ../../../../js-compute-runtime-cli.js --env LOCAL_TEST,TEST=\"foo\" --enable-experimental-high-resolution-time-methods src/index.js"

[local_server]

Expand Down
18 changes: 16 additions & 2 deletions integration-tests/js-compute/fixtures/app/src/env.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
/* eslint-env serviceworker */
import { env } from 'fastly:env';
import { routes, isRunningLocally } from './routes.js';
import { assert } from './assertions.js';
import { strictEqual } from './assertions.js';

// hostname didn't exist at initialization, so can still be captured at runtime
const wizerHostname = env('FASTLY_HOSTNAME');
const wizerLocal = env('LOCAL_TEST');

routes.set('/env', () => {
strictEqual(wizerHostname, undefined);

if (isRunningLocally()) {
assert(
strictEqual(
env('FASTLY_HOSTNAME'),
'localhost',
`env("FASTLY_HOSTNAME") === "localhost"`,
);
} else {
strictEqual(env('FASTLY_HOSTNAME'), undefined);
}

strictEqual(wizerLocal, 'local val');

// at runtime these remain captured from Wizer time, even if we didn't call env
strictEqual(env('LOCAL_TEST'), 'local val');
strictEqual(env('TEST'), 'foo');
});
3 changes: 3 additions & 0 deletions integration-tests/js-compute/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { copyFile, readFile, writeFile } from 'node:fs/promises';
import core from '@actions/core';
import TOML from '@iarna/toml';

// test environment variable handling
process.env.LOCAL_TEST = 'local val';

async function killPortProcess(port) {
zx.verbose = false;
const pids = (await zx`lsof -ti:${port}`).stdout;
Expand Down
2 changes: 2 additions & 0 deletions js-compute-runtime-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
output,
version,
help,
env,
} = await parseInputs(process.argv.slice(2));

if (version) {
Expand All @@ -41,6 +42,7 @@ if (version) {
aotCache,
moduleMode,
bundle,
env,
);
await addSdkMetadataField(output, enableAOT);
}
55 changes: 50 additions & 5 deletions runtime/fastly/builtins/fastly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ using fastly::fetch::RequestOrResponse;
using fastly::fetch::Response;
using fastly::logger::Logger;

extern char **environ;

namespace {

bool DEBUG_LOGGING_ENABLED = false;

api::Engine *ENGINE;

// Global storage for Wizer-time environment
std::unordered_map<std::string, std::string> initialized_env;

static void oom_callback(JSContext *cx, void *data) {
fprintf(stderr, "Critical Error: out of memory\n");
fflush(stderr);
Expand Down Expand Up @@ -319,15 +324,42 @@ bool Env::env_get(JSContext *cx, unsigned argc, JS::Value *vp) {
if (!args.requireAtLeast(cx, "fastly.env.get", 1))
return false;

auto var_name_chars = core::encode(cx, args[0]);
if (!var_name_chars) {
JS::RootedString str(cx, JS::ToString(cx, args[0]));
if (!str) {
return false;
}
JS::RootedString env_var(cx, JS_NewStringCopyZ(cx, getenv(var_name_chars.begin())));
if (!env_var)

JS::UniqueChars ptr = JS_EncodeStringToUTF8(cx, str);
if (!ptr) {
return false;
}

// This shouldn't fail, since the encode operation ensured `str` is linear.
JSLinearString *linear = JS_EnsureLinearString(cx, str);
uint32_t len = JS::GetDeflatedUTF8StringLength(linear);

std::string key_str(ptr.get(), len);

// First check initialized environment
if (auto it = initialized_env.find(key_str); it != initialized_env.end()) {
JS::RootedString env_var(cx, JS_NewStringCopyN(cx, it->second.data(), it->second.size()));
if (!env_var)
return false;
args.rval().setString(env_var);
return true;
}

args.rval().setString(env_var);
// Fallback to getenv with caching
if (const char *value = std::getenv(key_str.c_str())) {
auto [it, _] = initialized_env.emplace(key_str, value);
JS::RootedString env_var(cx, JS_NewStringCopyN(cx, it->second.data(), it->second.size()));
if (!env_var)
return false;
args.rval().setString(env_var);
return true;
}

args.rval().setUndefined();
return true;
}

Expand Down Expand Up @@ -475,6 +507,19 @@ bool install(api::Engine *engine) {
}

// fastly:env
// first, store the initialized environment vars from Wizer
initialized_env.clear();

for (char **env = environ; *env; env++) {
const char *entry = *env;
const char *eq = entry;
while (*eq && *eq != '=')
eq++;

if (*eq == '=') {
initialized_env.emplace(std::string(entry, eq - entry), std::string(eq + 1));
}
}
RootedValue env_get(engine->cx());
if (!JS_GetProperty(engine->cx(), Fastly::env, "get", &env_get)) {
return false;
Expand Down
Loading
Loading