Skip to content

Commit 36481d3

Browse files
authored
Merge pull request hashicorp#245 from hashicorp/limit-region-by-providers
Limit region by providers
2 parents 0bcf2fe + 3ae742b commit 36481d3

15 files changed

+317
-184
lines changed

governance/third-generation/aws/aws-functions/aws-functions.sentinel

Lines changed: 103 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "tfconfig-functions" as config
55
import "tfplan/v2" as tfplan
66
import "tfconfig/v2" as tfconfig
77
import "strings"
8+
import "types"
89

910
##### Functions #####
1011

@@ -156,75 +157,91 @@ validate_assumed_roles_with_map = func(roles_map, workspace_name) {
156157
return validated
157158
}
158159

159-
### filter_resources_by_region ###
160-
# Filter resources to those in a specific region using the tfconfig/v2 import.
161-
# The parameter, resources, should be a collection of AWS resources from tfconfig
162-
# The parameter, region, should be given as a string such as "us-east-1"
163-
# This function inspects the provider aliases associated with the resources.
164-
# It attempts to identify the region of the provider aliases in several ways
165-
# including constant values assigned to their `region` argument and resolution
166-
# of references to variables. It even tries to match provider aliases in proxy
167-
# configuration blocks (which do not specify regions) of child modules to
168-
# similarly-named provider aliases in the root module.
169-
# However, if the alias passed in the module call does not match the alias
170-
# in the root module, Sentinel has no way of linking the two provider aliases.
171-
# So, while this function does its best to restrict resources to the allowed
172-
# regions, it cannot guarantee 100% success.
173-
filter_resources_by_region = func(resources, region) {
174-
175-
resources_from_region = {}
176-
177-
for resources as address, r {
178-
pck = r.provider_config_key
179-
p = tfconfig.providers[pck]
180-
181-
# First make sure p is not null
182-
if p is not null {
183-
# First, process p itself
184-
if "config" in keys(p) and "region" in keys(p.config) {
185-
if validate_provider_region(p, region) {
186-
resources_from_region[address] = r
187-
}
188-
} else {
189-
# provider used by resource does not have region
190-
# So, we process it as an alias to a root module provider
191-
# A provider that does not have its own region should look like
192-
# <module_address>:<provider>.<alias>
193-
# we want to look at <provider>.<alias>
194-
# However, that might not exist
195-
p_segments = strings.split(p.provider_config_key, ":")
196-
if length(p_segments) == 1 {
197-
# Current provider is already in root module
198-
# So, it is not an alias to another provider
199-
# Continue on to next resource in for loop
200-
continue
160+
### filter_providers_by_regions ###
161+
# Filter instances of the AWS provider to those in a specific region using the
162+
# tfconfig/v2 and tfplan/v2 imports.
163+
# See the comments on the validate_provider_in_allowed_regions() function below
164+
# for details on how this is done.
165+
# The parameter, aws_providers, should be a list of AWS provider aliases
166+
# derived from tfconfig.providers.
167+
# The parameter, allowed_regions, should be given as a list of AWS regions
168+
# such as `["us-east-1" and "eu-west-2"]`.
169+
filter_providers_by_regions = func(aws_providers, allowed_regions) {
170+
171+
# Initialize empty list of validated AWS provider instances
172+
validated_providers = {}
173+
174+
# Process AWS providers
175+
for aws_providers as index, p {
176+
if "config" in keys(p) and "region" in keys(p.config) {
177+
# provider alias has a region
178+
validated = validate_provider_in_allowed_regions(p, allowed_regions)
179+
if validated {
180+
print("validated provider:", index)
181+
validated_providers[index] = p
182+
}
183+
} else {
184+
# provider alias does not have region
185+
# So, we process it as an alias to a root module provider
186+
# A provider that does not have its own region should look like
187+
# <module_address>:<provider>.<alias>
188+
# we want to look at <provider>.<alias>
189+
p_segments = strings.split(p.provider_config_key, ":")
190+
if length(p_segments) == 1 {
191+
# Current provider is already in root module
192+
# So, it is not an alias to another provider
193+
# Continue on to next resource in for loop
194+
continue
195+
}
196+
# Get the provider in root module that current provider aliases
197+
p_alias_name = p_segments[1]
198+
p_alias = tfconfig.providers[p_alias_name] else null
199+
200+
# Process p_alias
201+
if p_alias is not null {
202+
validated =
203+
validate_provider_in_allowed_regions(p_alias, allowed_regions)
204+
if validated {
205+
print("validated provider:", index)
206+
validated_providers[index] = p
201207
}
202-
# Get the provider in root module that current provider aliases
203-
p_alias_name = p_segments[1]
204-
p_alias = tfconfig.providers[p_alias_name]
208+
} // end p_alias not null
209+
} // end else no region
210+
} // end for
205211

206-
# Process p_alias
207-
if p_alias is not null and validate_provider_region(p_alias, region) {
208-
resources_from_region[address] = r
209-
}
210-
} // end processing of p_alias
211-
} // end check p not null
212-
} // end for resources
212+
# return validated providers
213+
return validated_providers
213214

214-
return resources_from_region
215215
}
216216

217-
### validate_provider_region ###
218-
# Validate if a specific provider instance is in a specific region
219-
# using the tfconfig/v2 and tfplan/v2 iimports.
217+
### validate_provider_in_allowed_regions ###
218+
# Validate if a specific provider instance is in a list of regions using the # tfconfig/v2 and tfplan/v2 iimports.
220219
# The parameter, p, should be a provider derived from tfconfig.providers
221-
# or from provider_config_key or a resource from tfconfig.resources.
222-
# The parameter, region, should be given as a string such as "us-east-1"
223-
validate_provider_region = func(p, region) {
220+
# or from provider_config_key of a resource from tfconfig.resources.
221+
# The parameter, regions, should be given as a list of allowed regions
222+
223+
# It attempts to identify the region of the provider aliases in several ways
224+
# including constant values assigned to their `region` argument and resolution
225+
# of references to variables. It first tries to process references to variables
226+
# as strings, then as maps with a key called "region". It handles references
227+
# to variables in the root module by using tfplan.variables. It handles references
228+
# to variables in non-root modules by examining the module call from the current
229+
# module's parent.
230+
231+
# It even tries to match provider aliases in proxy configuration blocks
232+
# (which do not specify regions) of child modules to similarly-named provider
233+
# aliases in the root module.
234+
235+
# If the alias passed in the module call does not match the alias in the root
236+
# module, Sentinel has no way of linking the two provider aliases. However,
237+
# since all providers that do specify regions will be restricted and since
238+
# provider alias proxies must point to other provider aliases in ancestor modules,
239+
# all provider aliases should be restricted by this policy.
240+
validate_provider_in_allowed_regions = func(p, regions) {
224241
if "config" in keys(p) and "region" in keys(p.config) {
225242
# provider has its own region
226243
if "constant_value" in keys(p.config.region) {
227-
if p.config.region.constant_value is region {
244+
if p.config.region.constant_value in regions {
228245
# Found resource from region
229246
return true
230247
} else {
@@ -241,10 +258,17 @@ validate_provider_region = func(p, region) {
241258
if p.module_address is "" {
242259
# Get root module variable from tfplan.variables
243260
variable_value = tfplan.variables[variable_name].value
244-
if variable_value is region {
261+
# Check type of variable
262+
variable_type = types.type_of(variable_value)
263+
if variable_type is "string" and variable_value in regions {
245264
# Found resource from region
246265
return true
247-
}
266+
} else if variable_type is "map" and
267+
"region" in keys(variable_value) and
268+
variable_value["region"] in regions {
269+
# Found resource from region
270+
return true
271+
} // end variable_type check
248272
} else {
249273
# Get non-root module variable
250274
# Could be value passed into variable by module call
@@ -269,7 +293,7 @@ validate_provider_region = func(p, region) {
269293
if variable_name in keys(mc.config) {
270294
mc_var = mc.config[variable_name]
271295
if "constant_value" in keys(mc_var) {
272-
if mc_var.constant_value is region {
296+
if mc_var.constant_value in regions {
273297
# Found resource from region
274298
return true
275299
} else {
@@ -281,16 +305,29 @@ validate_provider_region = func(p, region) {
281305
if strings.has_prefix(mc_reference, "var.") {
282306
mc_variable_name = strings.split(mc_reference, ".")[1]
283307
mc_variable_value = tfplan.variables[mc_variable_name].value
284-
if mc_variable_value is region {
308+
mc_variable_type = types.type_of(mc_variable_value)
309+
if mc_variable_type is "string" and mc_variable_value in regions {
285310
# Found resource from region
286311
return true
287-
} // end region check
312+
} else if mc_variable_type is "map" and
313+
"region" in keys(mc_variable_value) and
314+
mc_variable_value["region"] in regions {
315+
# Found resource from region
316+
return true
317+
} // end mc_variable_type check
288318
} // end if mc_reference is a root module variable
289319
} // end for mc_var.references
290320
} // end constant_value or references in module call
291321
} else {
292322
# check default value of variable in current module
293-
if tfconfig.variables[variable_name].default is region {
323+
default_value = tfconfig.variables[variable_name].default
324+
default_value_type = types.type_of(default_value)
325+
if default_value_type is "string" and default_value in regions {
326+
# Found resource from region
327+
return true
328+
} else if default_value_type is "map" and
329+
"region" in keys(default_value) and
330+
default_value["region"] in regions {
294331
# Found resource from region
295332
return true
296333
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# filter_providers_by_regions
2+
3+
This function filters instances of the AWS provider to those in a specific region using the tfconfig/v2 and tfplan/v2 imports.
4+
5+
See the documentation for the [validate_provider_in_allowed_regions](./validate_provider_in_allowed_regions.md) function for details on how this is done.
6+
7+
## Sentinel Module
8+
This function is contained in the [aws-functions.sentinel](../aws-functions.sentinel) module.
9+
10+
## Declaration
11+
`filter_providers_by_regions = func(aws_providers, allowed_regions)`
12+
13+
## Arguments
14+
* **aws_providers**: a collection of instances of the AWS provider derived from tfconfig.providers.
15+
* **allowed_regions**: a list of AWS regions given as strings like `["us-east-1" and "eu-west-2"]`
16+
17+
## Common Functions Used
18+
This function calls the the `validate_provider_in_allowed_regions` of the [aws-functions.sentinel](../aws-functions.sentinel) module.
19+
20+
## What It Returns
21+
This function returns a single flat map of AWS providers. The map is actually a filtered sub-collection of the [`tfconfig.providers`](https://www.terraform.io/docs/cloud/sentinel/import/tfconfig-v2.html#the-resources-collection) collection.
22+
23+
## What It Prints
24+
This function currently prints providers that are validated to assist evaluation of the function when used by customers. In the future, we might remove that printing.
25+
26+
## Examples
27+
Here is an example of calling this function, assuming that the aws-functions.sentinel file that contains it has been imported with the alias `aws`:
28+
```
29+
validated_providers =
30+
aws.filter_providers_by_regions(all_aws_providers, allowed_regions)
31+
```
32+
33+
This function is used by the [validate-providers-from-desired-regions.sentinel](../../validate-providers-from-desired-regions.sentinel) policy.

governance/third-generation/aws/aws-functions/docs/filter_resources_by_region.md

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# validate_provider_in_allowed_regions
2+
This function validates whether a specific instance of the AWS provider is in a list of regions. The provider instance should be derived from `tfconfig.providers` or from the `provider_config_key` of a resource derived from `tfconfig.resources`.
3+
4+
It attempts to identify the region of the provider aliases in several ways including constant values assigned to their `region` argument and resolution of references to variables. It first tries to process references to variables as strings, then as maps with a key called "region". It handles references to variables in the root module by using tfplan.variables. It handles references to variables in non-root modules by examining the module call from the current module's parent.
5+
6+
It even tries to match provider aliases in proxy configuration blocks (which do not specify regions) of child modules to similarly-named provider aliases in the root module.
7+
8+
If the alias passed in the module call does not match the alias in the root module, Sentinel has no way of linking the two provider aliases. However, since all providers that do specify regions will be restricted and since provider alias proxies must point to other provider aliases in ancestor modules, all provider aliases should be restricted by this policy.
9+
10+
## Sentinel Module
11+
This function is contained in the [aws-functions.sentinel](../aws-functions.sentinel) module.
12+
13+
## Declaration
14+
`validate_provider_in_allowed_regions = func(p, regions)`
15+
16+
## Arguments
17+
* **p**: a specific alias of the AWS provider derived from `tfconfig.providers` or from the `provider_config_key` attribute of a resource derived from `tfconfig.resources`.
18+
* **regions**: a list of AWS AWS regions given as strings like `["us-east-1" and "eu-west-2"]`
19+
20+
## Common Functions Used
21+
None
22+
23+
## What It Returns
24+
This function returns a boolean indicating whether the provider alias was in one of the desired regions.
25+
26+
## What It Prints
27+
This function does not print anything.
28+
29+
## Examples
30+
Here is an example of calling this function, assuming that the aws-functions.sentinel file that contains it has been imported with the alias `aws`:
31+
```
32+
validated_providers = {}
33+
for aws_providers as index, p {
34+
validated = validate_provider_in_allowed_regions(p, allowed_regions)
35+
if validated {
36+
validated_providers[index] = p
37+
}
38+
}
39+
```
40+
41+
This function is used by the `filter_providers_by_regions` function of the aws-functions module.

governance/third-generation/aws/aws-functions/docs/validate_provider_region.md

Lines changed: 0 additions & 33 deletions
This file was deleted.

governance/third-generation/aws/test/validate-resources-from-desired-regions/fail.json renamed to governance/third-generation/aws/test/validate-providers-from-desired-regions/fail.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
},
1010
"mock": {
1111
"tfplan/v2": "mock-tfplan-fail.sentinel",
12-
"tfconfig/v2": "mock-tfconfig-fail.sentinel"
12+
"tfconfig/v2": "mock-tfconfig-fail.sentinel",
13+
"tfrun": "mock-tfrun-fail.sentinel"
14+
1315
},
1416
"test": {
1517
"main": false

0 commit comments

Comments
 (0)