Skip to content

Commit e8dc594

Browse files
prefanatickevmoo
authored andcommitted
feat(gcp): current region methods
1 parent dd6bd8e commit e8dc594

File tree

5 files changed

+199
-57
lines changed

5 files changed

+199
-57
lines changed

gcp/lib/gcp.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ export 'src/logging.dart'
2929
cloudLoggingMiddleware,
3030
createLoggingMiddleware,
3131
currentLogger;
32+
export 'src/metadata.dart' show MetadataValue;
3233
export 'src/serve.dart' show listenPort, serveHandler;
3334
export 'src/terminate.dart' show waitForTerminate;

gcp/lib/src/gcp_project.dart

Lines changed: 8 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,16 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import 'dart:io';
16-
17-
import 'package:http/http.dart' as http;
18-
1915
import 'bad_configuration_exception.dart';
16+
import 'metadata.dart';
2017

2118
/// A convenience wrapper that first tries [projectIdFromEnvironment]
2219
/// then (if the value is `null`) tries [projectIdFromMetadataServer]
2320
///
2421
/// Like [projectIdFromMetadataServer], if no value is found, a
2522
/// [BadConfigurationException] is thrown.
26-
Future<String> computeProjectId() async {
27-
final localValue = projectIdFromEnvironment();
28-
if (localValue != null) {
29-
return localValue;
30-
}
31-
final result = await projectIdFromMetadataServer();
32-
33-
return result;
34-
}
23+
Future<String> computeProjectId() =>
24+
MetadataValue.project.fromEnvironmentOrMetadata();
3525

