-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(runtime): Load project config from environment
Loads the resolved project AST from the platform environment/filesystem at startup. This will be used to configure variables, secrets, auth, and routing without managing many environment variables.
- Loading branch information
Showing
6 changed files
with
244 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import 'package:celest/src/config/config_values.dart'; | ||
import 'package:celest/src/core/context.dart'; | ||
import 'package:celest/src/runtime/http/logging.dart'; | ||
import 'package:celest_ast/celest_ast.dart'; | ||
// ignore: implementation_imports | ||
import 'package:celest_ast/src/proto/celest/ast/v1/resolved_ast.pb.dart' as pb; | ||
import 'package:celest_core/_internal.dart'; | ||
import 'package:logging/logging.dart'; | ||
|
||
Map<String, Object?>? _loadJsonFromEnv(Context rootContext) { | ||
final configJson = rootContext.platform.environment['CELEST_CONFIG_JSON']; | ||
if (configJson == null) { | ||
return null; | ||
} | ||
try { | ||
return JsonUtf8.decodeMap(configJson); | ||
} on FormatException catch (e) { | ||
throw StateError('Failed to parse CELEST_CONFIG_JSON: $e'); | ||
} | ||
} | ||
|
||
Future<Map<String, Object?>?> _loadJsonFromFileSystem( | ||
Context rootContext, | ||
) async { | ||
var configPath = rootContext.platform.environment['CELEST_CONFIG']; | ||
if (configPath == null) { | ||
final script = rootContext.platform.script; | ||
configPath = script.resolve('./celest.json').toFilePath(); | ||
} | ||
if (!rootContext.fileSystem.isFileSync(configPath)) { | ||
return null; | ||
} | ||
|
||
final configFile = rootContext.fileSystem.file(configPath); | ||
final configData = await configFile | ||
.readAsBytes() | ||
.onError((e, _) => throw StateError('Failed to load celest.json: $e')); | ||
|
||
try { | ||
return JsonUtf8.decodeMap(configData); | ||
} on FormatException catch (e) { | ||
throw StateError('Failed to parse celest.json from "$configPath": $e'); | ||
} | ||
} | ||
|
||
/// Configures the environment in which Celest is running. | ||
Future<void> configure() async { | ||
configureLogging(); | ||
|
||
final rootContext = Context.root; | ||
|
||
final configJson = _loadJsonFromEnv(rootContext) ?? | ||
await _loadJsonFromFileSystem(rootContext); | ||
if (configJson == null) { | ||
throw StateError( | ||
'No project configuration found. Create a celest.json file and set ' | ||
'CELEST_CONFIG with its path or CELEST_CONFIG_JSON with its contents.', | ||
); | ||
} | ||
|
||
final configPb = pb.ResolvedProject()..mergeFromProto3Json(configJson); | ||
final config = ResolvedProject.fromProto(configPb); | ||
Logger.root | ||
..config('Loaded project configuration') | ||
..config(config); | ||
|
||
rootContext.put(ContextKey.project, config); | ||
|
||
for (final ResolvedVariable(:name, :value) in config.variables) { | ||
rootContext.put(env(name), value); | ||
} | ||
for (final ResolvedSecret(:name, :value) in config.secrets) { | ||
rootContext.put(secret(name), value); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
@TestOn('vm') | ||
library; | ||
|
||
import 'dart:convert'; | ||
|
||
import 'package:celest/src/config/config_values.dart'; | ||
import 'package:celest/src/core/context.dart'; | ||
import 'package:celest/src/runtime/configuration.dart'; | ||
import 'package:celest_ast/celest_ast.dart'; | ||
import 'package:file/memory.dart'; | ||
import 'package:platform/platform.dart'; | ||
import 'package:pub_semver/pub_semver.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
final testProject = ResolvedProject( | ||
projectId: 'test', | ||
environmentId: 'local', | ||
sdkConfig: SdkConfiguration( | ||
celest: Version.parse('1.0.0'), | ||
dart: Sdk( | ||
type: SdkType.dart, | ||
version: Version.parse('3.5.0'), | ||
enabledExperiments: ['native-assets'], | ||
), | ||
targetSdk: SdkType.dart, | ||
featureFlags: [], | ||
), | ||
apis: { | ||
'greeting': ResolvedApi( | ||
apiId: 'greeting', | ||
functions: { | ||
'hello-world': ResolvedCloudFunction( | ||
apiId: 'greeting', | ||
functionId: 'hello-world', | ||
variables: ['HELLO_WORLD'], | ||
secrets: ['HELLO_WORLD_SECRET'], | ||
streamConfig: null, | ||
httpConfig: ResolvedHttpConfig( | ||
method: 'POST', | ||
route: ResolvedHttpRoute( | ||
path: '/greeting/hello-world', | ||
), | ||
status: 200, | ||
statusMappings: {}, | ||
), | ||
), | ||
}, | ||
), | ||
}, | ||
variables: [ | ||
ResolvedVariable( | ||
name: 'HELLO_WORLD', | ||
value: 'hello', | ||
), | ||
], | ||
secrets: [ | ||
ResolvedSecret( | ||
name: 'HELLO_WORLD_SECRET', | ||
value: 'world', | ||
), | ||
], | ||
); | ||
|
||
void main() { | ||
group('configure', () { | ||
test('loads ./celest.json', () async { | ||
final platform = FakePlatform( | ||
environment: {}, | ||
script: Uri.parse('file:///app/test.aot'), | ||
); | ||
final fileSystem = MemoryFileSystem.test(); | ||
final configFile = fileSystem.file('/app/celest.json'); | ||
configFile.parent.createSync(); | ||
configFile.writeAsStringSync( | ||
jsonEncode(testProject.toProto().toProto3Json()), | ||
); | ||
|
||
context.put(ContextKey.platform, platform); | ||
context.put(ContextKey.fileSystem, fileSystem); | ||
Context.root = context; | ||
|
||
await configure(); | ||
|
||
expect(context.project, equals(testProject)); | ||
expect(context.get(const env('HELLO_WORLD')), equals('hello')); | ||
expect(context.get(const secret('HELLO_WORLD_SECRET')), equals('world')); | ||
}); | ||
|
||
test('loads CELEST_CONFIG', () async { | ||
final platform = FakePlatform( | ||
environment: { | ||
'CELEST_CONFIG': '/config/celest.json', | ||
}, | ||
script: Uri.parse('file:///app/test.aot'), | ||
); | ||
final fileSystem = MemoryFileSystem.test(); | ||
final configFile = fileSystem.file('/config/celest.json'); | ||
configFile.parent.createSync(); | ||
configFile.writeAsStringSync( | ||
jsonEncode(testProject.toProto().toProto3Json()), | ||
); | ||
|
||
context.put(ContextKey.platform, platform); | ||
context.put(ContextKey.fileSystem, fileSystem); | ||
Context.root = context; | ||
|
||
await configure(); | ||
|
||
expect(context.project, equals(testProject)); | ||
expect(context.get(const env('HELLO_WORLD')), equals('hello')); | ||
expect(context.get(const secret('HELLO_WORLD_SECRET')), equals('world')); | ||
}); | ||
|
||
test('loads CELEST_CONFIG_JSON', () async { | ||
final platform = FakePlatform( | ||
environment: { | ||
'CELEST_CONFIG_JSON': jsonEncode( | ||
testProject.toProto().toProto3Json(), | ||
), | ||
}, | ||
script: Uri.parse('file:///app/test.aot'), | ||
); | ||
|
||
context.put(ContextKey.platform, platform); | ||
Context.root = context; | ||
|
||
await configure(); | ||
|
||
expect(context.project, equals(testProject)); | ||
expect(context.get(const env('HELLO_WORLD')), equals('hello')); | ||
expect(context.get(const secret('HELLO_WORLD_SECRET')), equals('world')); | ||
}); | ||
|
||
test('fails hard when missing', () async { | ||
final platform = FakePlatform( | ||
environment: {}, | ||
script: Uri.parse('file:///app/test.aot'), | ||
); | ||
final fileSystem = MemoryFileSystem.test(); | ||
|
||
context.put(ContextKey.platform, platform); | ||
context.put(ContextKey.fileSystem, fileSystem); | ||
Context.root = context; | ||
|
||
await expectLater(configure(), throwsStateError); | ||
}); | ||
}); | ||
} |