Skip to content

Commit 390f145

Browse files
authored
Parameterized configuration with environment variables (#5577)
Load configuration from `app/config/<projectId>.yaml`.
1 parent 72f1d31 commit 390f145

File tree

6 files changed

+41
-145
lines changed

6 files changed

+41
-145
lines changed

.gitallowed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
app/lib/shared/configuration.dart:.*\.apps\.googleusercontent.com
2-
app/lib/shared/configuration.dart:.*\.apps\.googleusercontent.com
32
pkg/pub_integration/lib/src/pub_tool_client.dart:.*\.apps\.googleusercontent.com
3+
app/config/.*.yaml:.*\.apps\.googleusercontent.com

app/config/staging-config.yaml renamed to app/config/dartlang-pub-dev.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
2-
packageBucketName: dartlang-pub-dev--pub-packages
2+
# Configuration for the pub.dev staging site
33
projectId: dartlang-pub-dev
4+
packageBucketName: dartlang-pub-dev--pub-packages
45
searchServicePrefix: https://search-dot-dartlang-pub-dev.appspot.com
56
dartdocStorageBucketName: dartlang-pub-dev--dartdoc-storage
67
popularityDumpBucketName: dartlang-pub-dev--popularity
File renamed without changes.

app/lib/shared/configuration.dart

Lines changed: 37 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import 'package:collection/collection.dart' show UnmodifiableSetView;
99
import 'package:gcloud/service_scope.dart' as ss;
1010
import 'package:json_annotation/json_annotation.dart';
1111
import 'package:meta/meta.dart';
12+
import 'package:path/path.dart' as path;
13+
import 'package:pub_dev/frontend/static_files.dart';
1214
import 'package:yaml/yaml.dart';
1315

14-
import 'env_config.dart';
15-
1616
part 'configuration.g.dart';
1717

1818
final _configurationKey = #_active_configuration;
@@ -21,7 +21,7 @@ final _configurationKey = #_active_configuration;
2121
Configuration get activeConfiguration {
2222
Configuration? config = ss.lookup(_configurationKey) as Configuration?;
2323
if (config == null) {
24-
config = Configuration.fromEnv(envConfig);
24+
config = Configuration.fromEnv();
2525
ss.register(_configurationKey, config);
2626
}
2727
return config;
@@ -32,10 +32,6 @@ void registerActiveConfiguration(Configuration configuration) {
3232
ss.register(_configurationKey, configuration);
3333
}
3434

35-
/// The OAuth audience (`client_id`) that the `pub` client uses.
36-
const _pubClientAudience =
37-
'818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com';
38-
3935
/// Special value to indicate that the site is running in fake mode, and the
4036
/// client side authentication should use the fake authentication tokens.
4137
const _fakeSiteAudience = 'fake-site-audience';
@@ -45,6 +41,7 @@ const _fakeSiteAudience = 'fake-site-audience';
4541
/// The configuration define the location of the Datastore with the
4642
/// package metadata and the Cloud Storage bucket for the actual package
4743
/// tar files.
44+
@sealed
4845
@JsonSerializable(
4946
anyMap: true,
5047
explicitToJson: true,
@@ -143,116 +140,22 @@ class Configuration {
143140
/// The identifier of admins.
144141
final List<AdminId>? admins;
145142

143+
/// Load [Configuration] from YAML file at [path] substituting `{{ENV}}` for
144+
/// the value of environment variable `ENV`.
146145
factory Configuration.fromYamlFile(final String path) {
147-
final file = File(path);
148-
final content = file.readAsStringSync();
149-
final map =
150-
json.decode(json.encode(loadYaml(content))) as Map<String, dynamic>;
151-
return Configuration.fromJson(map);
152-
}
153-
154-
/// Create a configuration for production deployment.
155-
///
156-
/// This will use the Datastore from the cloud project and the Cloud Storage
157-
/// bucket 'pub-packages'. The credentials for accessing the Cloud
158-
/// Storage is retrieved from the Datastore.
159-
@visibleForTesting
160-
factory Configuration.prod() {
161-
final projectId = 'dartlang-pub';
162-
return Configuration(
163-
projectId: projectId,
164-
packageBucketName: 'pub-packages',
165-
imageBucketName: '$projectId--pub-images',
166-
dartdocStorageBucketName: '$projectId--dartdoc-storage',
167-
popularityDumpBucketName: '$projectId--popularity',
168-
searchSnapshotBucketName: '$projectId--search-snapshot',
169-
searchServicePrefix: 'https://search-dot-$projectId.appspot.com',
170-
storageBaseUrl: 'https://storage.googleapis.com/',
171-
pubClientAudience: _pubClientAudience,
172-
pubSiteAudience:
173-
'818368855108-e8skaopm5ih5nbb82vhh66k7ft5o7dn3.apps.googleusercontent.com',
174-
adminAudience: 'https://pub.dev',
175-
gmailRelayServiceAccount:
176-
177-
gmailRelayImpersonatedGSuiteUser: '[email protected]',
178-
uploadSignerServiceAccount:
179-
180-
blockRobots: false,
181-
productionHosts: const ['pub.dartlang.org', 'pub.dev', 'api.pub.dev'],
182-
primaryApiUri: Uri.parse('https://pub.dartlang.org/'),
183-
primarySiteUri: Uri.parse('https://pub.dev/'),
184-
admins: [
185-
AdminId(
186-
187-
oauthUserId: '106306194842560376600',
188-
permissions: {AdminPermission.manageAssignedTags},
189-
),
190-
AdminId(
191-
192-
oauthUserId: '114536496314409930448',
193-
permissions: {
194-
AdminPermission.listUsers,
195-
AdminPermission.managePackageOwnership,
196-
AdminPermission.removeUsers,
197-
},
198-
),
199-
AdminId(
200-
201-
oauthUserId: '108693445730271975989',
202-
permissions: AdminPermission.values,
203-
)
204-
],
205-
);
206-
}
207-
208-
/// Create a configuration for development/staging deployment.
209-
@visibleForTesting
210-
factory Configuration.staging() {
211-
final projectId = 'dartlang-pub-dev';
212-
return Configuration(
213-
projectId: projectId,
214-
packageBucketName: '$projectId--pub-packages',
215-
imageBucketName: '$projectId--pub-images',
216-
dartdocStorageBucketName: '$projectId--dartdoc-storage',
217-
popularityDumpBucketName: '$projectId--popularity',
218-
searchSnapshotBucketName: '$projectId--search-snapshot',
219-
// TODO: Support finding search on localhost when envConfig.isRunningLocally
220-
// is true, this also requires running search on localhost.
221-
searchServicePrefix: 'https://search-dot-$projectId.appspot.com',
222-
storageBaseUrl: 'https://storage.googleapis.com/',
223-
pubClientAudience: _pubClientAudience,
224-
pubSiteAudience:
225-
'621485135717-idb8t8nnguphtu2drfn2u4ig7r56rm6n.apps.googleusercontent.com',
226-
adminAudience: 'https://pub.dev',
227-
gmailRelayServiceAccount: null, // disable email sending
228-
gmailRelayImpersonatedGSuiteUser: null, // disable email sending
229-
uploadSignerServiceAccount:
230-
231-
blockRobots: true,
232-
productionHosts: envConfig.isRunningLocally
233-
? ['localhost']
234-
: [
235-
'dartlang-pub-dev.appspot.com',
236-
'${envConfig.gaeService}-dot-dartlang-pub-dev.appspot.com',
237-
],
238-
primaryApiUri: Uri.parse('https://dartlang-pub-dev.appspot.com'),
239-
primarySiteUri: envConfig.isRunningLocally
240-
? Uri.parse('http://localhost:8080')
241-
: Uri.parse(
242-
'https://${envConfig.gaeVersion}-dot-dartlang-pub-dev.appspot.com',
243-
),
244-
admins: [
245-
AdminId(
246-
oauthUserId: '111042304059633250784',
247-
248-
permissions: AdminPermission.values,
249-
),
250-
AdminId(
251-
oauthUserId: '117672289743137340098',
252-
253-
permissions: {AdminPermission.manageAssignedTags},
254-
)
255-
],
146+
final content = File(path)
147+
.readAsStringSync()
148+
.replaceAllMapped(RegExp(r'\{\{([A-Z]+[A-Z0-9_]*)\}\}'), (match) {
149+
final name = match.group(1);
150+
if (name != null &&
151+
Platform.environment.containsKey(name) &&
152+
Platform.environment[name]!.isNotEmpty) {
153+
return Platform.environment[name]!;
154+
}
155+
return match.group(0)!;
156+
});
157+
return Configuration.fromJson(
158+
json.decode(json.encode(loadYaml(content))) as Map<String, dynamic>,
256159
);
257160
}
258161

@@ -278,21 +181,25 @@ class Configuration {
278181
required this.admins,
279182
});
280183

281-
/// Create a configuration based on the environment variables.
282-
factory Configuration.fromEnv(EnvConfig env) {
283-
if (env.gcloudProject == 'dartlang-pub') {
284-
return Configuration.prod();
285-
} else if (env.gcloudProject == 'dartlang-pub-dev') {
286-
return Configuration.staging();
287-
} else if (env.configPath?.isEmpty ?? true) {
288-
throw Exception(
289-
'Unknown project id: ${env.gcloudProject}. Please setup env var GCLOUD_PROJECT or PUB_SERVER_CONFIG');
290-
} else if (File(env.configPath!).existsSync()) {
291-
return Configuration.fromYamlFile(env.configPath!);
292-
} else {
293-
throw Exception(
294-
'File ${env.configPath} doesnt exist. Please ensure PUB_SERVER_CONFIG env is pointing to the existing config');
184+
/// Load configuration from `app/config/<projectId>.yaml` where `projectId`
185+
/// is the GCP Project ID (specified using `GOOGLE_CLOUD_PROJECT`).
186+
factory Configuration.fromEnv() {
187+
// The GOOGLE_CLOUD_PROJECT is the canonical manner to specify project ID.
188+
// This is undocumented for appengine custom runtime, but documented for the
189+
// other runtimes:
190+
// https://cloud.google.com/appengine/docs/standard/nodejs/runtime
191+
final projectId = Platform.environment['GOOGLE_CLOUD_PROJECT'];
192+
if (projectId == null || projectId.isEmpty) {
193+
throw StateError(
194+
'Environment variable \$GOOGLE_CLOUD_PROJECT must be specified!',
195+
);
196+
}
197+
198+
final configFile = path.join(resolveAppDir(), projectId + '.yaml');
199+
if (!File(configFile).existsSync()) {
200+
throw StateError('Could not find configuration file: "$configFile"');
295201
}
202+
return Configuration.fromYamlFile(configFile);
296203
}
297204

298205
/// Configuration for pkg/fake_pub_server.

app/lib/shared/env_config.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class EnvConfig {
5858
Platform.environment['GAE_SERVICE'],
5959
Platform.environment['GAE_VERSION'],
6060
Platform.environment['GAE_INSTANCE'],
61-
Platform.environment['GCLOUD_PROJECT'],
61+
Platform.environment['GOOGLE_CLOUD_PROJECT'],
6262
Platform.environment['GCLOUD_KEY'],
6363
Platform.environment['TOOL_STABLE_DART_SDK'],
6464
Platform.environment['TOOL_STABLE_FLUTTER_SDK'],

app/test/shared/configuration_test.dart

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,4 @@ void main() {
2020
expect(config.admins![0].permissions.contains(AdminPermission.listUsers),
2121
isTrue);
2222
});
23-
24-
test('Prod config from yaml file', () async {
25-
final config = Configuration.fromYamlFile('config/prod-config.yaml');
26-
final expectedConfig = Configuration.prod();
27-
expect(config.toJson(), expectedConfig.toJson());
28-
});
29-
30-
test('Staging config from yaml file', () async {
31-
final config = Configuration.fromYamlFile('config/staging-config.yaml');
32-
final expectedConfig = Configuration.staging();
33-
expect(config.toJson(), expectedConfig.toJson());
34-
});
3523
}

0 commit comments

Comments
 (0)