3626
/// Returns the
3727
/// [Project ID](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects)
@@ -41,14 +31,7 @@ Future<String> computeProjectId() async {
4131
/// The list is checked in order. This is useful for local development.
4232
///
4333
/// If no matching variable is found, `null` is returned.
44-
String? projectIdFromEnvironment() {
45-
for (var envKey in gcpProjectIdEnvironmentVariables) {
46-
final value = Platform.environment[envKey];
47-
if (value != null) return value;
48-
}
49-
50-
return null;
51-
}
34+
String? projectIdFromEnvironment() => MetadataValue.project.fromEnvironment();
5235

5336
/// Returns a [Future] that completes with the
5437
/// [Project ID](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects)
@@ -57,36 +40,8 @@ String? projectIdFromEnvironment() {
5740
///
5841
/// If the metadata server cannot be contacted, a [BadConfigurationException] is
5942
/// thrown.
60-
Future<String> projectIdFromMetadataServer() async {
61-
const host = 'http://metadata.google.internal/';
62-
final url = Uri.parse('$host/computeMetadata/v1/project/project-id');
63-
64-
try {
65-
final response = await http.get(
66-
url,
67-
headers: {'Metadata-Flavor': 'Google'},
68-
);
69-
70-
if (response.statusCode != 200) {
71-
throw HttpException(
72-
'${response.body} (${response.statusCode})',
73-
uri: url,
74-
);
75-
}
76-
77-
return response.body;
78-
} on SocketException catch (e) {
79-
throw BadConfigurationException(
80-
'''
81-
Could not connect to $host.
82-
If not running on Google Cloud, one of these environment variables must be set
83-
to the target Google Project ID:
84-
${gcpProjectIdEnvironmentVariables.join('\n')}
85-
''',
86-
details: e.toString(),
87-
);
88-
}
89-
}
43+
Future<String> projectIdFromMetadataServer() =>
44+
MetadataValue.project.fromMetadataServer();
9045

9146
/// A set of typical environment variables that are likely to represent the
9247
/// current Google Cloud project ID.
@@ -98,9 +53,5 @@ ${gcpProjectIdEnvironmentVariables.join('\n')}
9853
///
9954
/// Note: these are ordered starting from the most current/canonical to least.
10055
/// (At least as could be determined at the time of writing.)
101-
const gcpProjectIdEnvironmentVariables = {
102-
'GCP_PROJECT',
103-
'GCLOUD_PROJECT',
104-
'CLOUDSDK_CORE_PROJECT',
105-
'GOOGLE_CLOUD_PROJECT',
106-
};
56+
Set<String> get gcpProjectIdEnvironmentVariables =>
57+
MetadataValue.project.environmentValues;

gcp/lib/src/metadata.dart

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:io';
16+
17+
import 'package:http/http.dart' as http;
18+
19+
import 'bad_configuration_exception.dart';
20+
21+
enum MetadataValue {
22+
/// A set of typical environment variables that are likely to represent the
23+
/// current Google Cloud instance region.
24+
///
25+
/// For context, see:
26+
/// * https://cloud.google.com/functions/docs/env-var
27+
/// * https://cloud.google.com/compute/docs/gcloud-compute#default_project
28+
/// * https://github.com/GoogleContainerTools/gcp-auth-webhook/blob/08136ca171fe5713cc70ef822c911fbd3a1707f5/server.go#L38-L44
29+
///
30+
/// Note: these are ordered starting from the most current/canonical to least.
31+
/// (At least as could be determined at the time of writing.)
32+
project(
33+
path: 'project/project-id',
34+
environmentValues: {
35+
'GCP_PROJECT',
36+
'GCLOUD_PROJECT',
37+
'CLOUDSDK_CORE_PROJECT',
38+
'GOOGLE_CLOUD_PROJECT',
39+
},
40+
),
41+
42+
/// A set of typical environment variables that are likely to represent the
43+
/// current Google Cloud instance region.
44+
///
45+
/// For context, see:
46+
/// * https://cloud.google.com/functions/docs/env-var
47+
/// * https://cloud.google.com/compute/docs/gcloud-compute#default_project
48+
/// * https://github.com/GoogleContainerTools/gcp-auth-webhook/blob/08136ca171fe5713cc70ef822c911fbd3a1707f5/server.go#L38-L44
49+
///
50+
/// Note: these are ordered starting from the most current/canonical to least.
51+
/// (At least as could be determined at the time of writing.)
52+
region(
53+
path: 'instance/region',
54+
environmentValues: {
55+
'FUNCTION_REGION',
56+
'CLOUDSDK_COMPUTE_REGION',
57+
},
58+
);
59+
60+
const MetadataValue({
61+
required this.path,
62+
required this.environmentValues,
63+
});
64+
65+
final String path;
66+
67+
final Set<String> environmentValues;
68+
69+
/// A convenience wrapper that first tries [fromEnvironment]
70+
/// then (if the value is `null`) tries [fromMetadataServer]
71+
///
72+
/// Like [fromMetadataServer], if no value is found, a
73+
/// [BadConfigurationException] is thrown.
74+
Future<String> fromEnvironmentOrMetadata() async {
75+
final localValue = fromEnvironment();
76+
if (localValue != null) {
77+
return localValue;
78+
}
79+
final result = await fromMetadataServer();
80+
81+
return result;
82+
}
83+
84+
/// Returns the
85+
/// [Region](https://cloud.google.com/compute/docs/regions-zones#identifying_a_region_or_zone)
86+
/// for the current instance by checking the environment variables in
87+
/// [environmentValues].
88+
///
89+
/// The list is checked in order. This is useful for local development.
90+
///
91+
/// If no matching variable is found, `null` is returned.
92+
String? fromEnvironment() {
93+
for (var envKey in environmentValues) {
94+
final value = Platform.environment[envKey];
95+
if (value != null) return value;
96+
}
97+
98+
return null;
99+
}
100+
101+
/// Returns a [Future] that completes with the
102+
/// [Region](https://cloud.google.com/compute/docs/regions-zones#identifying_a_region_or_zone)
103+
/// for the current instance by checking
104+
/// [instance metadata](https://cloud.google.com/compute/docs/metadata/default-metadata-values#vm_instance_metadata).
105+
///
106+
/// If the metadata server cannot be contacted, a [BadConfigurationException]
107+
/// is thrown.
108+
Future<String> fromMetadataServer() async {
109+
const host = 'http://metadata.google.internal/';
110+
final url = Uri.parse('$host/computeMetadata/v1/$path');
111+
112+
try {
113+
final response = await http.get(
114+
url,
115+
headers: {'Metadata-Flavor': 'Google'},
116+
);
117+
118+
if (response.statusCode != 200) {
119+
throw HttpException(
120+
'${response.body} (${response.statusCode})',
121+
uri: url,
122+
);
123+
}
124+
125+
return response.body;
126+
} on SocketException catch (e) {
127+
throw BadConfigurationException(
128+
'''
129+
Could not connect to $host.
130+
If not running on Google Cloud, one of these environment variables must be set
131+
to the target region:
132+
${environmentValues.join('\n')}
133+
''',
134+
details: e.toString(),
135+
);
136+
}
137+
}
138+
}

gcp/test/gcp_test.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,40 @@ void main() {
131131
});
132132
},
133133
);
134+
135+
group('currentRegion', () {
136+
const regionPrint = 'test/src/region_print.dart';
137+
138+
test('not environment', () async {
139+
final proc = await _run(regionPrint);
140+
141+
final errorOut = await proc.stderrStream().toList();
142+
143+
await expectLater(
144+
errorOut,
145+
containsAll(MetadataValue.region.environmentValues),
146+
);
147+
await expectLater(proc.stdout, emitsDone);
148+
149+
await proc.shouldExit(255);
150+
});
151+
152+
test('environment set', () async {
153+
final proc = await _run(
154+
regionPrint,
155+
environment: {
156+
MetadataValue.region.environmentValues.first: 'us-central1',
157+
},
158+
);
159+
160+
await expectLater(proc.stdout, emits('us-central1'));
161+
await expectLater(proc.stderr, emitsDone);
162+
163+
await proc.shouldExit(0);
164+
});
165+
166+
// TODO: worth emulating the metadata server?
167+
});
134168
}
135169

136170
Future<TestProcess> _run(

gcp/test/src/region_print.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
import 'package:gcp/gcp.dart';
15+
16+
Future<void> main() async {
17+
print(await MetadataValue.region.fromEnvironmentOrMetadata());
18+
}

0 commit comments

Comments
 (0)