Skip to content

Commit 8838d5b

Browse files
prefanatickevmoo
authored andcommitted
feat(gcp): current region methods
1 parent 6ebc248 commit 8838d5b

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed

gcp/lib/gcp.dart

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export 'src/gcp_project.dart'
2222
gcpProjectIdEnvironmentVariables,
2323
projectIdFromEnvironment,
2424
projectIdFromMetadataServer;
25+
export 'src/gpc_region.dart'
26+
show
27+
computeRegion,
28+
gcpRegionEnvironmentVariables,
29+
regionFromEnvironment,
30+
regionFromMetadataServer;
2531
export 'src/log_severity.dart' show LogSeverity, RequestLogger;
2632
export 'src/logging.dart'
2733
show

gcp/lib/src/gpc_region.dart

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
/// A convenience wrapper that first tries [regionFromEnvironment]
22+
/// then (if the value is `null`) tries [regionFromMetadataServer]
23+
///
24+
/// Like [regionFromMetadataServer], if no value is found, a
25+
/// [BadConfigurationException] is thrown.
26+
Future<String> computeRegion() async {
27+
final localValue = regionFromEnvironment();
28+
if (localValue != null) {
29+
return localValue;
30+
}
31+
final result = await regionFromMetadataServer();
32+
33+
return result;
34+
}
35+
36+
/// Returns the
37+
/// [Region](https://cloud.google.com/compute/docs/regions-zones#identifying_a_region_or_zone)
38+
/// for the current instance by checking the environment variables
39+
/// in [gcpRegionEnvironmentVariables].
40+
///
41+
/// The list is checked in order. This is useful for local development.
42+
///
43+
/// If no matching variable is found, `null` is returned.
44+
String? regionFromEnvironment() {
45+
for (var envKey in gcpRegionEnvironmentVariables) {
46+
final value = Platform.environment[envKey];
47+
if (value != null) return value;
48+
}
49+
50+
return null;
51+
}
52+
53+
/// Returns a [Future] that completes with the
54+
/// [Region](https://cloud.google.com/compute/docs/regions-zones#identifying_a_region_or_zone)
55+
/// for the current instance by checking
56+
/// [instance metadata](https://cloud.google.com/compute/docs/metadata/default-metadata-values#vm_instance_metadata).
57+
///
58+
/// If the metadata server cannot be contacted, a [BadConfigurationException] is
59+
/// thrown.
60+
Future<String> regionFromMetadataServer() async {
61+
const host = 'http://metadata.google.internal/';
62+
final url = Uri.parse('$host/computeMetadata/v1/instance/region');
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 region:
84+
${gcpRegionEnvironmentVariables.join('\n')}
85+
''',
86+
details: e.toString(),
87+
);
88+
}
89+
}
90+
91+
/// A set of typical environment variables that are likely to represent the
92+
/// current Google Cloud instance region.
93+
///
94+
/// For context, see:
95+
/// * https://cloud.google.com/functions/docs/env-var
96+
/// * https://cloud.google.com/compute/docs/gcloud-compute#default_project
97+
/// * https://github.com/GoogleContainerTools/gcp-auth-webhook/blob/08136ca171fe5713cc70ef822c911fbd3a1707f5/server.go#L38-L44
98+
///
99+
/// Note: these are ordered starting from the most current/canonical to least.
100+
/// (At least as could be determined at the time of writing.)
101+
const gcpRegionEnvironmentVariables = {
102+
'FUNCTION_REGION',
103+
'CLOUDSDK_COMPUTE_REGION',
104+
};

gcp/test/gcp_test.dart

+32
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,38 @@ 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(gcpRegionEnvironmentVariables),
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: {gcpRegionEnvironmentVariables.first: 'us-central1'},
156+
);
157+
158+
await expectLater(proc.stdout, emits('us-central1'));
159+
await expectLater(proc.stderr, emitsDone);
160+
161+
await proc.shouldExit(0);
162+
});
163+
164+
// TODO: worth emulating the metadata server?
165+
});
134166
}
135167

136168
Future<TestProcess> _run(

gcp/test/src/region_print.dart

+18
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 computeRegion());
18+
}

0 commit comments

Comments
 (0)