Merge pull request #1029 from GoogleCloudPlatform/jccb/tests-revamp

Testing framework revamp
This commit is contained in:
Julio Castillo
2022-12-06 16:26:35 +01:00
committed by GitHub
76 changed files with 2140 additions and 868 deletions

92
tests/collectors.py Normal file
View File

@@ -0,0 +1,92 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Pytest plugin to discover tests specified in YAML files.
This plugin uses the pytest_collect_file hook to collect all files
matching tftest*.yaml and runs plan_validate for each test found.
See FabricTestFile for details on the file structure.
"""
import pytest
import yaml
from .fixtures import plan_summary, plan_validator
class FabricTestFile(pytest.File):
def collect(self):
"""Read yaml test spec and yield test items for each test definition.
The test spec should contain a `module` key with the path of the
terraform module to test, relative to the root of the repository
Tests are defined within the top-level `tests` key, and should
have the following structure:
test-name:
tfvars:
- tfvars1.tfvars
- tfvars2.tfvars
inventory:
- inventory1.yaml
- inventory2.yaml
All paths specifications are relative to the location of the test
spec. The inventory key is optional, if omitted, the inventory
will be taken from the file test-name.yaml
"""
try:
raw = yaml.safe_load(self.path.open())
module = raw.pop('module')
except (IOError, OSError, yaml.YAMLError) as e:
raise Exception(f'cannot read test spec {self.path}: {e}')
except KeyError as e:
raise Exception(f'`module` key not found in {self.path}: {e}')
common = raw.pop('common_tfvars', [])
for test_name, spec in raw.get('tests', {}).items():
spec = {} if spec is None else spec
inventories = spec.get('inventory', [f'{test_name}.yaml'])
tfvars = common + [f'{test_name}.tfvars'] + spec.get('tfvars', [])
for i in inventories:
name = test_name
if isinstance(inventories, list) and len(inventories) > 1:
name = f'{test_name}[{i}]'
yield FabricTestItem.from_parent(self, name=name, module=module,
inventory=[i], tfvars=tfvars)
class FabricTestItem(pytest.Item):
def __init__(self, name, parent, module, inventory, tfvars):
super().__init__(name, parent)
self.module = module
self.inventory = inventory
self.tfvars = tfvars
def runtest(self):
s = plan_validator(self.module, self.inventory, self.parent.path.parent,
self.tfvars)
def reportinfo(self):
return self.path, None, self.name
def pytest_collect_file(parent, file_path):
'Collect tftest*.yaml files and run plan_validator from them.'
if file_path.suffix == '.yaml' and file_path.name.startswith('tftest'):
return FabricTestFile.from_parent(parent, path=file_path)

View File

@@ -11,144 +11,12 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"Shared fixtures"
import inspect
import os
import shutil
import tempfile
'Pytest configuration.'
import pytest
import tftest
BASEDIR = os.path.dirname(os.path.dirname(__file__))
@pytest.fixture(scope='session')
def _plan_runner():
"Returns a function to run Terraform plan on a fixture."
def run_plan(fixture_path=None, extra_files=None, tf_var_file=None,
targets=None, refresh=True, tmpdir=True, **tf_vars):
"Runs Terraform plan and returns parsed output."
if fixture_path is None:
# find out the fixture directory from the caller's directory
caller = inspect.stack()[2]
fixture_path = os.path.join(os.path.dirname(caller.filename), "fixture")
fixture_parent = os.path.dirname(fixture_path)
fixture_prefix = os.path.basename(fixture_path) + "_"
with tempfile.TemporaryDirectory(prefix=fixture_prefix,
dir=fixture_parent) as tmp_path:
# copy fixture to a temporary directory so we can execute
# multiple tests in parallel
if tmpdir:
shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
tf = tftest.TerraformTest(tmp_path if tmpdir else fixture_path, BASEDIR,
os.environ.get('TERRAFORM', 'terraform'))
tf.setup(extra_files=extra_files, upgrade=True)
plan = tf.plan(output=True, refresh=refresh, tf_var_file=tf_var_file,
tf_vars=tf_vars, targets=targets)
return plan
return run_plan
@pytest.fixture(scope='session')
def plan_runner(_plan_runner):
"Returns a function to run Terraform plan on a module fixture."
def run_plan(fixture_path=None, extra_files=None, tf_var_file=None,
targets=None, **tf_vars):
"Runs Terraform plan and returns plan and module resources."
plan = _plan_runner(fixture_path, extra_files=extra_files,
tf_var_file=tf_var_file, targets=targets, **tf_vars)
# skip the fixture
root_module = plan.root_module['child_modules'][0]
return plan, root_module['resources']
return run_plan
@pytest.fixture(scope='session')
def e2e_plan_runner(_plan_runner):
"Returns a function to run Terraform plan on an end-to-end fixture."
def run_plan(fixture_path=None, tf_var_file=None, targets=None,
refresh=True, include_bare_resources=False, **tf_vars):
"Runs Terraform plan on an end-to-end module using defaults, returns data."
plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets,
refresh=refresh, **tf_vars)
# skip the fixture
root_module = plan.root_module['child_modules'][0]
modules = dict((mod['address'], mod['resources'])
for mod in root_module['child_modules'])
resources = [r for m in modules.values() for r in m]
if include_bare_resources:
bare_resources = root_module['resources']
resources.extend(bare_resources)
return modules, resources
return run_plan
@pytest.fixture(scope='session')
def recursive_e2e_plan_runner(_plan_runner):
"""Plan runner for end-to-end root module, returns total number of
(nested) modules and resources"""
def walk_plan(node, modules, resources):
# TODO(jccb): this would be better with node.get() but
# TerraformPlanOutput objects don't have it
new_modules = node.get('child_modules', [])
resources += node.get('resources', [])
modules += new_modules
for module in new_modules:
walk_plan(module, modules, resources)
def run_plan(fixture_path=None, tf_var_file=None, targets=None, refresh=True,
include_bare_resources=False, compute_sums=True, tmpdir=True,
**tf_vars):
"Runs Terraform plan on a root module using defaults, returns data."
plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets,
refresh=refresh, tmpdir=tmpdir, **tf_vars)
modules = []
resources = []
walk_plan(plan.root_module, modules, resources)
return len(modules), len(resources)
return run_plan
@pytest.fixture(scope='session')
def apply_runner():
"Returns a function to run Terraform apply on a fixture."
def run_apply(fixture_path=None, **tf_vars):
"Runs Terraform plan and returns parsed output."
if fixture_path is None:
# find out the fixture directory from the caller's directory
caller = inspect.stack()[1]
fixture_path = os.path.join(os.path.dirname(caller.filename), "fixture")
fixture_parent = os.path.dirname(fixture_path)
fixture_prefix = os.path.basename(fixture_path) + "_"
with tempfile.TemporaryDirectory(prefix=fixture_prefix,
dir=fixture_parent) as tmp_path:
# copy fixture to a temporary directory so we can execute
# multiple tests in parallel
shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
tf = tftest.TerraformTest(tmp_path, BASEDIR,
os.environ.get('TERRAFORM', 'terraform'))
tf.setup(upgrade=True)
apply = tf.apply(tf_vars=tf_vars)
output = tf.output(json_format=True)
return apply, output
return run_apply
@pytest.fixture
def basedir():
return BASEDIR
pytest_plugins = (
'tests.fixtures',
'tests.legacy_fixtures',
'tests.collectors',
)

View File

@@ -1,29 +0,0 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module "stage" {
source = "../../../../../fast/stages/00-bootstrap"
prefix = "fast"
organization = {
domain = "fast.example.com"
id = 123456789012
customer_id = "C00000000"
}
billing_account = {
id = "000000-111111-222222"
organization_id = 123456789012
}
}

View File

@@ -0,0 +1,11 @@
organization = {
domain = "fast.example.com"
id = 123456789012
customer_id = "C00000000"
}
billing_account = {
id = "000000-111111-222222"
organization_id = 123456789012
}
prefix = "fast"
outputs_location = "/fast-config"

View File

@@ -0,0 +1,49 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
counts:
google_bigquery_dataset: 2
google_bigquery_dataset_iam_member: 2
google_bigquery_default_service_account: 3
google_logging_organization_sink: 2
google_organization_iam_binding: 19
google_organization_iam_custom_role: 2
google_organization_iam_member: 16
google_project: 3
google_project_iam_binding: 9
google_project_iam_member: 1
google_project_service: 29
google_project_service_identity: 2
google_service_account: 3
google_service_account_iam_binding: 3
google_storage_bucket: 4
google_storage_bucket_iam_binding: 2
google_storage_bucket_iam_member: 3
google_storage_bucket_object: 5
google_storage_project_service_account: 3
local_file: 5
outputs:
custom_roles:
organization_iam_admin: organizations/123456789012/roles/organizationIamAdmin
service_project_network_admin: organizations/123456789012/roles/serviceProjectNetworkAdmin
outputs_bucket: fast-prod-iac-core-outputs-0
project_ids:
automation: fast-prod-iac-core-0
billing-export: fast-prod-billing-exp-0
log-export: fast-prod-audit-logs-0
service_accounts:
bootstrap: fast-prod-bootstrap-0@fast-prod-iac-core-0.iam.gserviceaccount.com
cicd: fast-prod-cicd-0@fast-prod-iac-core-0.iam.gserviceaccount.com
resman: fast-prod-resman-0@fast-prod-iac-core-0.iam.gserviceaccount.com

View File

@@ -0,0 +1,33 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
module.automation-project.google_project.project[0]:
auto_create_network: false
billing_account: 000000-111111-222222
name: fast-prod-iac-core-0
org_id: '123456789012'
project_id: fast-prod-iac-core-0
module.billing-export-project[0].google_project.project[0]:
auto_create_network: false
billing_account: 000000-111111-222222
name: fast-prod-billing-exp-0
org_id: '123456789012'
project_id: fast-prod-billing-exp-0
module.log-export-project.google_project.project[0]:
auto_create_network: false
billing_account: 000000-111111-222222
name: fast-prod-audit-logs-0
org_id: '123456789012'
project_id: fast-prod-audit-logs-0

View File

@@ -0,0 +1,27 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
module.automation-tf-bootstrap-sa.google_service_account.service_account[0]:
account_id: fast-prod-bootstrap-0
display_name: Terraform organization bootstrap service account.
project: fast-prod-iac-core-0
module.automation-tf-cicd-provisioning-sa.google_service_account.service_account[0]:
account_id: fast-prod-cicd-0
display_name: Terraform stage 1 CICD service account.
project: fast-prod-iac-core-0
module.automation-tf-resman-sa.google_service_account.service_account[0]:
account_id: fast-prod-resman-0
display_name: Terraform stage 1 resman service account.
project: fast-prod-iac-core-0

View File

@@ -1,33 +0,0 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# _RESOURCE_COUNT = {
# 'module.organization': 28,
# 'module.automation-project': 23,
# 'module.automation-tf-bootstrap-gcs': 1,
# 'module.automation-tf-bootstrap-sa': 1,
# 'module.automation-tf-resman-gcs': 2,
# 'module.automation-tf-resman-sa': 1,
# 'module.billing-export-dataset': 1,
# 'module.billing-export-project': 7,
# 'module.log-export-dataset': 1,
# 'module.log-export-project': 7,
# }
def test_counts(recursive_e2e_plan_runner):
"Test stage."
# TODO: to re-enable per-module resource count check print _, then test
num_modules, num_resources = recursive_e2e_plan_runner()
assert num_modules > 0 and num_resources > 0

View File

@@ -0,0 +1,12 @@
# skip boilerplate check
module: fast/stages/00-bootstrap
tests:
simple:
tfvars:
- simple.tfvars
inventory:
- simple.yaml
- simple_projects.yaml
- simple_sas.yaml

235
tests/fixtures.py Normal file
View File

@@ -0,0 +1,235 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Common fixtures."""
import collections
import contextlib
import itertools
import os
import shutil
import tempfile
from pathlib import Path
import pytest
import tftest
import yaml
PlanSummary = collections.namedtuple('PlanSummary', 'values counts outputs')
@contextlib.contextmanager
def _prepare_root_module(path):
"""Context manager to prepare a terraform module to be tested.
If the TFTEST_COPY environment variable is set, `path` is copied to
a temporary directory and a few terraform files (e.g.
terraform.tfvars) are delete to ensure a clean test environment.
Otherwise, `path` is simply returned untouched.
"""
if os.environ.get('TFTEST_COPY'):
# if the TFTEST_COPY is set, create temp dir and copy the root
# module there
with tempfile.TemporaryDirectory(dir=path.parent) as tmp_path:
tmp_path = Path(tmp_path)
# if we're copying the module, we might as well ignore files and
# directories that are automatically read by terraform. Useful
# to avoid surprises if, for example, you have an active fast
# deployment with links to configs)
ignore_patterns = shutil.ignore_patterns('*.auto.tfvars',
'*.auto.tfvars.json',
'terraform.tfstate*',
'terraform.tfvars', '.terraform')
shutil.copytree(path, tmp_path, dirs_exist_ok=True,
ignore=ignore_patterns)
yield tmp_path
else:
# if TFTEST_COPY is not set, just return the same path
yield path
def plan_summary(module_path, basedir, tf_var_files=None, **tf_vars):
"""
Run a Terraform plan on the module located at `module_path`.
- module_path: terraform root module to run. Can be an absolute
path or relative to the root of the repository
- basedir: directory root to use for relative paths in
tf_var_files.
- tf_var_files: set of terraform variable files (tfvars) to pass
in to terraform
Returns a PlanSummary object containing 3 attributes:
- values: dictionary where the keys are terraform plan addresses
and values are the JSON representation (converted to python
types) of the attribute values of the resource.
- counts: dictionary where the keys are the terraform resource
types and the values are the number of times that type appears
in the plan
- outputs: dictionary of the modules outputs that can be
determined at plan type.
Consult [1] for mode details on the structure of values and outputs
[1] https://developer.hashicorp.com/terraform/internals/json-format
"""
# make the module_path relative to the root of the repo while still
# supporting absolute paths
module_path = Path(__file__).parents[1] / module_path
with _prepare_root_module(module_path) as test_path:
binary = os.environ.get('TERRAFORM', 'terraform')
tf = tftest.TerraformTest(test_path, binary=binary)
tf.setup(upgrade=True)
tf_var_files = [(basedir / x).resolve() for x in tf_var_files or []]
plan = tf.plan(output=True, tf_var_file=tf_var_files, tf_vars=tf_vars)
# compute resource type counts and address->values map
values = {}
counts = collections.defaultdict(int)
q = collections.deque([plan.root_module])
while q:
e = q.popleft()
if 'type' in e:
counts[e['type']] += 1
if 'values' in e:
values[e['address']] = e['values']
for x in e.get('resources', []):
q.append(x)
for x in e.get('child_modules', []):
q.append(x)
# extract planned outputs
outputs = plan.get('planned_values', {}).get('outputs', {})
return PlanSummary(values, dict(counts), outputs)
@pytest.fixture(name='plan_summary')
def plan_summary_fixture(request):
"""Return a function to generate a PlanSummary.
In the returned function `basedir` becomes optional and it defaults
to the directory of the calling test
"""
def inner(module_path, basedir=None, tf_var_files=None, **tf_vars):
if basedir is None:
basedir = Path(request.fspath).parent
return plan_summary(module_path=module_path, basedir=basedir,
tf_var_files=tf_var_files, **tf_vars)
return inner
def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None,
**tf_vars):
summary = plan_summary(module_path=module_path, tf_var_files=tf_var_files,
basedir=basedir, **tf_vars)
# allow single single string for inventory_paths
if not isinstance(inventory_paths, list):
inventory_paths = [inventory_paths]
for path in inventory_paths:
# allow tfvars and inventory to be relative to the caller
path = basedir / path
try:
inventory = yaml.safe_load(path.read_text())
except (IOError, OSError, yaml.YAMLError) as e:
raise Exception(f'cannot read test inventory {path}: {e}')
# don't fail if the inventory is empty
inventory = inventory or {}
# If you add additional asserts to this function:
# - put the values coming from the plan on the left side of
# any comparison operators
# - put the values coming from user's inventory the right
# side of any comparison operators.
# - include a descriptive error message to the assert
# for values:
# - verify each address in the user's inventory exists in the plan
# - for those address that exist on both the user's inventory and
# the plan output, ensure the set of keys on the inventory are a
# subset of the keys in the plan, and compare their values by
# equality
if 'values' in inventory:
expected_values = inventory['values']
for address, expected_value in expected_values.items():
assert address in summary.values, \
f'{address} is not a valid address in the plan'
for k, v in expected_value.items():
assert k in summary.values[address], \
f'{k} not found at {address}'
plan_value = summary.values[address][k]
assert plan_value == v, \
f'{k} at {address} failed. Got `{plan_value}`, expected `{v}`'
if 'counts' in inventory:
expected_counts = inventory['counts']
for type_, expected_count in expected_counts.items():
assert type_ in summary.counts, \
f'module does not create any resources of type `{type_}`'
plan_count = summary.counts[type_]
assert plan_count == expected_count, \
f'count of {type_} resources failed. Got {plan_count}, expected {expected_count}'
if 'outputs' in inventory:
expected_outputs = inventory['outputs']
for output_name, expected_output in expected_outputs.items():
assert output_name in summary.outputs, \
f'module does not output `{output_name}`'
output = summary.outputs[output_name]
# assert 'value' in output, \
# f'output `{output_name}` does not have a value (is it sensitive or dynamic?)'
plan_output = output.get('value', '__missing__')
assert plan_output == expected_output, \
f'output {output_name} failed. Got `{plan_output}`, expected `{expected_output}`'
return summary
@pytest.fixture(name='plan_validator')
def plan_validator_fixture(request):
"""Return a function to build a PlanSummary and compare it to a YAML inventory.
In the returned function `basedir` becomes optional and it defaults
to the directory of the calling test'
"""
def inner(module_path, inventory_paths, basedir=None, tf_var_files=None,
**tf_vars):
if basedir is None:
basedir = Path(request.fspath).parent
return plan_validator(module_path=module_path,
inventory_paths=inventory_paths, basedir=basedir,
tf_var_files=tf_var_paths, **tf_vars)
return inner
# @pytest.fixture
# def repo_root():
# 'Return a pathlib.Path to the root of the repository'
# return Path(__file__).parents[1]

153
tests/legacy_fixtures.py Normal file
View File

@@ -0,0 +1,153 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Legacy pytest fixtures.
The fixtures contained in this file will eventually go away. Consider
using one of the fixtures in fixtures.py
"""
import inspect
import os
import shutil
import tempfile
import pytest
import tftest
BASEDIR = os.path.dirname(os.path.dirname(__file__))
@pytest.fixture(scope='session')
def _plan_runner():
'Return a function to run Terraform plan on a fixture.'
def run_plan(fixture_path=None, extra_files=None, tf_var_file=None,
targets=None, refresh=True, tmpdir=True, **tf_vars):
'Run Terraform plan and returns parsed output.'
if fixture_path is None:
# find out the fixture directory from the caller's directory
caller = inspect.stack()[2]
fixture_path = os.path.join(os.path.dirname(caller.filename), 'fixture')
fixture_parent = os.path.dirname(fixture_path)
fixture_prefix = os.path.basename(fixture_path) + '_'
with tempfile.TemporaryDirectory(prefix=fixture_prefix,
dir=fixture_parent) as tmp_path:
# copy fixture to a temporary directory so we can execute
# multiple tests in parallel
if tmpdir:
shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
tf = tftest.TerraformTest(tmp_path if tmpdir else fixture_path, BASEDIR,
os.environ.get('TERRAFORM', 'terraform'))
tf.setup(extra_files=extra_files, upgrade=True)
plan = tf.plan(output=True, refresh=refresh, tf_var_file=tf_var_file,
tf_vars=tf_vars, targets=targets)
return plan
return run_plan
@pytest.fixture(scope='session')
def plan_runner(_plan_runner):
'Return a function to run Terraform plan on a module fixture.'
def run_plan(fixture_path=None, extra_files=None, tf_var_file=None,
targets=None, **tf_vars):
'Run Terraform plan and returns plan and module resources.'
plan = _plan_runner(fixture_path, extra_files=extra_files,
tf_var_file=tf_var_file, targets=targets, **tf_vars)
# skip the fixture
root_module = plan.root_module['child_modules'][0]
return plan, root_module['resources']
return run_plan
@pytest.fixture(scope='session')
def e2e_plan_runner(_plan_runner):
'Return a function to run Terraform plan on an end-to-end fixture.'
def run_plan(fixture_path=None, tf_var_file=None, targets=None, refresh=True,
include_bare_resources=False, **tf_vars):
'Run Terraform plan on an end-to-end module using defaults, returns data.'
plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets,
refresh=refresh, **tf_vars)
# skip the fixture
root_module = plan.root_module['child_modules'][0]
modules = dict((mod['address'], mod['resources'])
for mod in root_module['child_modules'])
resources = [r for m in modules.values() for r in m]
if include_bare_resources:
bare_resources = root_module['resources']
resources.extend(bare_resources)
return modules, resources
return run_plan
@pytest.fixture(scope='session')
def recursive_e2e_plan_runner(_plan_runner):
"""
Plan runner for end-to-end root module, returns total number of
(nested) modules and resources
"""
def walk_plan(node, modules, resources):
new_modules = node.get('child_modules', [])
resources += node.get('resources', [])
modules += new_modules
for module in new_modules:
walk_plan(module, modules, resources)
def run_plan(fixture_path=None, tf_var_file=None, targets=None, refresh=True,
include_bare_resources=False, compute_sums=True, tmpdir=True,
**tf_vars):
'Run Terraform plan on a root module using defaults, returns data.'
plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets,
refresh=refresh, tmpdir=tmpdir, **tf_vars)
modules = []
resources = []
walk_plan(plan.root_module, modules, resources)
return len(modules), len(resources)
return run_plan
@pytest.fixture(scope='session')
def apply_runner():
'Return a function to run Terraform apply on a fixture.'
def run_apply(fixture_path=None, **tf_vars):
'Run Terraform plan and returns parsed output.'
if fixture_path is None:
# find out the fixture directory from the caller's directory
caller = inspect.stack()[1]
fixture_path = os.path.join(os.path.dirname(caller.filename), 'fixture')
fixture_parent = os.path.dirname(fixture_path)
fixture_prefix = os.path.basename(fixture_path) + '_'
with tempfile.TemporaryDirectory(prefix=fixture_prefix,
dir=fixture_parent) as tmp_path:
# copy fixture to a temporary directory so we can execute
# multiple tests in parallel
shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
tf = tftest.TerraformTest(tmp_path, BASEDIR,
os.environ.get('TERRAFORM', 'terraform'))
tf.setup(upgrade=True)
apply = tf.apply(tf_vars=tf_vars)
output = tf.output(json_format=True)
return apply, output
return run_apply

View File

@@ -20,16 +20,11 @@ import pytest
import yaml
def pytest_collection_modifyitems(config, items):
for item in items:
item.add_marker(pytest.mark.xdist_group(name=item.path.parent.name))
@pytest.fixture(scope='session')
def tfvars_to_yaml():
@pytest.fixture()
def tfvars_to_yaml(request):
def converter(source, dest, from_var, to_var=None):
p_fixture = pathlib.Path(inspect.stack()[1].filename).parent / 'fixture'
p_fixture = pathlib.Path(request.path).parent
p_source = p_fixture / source
if not p_source.exists():
raise ValueError(f"tfvars '{source}' not found")

View File

@@ -31,13 +31,14 @@ def test_policy_list(plan_runner):
def test_factory_policy_boolean(plan_runner, tfvars_to_yaml, tmp_path):
dest = tmp_path / 'policies.yaml'
tfvars_to_yaml('test.orgpolicies-boolean.tfvars', dest, 'org_policies')
tfvars_to_yaml('fixture/test.orgpolicies-boolean.tfvars', dest,
'org_policies')
_, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"')
validate_policy_boolean(resources)
def test_factory_policy_list(plan_runner, tfvars_to_yaml, tmp_path):
dest = tmp_path / 'policies.yaml'
tfvars_to_yaml('test.orgpolicies-list.tfvars', dest, 'org_policies')
tfvars_to_yaml('fixture/test.orgpolicies-list.tfvars', dest, 'org_policies')
_, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"')
validate_policy_list(resources)

View File

@@ -0,0 +1,2 @@
project_id = "test-project"
name = "test"

View File

@@ -0,0 +1 @@
data_folder = "../../tests/modules/net_vpc/data"

View File

@@ -0,0 +1,44 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_subnetwork.subnetwork["europe-west1/factory-subnet"]:
description: 'Sample description'
ip_cidr_range: '10.128.0.0/24'
ipv6_access_type: null
log_config: []
name: 'factory-subnet'
private_ip_google_access: false
project: 'test-project'
region: 'europe-west1'
role: null
secondary_ip_range:
- ip_cidr_range: '192.168.128.0/24'
range_name: 'secondary-range-a'
google_compute_subnetwork.subnetwork["europe-west4/factory-subnet2"]:
description: 'Sample description'
ip_cidr_range: '10.129.0.0/24'
log_config: []
name: 'factory-subnet2'
private_ip_google_access: true
project: 'test-project'
region: 'europe-west4'
role: null
secondary_ip_range: []
# FIXME: should we have some bindings here?
counts:
google_compute_network: 1
google_compute_subnetwork: 2

View File

@@ -0,0 +1,5 @@
peering_config = {
peer_vpc_self_link = "projects/my-project/global/networks/peer"
export_routes = true
import_routes = null
}

View File

@@ -0,0 +1,47 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_network.network[0]:
auto_create_subnetworks: false
delete_default_routes_on_create: false
description: Terraform-managed.
name: test
project: test-project
routing_mode: GLOBAL
google_compute_network_peering.local[0]:
export_custom_routes: true
import_custom_routes: false
name: test-peer
peer_network: projects/my-project/global/networks/peer
google_compute_network_peering.remote[0]:
export_custom_routes: false
import_custom_routes: true
name: peer-test
network: projects/my-project/global/networks/peer
counts:
google_compute_network: 1
google_compute_network_peering: 2
outputs:
bindings: {}
project_id: test-project
subnet_ips: {}
subnet_regions: {}
subnet_secondary_ranges: {}
subnet_self_links: {}
subnets: {}
subnets_proxy_only: {}
subnets_psc: {}

View File

@@ -0,0 +1,7 @@
psa_config = {
ranges = {
bar = "172.16.100.0/24"
}
export_routes = true
import_routes = false
}

View File

@@ -0,0 +1,60 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_global_address.psa_ranges["bar"]:
address: 172.16.100.0
address_type: INTERNAL
description: null
ip_version: null
name: bar
prefix_length: 24
project: test-project
purpose: VPC_PEERING
google_compute_network.network[0]:
auto_create_subnetworks: false
delete_default_routes_on_create: false
description: Terraform-managed.
enable_ula_internal_ipv6: null
name: test
project: test-project
routing_mode: GLOBAL
google_compute_network_peering_routes_config.psa_routes["1"]:
export_custom_routes: true
import_custom_routes: false
project: test-project
google_service_networking_connection.psa_connection["1"]:
reserved_peering_ranges:
- bar
service: servicenetworking.googleapis.com
counts:
google_compute_global_address: 1
google_compute_network: 1
google_compute_network_peering_routes_config: 1
google_service_networking_connection: 1
outputs:
bindings: {}
name: __missing__
network: __missing__
project_id: test-project
self_link: __missing__
subnet_ips: {}
subnet_regions: {}
subnet_secondary_ranges: {}
subnet_self_links: {}
subnets: {}
subnets_proxy_only: {}
subnets_psc: {}

View File

@@ -0,0 +1,7 @@
psa_config = {
ranges = {
bar = "172.16.100.0/24"
}
export_routes = false
import_routes = true
}

View File

@@ -0,0 +1,60 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_global_address.psa_ranges["bar"]:
address: 172.16.100.0
address_type: INTERNAL
description: null
ip_version: null
name: bar
prefix_length: 24
project: test-project
purpose: VPC_PEERING
google_compute_network.network[0]:
auto_create_subnetworks: false
delete_default_routes_on_create: false
description: Terraform-managed.
enable_ula_internal_ipv6: null
name: test
project: test-project
routing_mode: GLOBAL
google_compute_network_peering_routes_config.psa_routes["1"]:
export_custom_routes: false
import_custom_routes: true
project: test-project
google_service_networking_connection.psa_connection["1"]:
reserved_peering_ranges:
- bar
service: servicenetworking.googleapis.com
counts:
google_compute_global_address: 1
google_compute_network: 1
google_compute_network_peering_routes_config: 1
google_service_networking_connection: 1
outputs:
bindings: {}
name: __missing__
network: __missing__
project_id: test-project
self_link: __missing__
subnet_ips: {}
subnet_regions: {}
subnet_secondary_ranges: {}
subnet_self_links: {}
subnets: {}
subnets_proxy_only: {}
subnets_psc: {}

View File

@@ -0,0 +1,7 @@
psa_config = {
ranges = {
bar = "172.16.100.0/24"
}
export_routes = true
import_routes = true
}

View File

@@ -0,0 +1,60 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_global_address.psa_ranges["bar"]:
address: 172.16.100.0
address_type: INTERNAL
description: null
ip_version: null
name: bar
prefix_length: 24
project: test-project
purpose: VPC_PEERING
google_compute_network.network[0]:
auto_create_subnetworks: false
delete_default_routes_on_create: false
description: Terraform-managed.
enable_ula_internal_ipv6: null
name: test
project: test-project
routing_mode: GLOBAL
google_compute_network_peering_routes_config.psa_routes["1"]:
export_custom_routes: true
import_custom_routes: true
project: test-project
google_service_networking_connection.psa_connection["1"]:
reserved_peering_ranges:
- bar
service: servicenetworking.googleapis.com
counts:
google_compute_global_address: 1
google_compute_network: 1
google_compute_network_peering_routes_config: 1
google_service_networking_connection: 1
outputs:
bindings: {}
name: __missing__
network: __missing__
project_id: test-project
self_link: __missing__
subnet_ips: {}
subnet_regions: {}
subnet_secondary_ranges: {}
subnet_self_links: {}
subnets: {}
subnets_proxy_only: {}
subnets_psc: {}

View File

@@ -0,0 +1,7 @@
psa_config = {
ranges = {
bar = "172.16.100.0/24"
foo = "172.16.101.0/24"
}
routes = null
}

View File

@@ -0,0 +1,70 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_global_address.psa_ranges["bar"]:
address: 172.16.100.0
address_type: INTERNAL
description: null
ip_version: null
name: bar
prefix_length: 24
project: test-project
purpose: VPC_PEERING
google_compute_global_address.psa_ranges["foo"]:
address: 172.16.101.0
address_type: INTERNAL
description: null
ip_version: null
name: foo
prefix_length: 24
project: test-project
purpose: VPC_PEERING
google_compute_network.network[0]:
auto_create_subnetworks: false
delete_default_routes_on_create: false
description: Terraform-managed.
enable_ula_internal_ipv6: null
name: test
project: test-project
routing_mode: GLOBAL
google_compute_network_peering_routes_config.psa_routes["1"]:
export_custom_routes: false
import_custom_routes: false
project: test-project
google_service_networking_connection.psa_connection["1"]:
reserved_peering_ranges:
- bar
- foo
service: servicenetworking.googleapis.com
counts:
google_compute_global_address: 2
google_compute_network: 1
google_compute_network_peering_routes_config: 1
google_service_networking_connection: 1
outputs:
bindings: {}
name: __missing__
network: __missing__
project_id: test-project
self_link: __missing__
subnet_ips: {}
subnet_regions: {}
subnet_secondary_ranges: {}
subnet_self_links: {}
subnets: {}
subnets_proxy_only: {}
subnets_psc: {}

View File

@@ -0,0 +1,2 @@
shared_vpc_host = true
shared_vpc_service_projects = ["tf-a", "tf-b"]

View File

@@ -0,0 +1,46 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_network.network[0]:
auto_create_subnetworks: false
delete_default_routes_on_create: false
description: Terraform-managed.
name: test
project: test-project
routing_mode: GLOBAL
google_compute_shared_vpc_host_project.shared_vpc_host[0]:
project: test-project
google_compute_shared_vpc_service_project.service_projects["tf-a"]:
host_project: test-project
service_project: tf-a
google_compute_shared_vpc_service_project.service_projects["tf-b"]:
host_project: test-project
service_project: tf-b
counts:
google_compute_network: 1
google_compute_shared_vpc_host_project: 1
google_compute_shared_vpc_service_project: 2
outputs:
bindings: {}
project_id: test-project
subnet_ips: {}
subnet_regions: {}
subnet_secondary_ranges: {}
subnet_self_links: {}
subnets: {}
subnets_proxy_only: {}
subnets_psc: {}

View File

@@ -0,0 +1 @@
# skip boilerplate check

View File

@@ -0,0 +1,36 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_network.network[0]:
auto_create_subnetworks: false
delete_default_routes_on_create: false
description: Terraform-managed.
name: test
project: test-project
routing_mode: GLOBAL
counts:
google_compute_network: 1
outputs:
bindings: {}
project_id: test-project
subnet_ips: {}
subnet_regions: {}
subnet_secondary_ranges: {}
subnet_self_links: {}
subnets: {}
subnets_proxy_only: {}
subnets_psc: {}

View File

@@ -0,0 +1,44 @@
subnet_iam = {
"europe-west1/a" = {
"roles/compute.networkUser" = [
"user:a@example.com", "group:g-a@example.com"
]
}
"europe-west1/c" = {
"roles/compute.networkUser" = [
"user:c@example.com", "group:g-c@example.com"
]
}
}
subnets = [
{
name = "a"
region = "europe-west1"
ip_cidr_range = "10.0.0.0/24"
},
{
name = "b"
region = "europe-west1"
ip_cidr_range = "10.0.1.0/24",
description = "Subnet b"
enable_private_access = false
},
{
name = "c"
region = "europe-west1"
ip_cidr_range = "10.0.2.0/24"
secondary_ip_ranges = {
a = "192.168.0.0/24"
b = "192.168.1.0/24"
}
},
{
name = "d"
region = "europe-west1"
ip_cidr_range = "10.0.3.0/24"
flow_logs_config = {
flow_sampling = 0.5
aggregation_interval = "INTERVAL_10_MIN"
}
}
]

View File

@@ -0,0 +1,120 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_network.network[0]:
auto_create_subnetworks: false
delete_default_routes_on_create: false
description: Terraform-managed.
name: test
project: test-project
routing_mode: GLOBAL
google_compute_subnetwork.subnetwork["europe-west1/a"]:
description: Terraform-managed.
ip_cidr_range: 10.0.0.0/24
log_config: []
name: a
private_ip_google_access: true
project: test-project
region: europe-west1
role: null
secondary_ip_range: []
google_compute_subnetwork.subnetwork["europe-west1/b"]:
description: Subnet b
ip_cidr_range: 10.0.1.0/24
log_config: []
name: b
private_ip_google_access: false
project: test-project
region: europe-west1
role: null
secondary_ip_range: []
google_compute_subnetwork.subnetwork["europe-west1/c"]:
description: Terraform-managed.
ip_cidr_range: 10.0.2.0/24
ipv6_access_type: null
log_config: []
name: c
private_ip_google_access: true
project: test-project
region: europe-west1
role: null
secondary_ip_range:
- ip_cidr_range: 192.168.0.0/24
range_name: a
- ip_cidr_range: 192.168.1.0/24
range_name: b
google_compute_subnetwork.subnetwork["europe-west1/d"]:
description: Terraform-managed.
ip_cidr_range: 10.0.3.0/24
log_config:
- aggregation_interval: INTERVAL_10_MIN
filter_expr: 'true'
flow_sampling: 0.5
metadata: INCLUDE_ALL_METADATA
metadata_fields: null
name: d
private_ip_google_access: true
project: test-project
region: europe-west1
role: null
secondary_ip_range: []
google_compute_subnetwork_iam_binding.binding["europe-west1/a.roles/compute.networkUser"]:
condition: []
members:
- group:g-a@example.com
- user:a@example.com
project: test-project
region: europe-west1
role: roles/compute.networkUser
subnetwork: a
google_compute_subnetwork_iam_binding.binding["europe-west1/c.roles/compute.networkUser"]:
condition: []
members:
- group:g-c@example.com
- user:c@example.com
project: test-project
region: europe-west1
role: roles/compute.networkUser
subnetwork: c
counts:
google_compute_network: 1
google_compute_subnetwork: 4
google_compute_subnetwork_iam_binding: 2
outputs:
bindings: __missing__
project_id: test-project
subnet_ips:
europe-west1/a: 10.0.0.0/24
europe-west1/b: 10.0.1.0/24
europe-west1/c: 10.0.2.0/24
europe-west1/d: 10.0.3.0/24
subnet_regions:
europe-west1/a: europe-west1
europe-west1/b: europe-west1
europe-west1/c: europe-west1
europe-west1/d: europe-west1
subnet_secondary_ranges:
europe-west1/a: {}
europe-west1/b: {}
europe-west1/c:
a: 192.168.0.0/24
b: 192.168.1.0/24
europe-west1/d: {}
subnet_self_links: __missing__
subnets: __missing__
subnets_proxy_only: {}
subnets_psc: {}

View File

@@ -1,81 +0,0 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
_VAR_PEER_VPC_CONFIG = '''{
peer_vpc_self_link="projects/my-project/global/networks/peer",
export_routes=true, import_routes=null
}'''
_VAR_ROUTES_TEMPLATE = '''{
next-hop = {
dest_range="192.168.128.0/24", tags=null,
next_hop_type="%s", next_hop="%s"},
gateway = {
dest_range="0.0.0.0/0", priority=100, tags=["tag-a"],
next_hop_type="gateway",
next_hop="global/gateways/default-internet-gateway"}
}'''
_VAR_ROUTES_NEXT_HOPS = {
'gateway': 'global/gateways/default-internet-gateway',
'instance': 'zones/europe-west1-b/test',
'ip': '192.168.0.128',
'ilb': 'regions/europe-west1/forwardingRules/test',
'vpn_tunnel': 'regions/europe-west1/vpnTunnels/foo'
}
def test_vpc_simple(plan_runner):
"Test vpc with no extra options."
_, resources = plan_runner()
assert len(resources) == 1
assert [r['type'] for r in resources] == ['google_compute_network']
assert [r['values']['name'] for r in resources] == ['test']
assert [r['values']['project'] for r in resources] == ['test-project']
def test_vpc_shared(plan_runner):
"Test shared vpc variables."
_, resources = plan_runner(shared_vpc_host='true',
shared_vpc_service_projects='["tf-a", "tf-b"]')
assert len(resources) == 4
assert set(r['type'] for r in resources) == set([
'google_compute_network', 'google_compute_shared_vpc_host_project',
'google_compute_shared_vpc_service_project'
])
def test_vpc_peering(plan_runner):
"Test vpc peering variables."
_, resources = plan_runner(peering_config=_VAR_PEER_VPC_CONFIG)
assert len(resources) == 3
assert set(r['type'] for r in resources) == set(
['google_compute_network', 'google_compute_network_peering'])
peerings = [
r['values']
for r in resources
if r['type'] == 'google_compute_network_peering'
]
assert [p['name'] for p in peerings] == ['test-peer', 'peer-test']
assert [p['export_custom_routes'] for p in peerings] == [True, False]
assert [p['import_custom_routes'] for p in peerings] == [False, True]
def test_vpc_routes(plan_runner):
"Test vpc routes."
for next_hop_type, next_hop in _VAR_ROUTES_NEXT_HOPS.items():
_var_routes = _VAR_ROUTES_TEMPLATE % (next_hop_type, next_hop)
_, resources = plan_runner(routes=_var_routes)
assert len(resources) == 3
resource = [r for r in resources if r['values']['name'] == 'test-next-hop'
][0]
assert resource['values']['next_hop_%s' % next_hop_type]

View File

@@ -1,79 +0,0 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
def test_single_range(plan_runner):
"Test single PSA range."
psa_config = '''{
ranges = {
bar = "172.16.100.0/24"
foo = "172.16.101.0/24"
},
routes = null
}'''
_, resources = plan_runner(psa_config=psa_config)
assert len(resources) == 5
for r in resources:
if r['type'] == 'google_compute_network_peering_routes_config':
assert not r['values']['export_custom_routes']
assert not r['values']['import_custom_routes']
def test_routes_export(plan_runner):
"Test routes export."
psa_config = '''{
ranges = {
bar = "172.16.100.0/24"
}
export_routes = true
import_routes = false
}'''
_, resources = plan_runner(psa_config=psa_config)
assert len(resources) == 4
for r in resources:
if r['type'] == 'google_compute_network_peering_routes_config':
assert r['values']['export_custom_routes']
assert not r['values']['import_custom_routes']
def test_routes_import(plan_runner):
"Test routes import."
psa_config = '''{
ranges = {
bar = "172.16.100.0/24"
},
export_routes = false
import_routes = true
}'''
_, resources = plan_runner(psa_config=psa_config)
for r in resources:
if r['type'] == 'google_compute_network_peering_routes_config':
assert not r['values']['export_custom_routes']
assert r['values']['import_custom_routes']
def test_routes_export_import(plan_runner):
"Test routes export and import."
psa_config = '''{
ranges = {
bar = "172.16.100.0/24"
},
export_routes = true
import_routes = true
}'''
_, resources = plan_runner(psa_config=psa_config)
for r in resources:
if r['type'] == 'google_compute_network_peering_routes_config':
assert r['values']['export_custom_routes']
assert r['values']['import_custom_routes']

View File

@@ -1,71 +0,0 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
DATA_FOLDER = "data"
def test_subnet_factory(plan_runner):
"Test subnet factory."
_, resources = plan_runner(data_folder=DATA_FOLDER)
assert len(resources) == 3
subnets = [
r['values'] for r in resources if r['type'] == 'google_compute_subnetwork'
]
assert {s['name'] for s in subnets} == {'factory-subnet', 'factory-subnet2'}
assert {len(s['secondary_ip_range']) for s in subnets} == {0, 1}
assert {s['private_ip_google_access'] for s in subnets} == {True, False}
def test_subnets(plan_runner):
"Test subnets variable."
_, resources = plan_runner(tf_var_file='test.subnets.tfvars')
assert len(resources) == 7
subnets = [
r['values'] for r in resources if r['type'] == 'google_compute_subnetwork'
]
assert {s['name'] for s in subnets} == {'a', 'b', 'c', 'd'}
assert {len(s['secondary_ip_range']) for s in subnets} == {0, 0, 2, 0}
log_config = {s['name']: s['log_config'] for s in subnets if s['log_config']}
assert log_config == {
'd': [{
'aggregation_interval': 'INTERVAL_10_MIN',
'filter_expr': 'true',
'flow_sampling': 0.5,
'metadata': 'INCLUDE_ALL_METADATA',
'metadata_fields': None
}]
}
bindings = {
r['index']: r['values']
for r in resources
if r['type'] == 'google_compute_subnetwork_iam_binding'
}
assert bindings == {
'europe-west1/a.roles/compute.networkUser': {
'condition': [],
'members': ['group:g-a@example.com', 'user:a@example.com'],
'project': 'test-project',
'region': 'europe-west1',
'role': 'roles/compute.networkUser',
'subnetwork': 'a'
},
'europe-west1/c.roles/compute.networkUser': {
'condition': [],
'members': ['group:g-c@example.com', 'user:c@example.com'],
'project': 'test-project',
'region': 'europe-west1',
'role': 'roles/compute.networkUser',
'subnetwork': 'c'
},
}

View File

@@ -0,0 +1,47 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pytest
_route_parameters = [('gateway', 'global/gateways/default-internet-gateway'),
('instance', 'zones/europe-west1-b/test'),
('ip', '192.168.0.128'),
('ilb', 'regions/europe-west1/forwardingRules/test'),
('vpn_tunnel', 'regions/europe-west1/vpnTunnels/foo')]
@pytest.mark.parametrize('next_hop_type,next_hop', _route_parameters)
def test_vpc_routes(plan_summary, next_hop_type, next_hop):
'Test vpc routes.'
var_routes = '''{
next-hop = {
dest_range = "192.168.128.0/24"
tags = null
next_hop_type = "%s"
next_hop = "%s"
}
gateway = {
dest_range = "0.0.0.0/0",
priority = 100
tags = ["tag-a"]
next_hop_type = "gateway",
next_hop = "global/gateways/default-internet-gateway"
}
}''' % (next_hop_type, next_hop)
summary = plan_summary('modules/net-vpc', tf_var_files=['common.tfvars'],
routes=var_routes)
assert len(summary.values) == 3
route = summary.values[f'google_compute_route.{next_hop_type}["next-hop"]']
assert route[f'next_hop_{next_hop_type}'] == next_hop

View File

@@ -12,10 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import pytest
module: modules/net-vpc
common_tfvars:
- common.tfvars
def pytest_collection_modifyitems(config, items):
for item in items:
item.add_marker(
pytest.mark.xdist_group(name='/'.join(item.path.parent.parts[-2:])))
tests:
simple:
subnets:
peering:
shared_vpc:
factory:
psa_simple:
psa_routes_export:
psa_routes_import:
psa_routes_import_export:

View File

@@ -0,0 +1,6 @@
iam_audit_config = {
allServices = {
DATA_READ = [],
DATA_WRITE = ["user:me@example.org"]
}
}

View File

@@ -12,9 +12,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import pytest
def pytest_collection_modifyitems(config, items):
for item in items:
item.add_marker(pytest.mark.xdist_group(name=item.path.parent.name))
counts:
google_organization_iam_audit_config: 1

View File

@@ -0,0 +1 @@
organization_id = "organizations/1234567890"

View File

@@ -0,0 +1,45 @@
firewall_policies = {
policy1 = {
allow-ingress = {
description = ""
direction = "INGRESS"
action = "allow"
priority = 100
ranges = ["10.0.0.0/8"]
ports = {
tcp = ["22"]
}
target_service_accounts = null
target_resources = null
logging = false
}
deny-egress = {
description = ""
direction = "EGRESS"
action = "deny"
priority = 200
ranges = ["192.168.0.0/24"]
ports = {
tcp = ["443"]
}
target_service_accounts = null
target_resources = null
logging = false
}
}
policy2 = {
allow-ingress = {
description = ""
direction = "INGRESS"
action = "allow"
priority = 100
ranges = ["10.0.0.0/8"]
ports = {
tcp = ["22"]
}
target_service_accounts = null
target_resources = null
logging = false
}
}
}

View File

@@ -0,0 +1,73 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_firewall_policy.policy["policy1"]:
parent: organizations/1234567890
short_name: policy1
google_compute_firewall_policy.policy["policy2"]:
parent: organizations/1234567890
short_name: policy2
google_compute_firewall_policy_rule.rule["policy1-allow-ingress"]:
action: allow
direction: INGRESS
disabled: null
enable_logging: false
match:
- dest_ip_ranges: null
layer4_configs:
- ip_protocol: tcp
ports:
- '22'
src_ip_ranges:
- 10.0.0.0/8
priority: 100
target_resources: null
target_service_accounts: null
google_compute_firewall_policy_rule.rule["policy1-deny-egress"]:
action: deny
direction: EGRESS
disabled: null
enable_logging: false
match:
- dest_ip_ranges:
- 192.168.0.0/24
layer4_configs:
- ip_protocol: tcp
ports:
- '443'
src_ip_ranges: null
priority: 200
target_resources: null
target_service_accounts: null
google_compute_firewall_policy_rule.rule["policy2-allow-ingress"]:
action: allow
direction: INGRESS
disabled: null
enable_logging: false
match:
- dest_ip_ranges: null
layer4_configs:
- ip_protocol: tcp
ports:
- '22'
src_ip_ranges:
- 10.0.0.0/8
priority: 100
target_resources: null
target_service_accounts: null
counts:
google_compute_firewall_policy: 2
google_compute_firewall_policy_rule: 3

View File

@@ -0,0 +1,5 @@
firewall_policy_factory = {
cidr_file = "../../tests/modules/organization/data/firewall-cidrs.yaml"
policy_name = "factory-1"
rules_file = "../../tests/modules/organization/data/firewall-rules.yaml"
}

View File

@@ -0,0 +1,61 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_firewall_policy.policy["factory-1"]:
description: null
parent: organizations/1234567890
short_name: factory-1
timeouts: null
google_compute_firewall_policy_rule.rule["factory-1-allow-admins"]:
action: allow
description: Access from the admin subnet to all subnets
direction: INGRESS
disabled: null
enable_logging: null
match:
- dest_ip_ranges: null
layer4_configs:
- ip_protocol: all
ports: []
src_ip_ranges:
- 10.0.0.0/8
- 172.168.0.0/12
- 192.168.0.0/16
priority: 1000
target_resources: null
target_service_accounts: null
timeouts: null
google_compute_firewall_policy_rule.rule["factory-1-allow-ssh-from-iap"]:
action: allow
description: Enable SSH from IAP
direction: INGRESS
disabled: null
enable_logging: null
match:
- dest_ip_ranges: null
layer4_configs:
- ip_protocol: tcp
ports:
- '22'
src_ip_ranges:
- 35.235.240.0/20
priority: 1002
target_resources: null
target_service_accounts: null
timeouts: null
counts:
google_compute_firewall_policy: 1
google_compute_firewall_policy_rule: 2

View File

@@ -0,0 +1 @@
# skip boilerplate check

View File

@@ -0,0 +1,27 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_compute_firewall_policy.policy["factory-1"]: {}
google_compute_firewall_policy.policy["policy1"]: {}
google_compute_firewall_policy.policy["policy2"]: {}
google_compute_firewall_policy_rule.rule["factory-1-allow-admins"]: {}
google_compute_firewall_policy_rule.rule["factory-1-allow-ssh-from-iap"]: {}
google_compute_firewall_policy_rule.rule["policy1-allow-ingress"]: {}
google_compute_firewall_policy_rule.rule["policy1-deny-egress"]: {}
google_compute_firewall_policy_rule.rule["policy2-allow-ingress"]: {}
counts:
google_compute_firewall_policy: 3
google_compute_firewall_policy_rule: 5

View File

@@ -1,5 +0,0 @@
network_tags = {
net_environment = {
network = "foobar"
}
}

View File

@@ -0,0 +1,18 @@
group_iam = {
"owners@example.org" = [
"roles/owner",
"roles/resourcemanager.folderAdmin"
],
"viewers@example.org" = [
"roles/viewer"
]
}
iam = {
"roles/owner" = [
"user:one@example.org",
"user:two@example.org"
],
"roles/browser" = [
"domain:example.org"
]
}

View File

@@ -0,0 +1,44 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_organization_iam_binding.authoritative["roles/browser"]:
condition: []
members:
- domain:example.org
org_id: '1234567890'
role: roles/browser
google_organization_iam_binding.authoritative["roles/owner"]:
condition: []
members:
- group:owners@example.org
- user:one@example.org
- user:two@example.org
org_id: '1234567890'
role: roles/owner
google_organization_iam_binding.authoritative["roles/resourcemanager.folderAdmin"]:
condition: []
members:
- group:owners@example.org
org_id: '1234567890'
role: roles/resourcemanager.folderAdmin
google_organization_iam_binding.authoritative["roles/viewer"]:
condition: []
members:
- group:viewers@example.org
org_id: '1234567890'
role: roles/viewer
counts:
google_organization_iam_binding: 4

View File

@@ -0,0 +1,4 @@
iam = {
"user:one@example.org" = ["roles/owner"],
"user:two@example.org" = ["roles/owner", "roles/editor"]
}

View File

@@ -0,0 +1,31 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_organization_iam_binding.authoritative["user:one@example.org"]:
condition: []
members:
- roles/owner
org_id: '1234567890'
role: user:one@example.org
google_organization_iam_binding.authoritative["user:two@example.org"]:
condition: []
members:
- roles/editor
- roles/owner
org_id: '1234567890'
role: user:two@example.org
counts:
google_organization_iam_binding: 2

View File

@@ -0,0 +1,29 @@
logging_sinks = {
warning = {
destination = "mybucket"
type = "storage"
filter = "severity=WARNING"
}
info = {
destination = "projects/myproject/datasets/mydataset"
type = "bigquery"
filter = "severity=INFO"
disabled = true
}
notice = {
destination = "projects/myproject/topics/mytopic"
type = "pubsub"
filter = "severity=NOTICE"
include_children = false
}
debug = {
destination = "projects/myproject/locations/global/buckets/mybucket"
type = "logging"
filter = "severity=DEBUG"
include_children = false
exclusions = {
no-compute = "logName:compute"
no-container = "logName:container"
}
}
}

View File

@@ -0,0 +1,86 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_bigquery_dataset_iam_member.bq-sinks-binding["info"]:
condition: []
dataset_id: mydataset
project: myproject
role: roles/bigquery.dataEditor
google_logging_organization_sink.sink["debug"]:
description: debug (Terraform-managed).
destination: logging.googleapis.com/projects/myproject/locations/global/buckets/mybucket
disabled: false
exclusions:
- description: null
disabled: false
filter: logName:compute
name: no-compute
- description: null
disabled: false
filter: logName:container
name: no-container
filter: severity=DEBUG
include_children: false
name: debug
org_id: '1234567890'
google_logging_organization_sink.sink["info"]:
description: info (Terraform-managed).
destination: bigquery.googleapis.com/projects/myproject/datasets/mydataset
disabled: true
exclusions: []
filter: severity=INFO
include_children: true
name: info
org_id: '1234567890'
google_logging_organization_sink.sink["notice"]:
description: notice (Terraform-managed).
destination: pubsub.googleapis.com/projects/myproject/topics/mytopic
disabled: false
exclusions: []
filter: severity=NOTICE
include_children: false
name: notice
org_id: '1234567890'
google_logging_organization_sink.sink["warning"]:
description: warning (Terraform-managed).
destination: storage.googleapis.com/mybucket
disabled: false
exclusions: []
filter: severity=WARNING
include_children: true
name: warning
org_id: '1234567890'
google_project_iam_member.bucket-sinks-binding["debug"]:
condition:
- expression: resource.name.endsWith('projects/myproject/locations/global/buckets/mybucket')
title: debug bucket writer
project: myproject
role: roles/logging.bucketWriter
google_pubsub_topic_iam_member.pubsub-sinks-binding["notice"]:
condition: []
project: myproject
role: roles/pubsub.publisher
topic: mytopic
google_storage_bucket_iam_member.storage-sinks-binding["warning"]:
bucket: mybucket
condition: []
role: roles/storage.objectCreator
counts:
google_bigquery_dataset_iam_member: 1
google_logging_organization_sink: 4
google_project_iam_member: 1
google_pubsub_topic_iam_member: 1
google_storage_bucket_iam_member: 1

View File

@@ -0,0 +1,4 @@
logging_exclusions = {
exclusion1 = "resource.type=gce_instance"
exclusion2 = "severity=NOTICE"
}

View File

@@ -0,0 +1,30 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_logging_organization_exclusion.logging-exclusion["exclusion1"]:
description: exclusion1 (Terraform-managed).
disabled: null
filter: resource.type=gce_instance
name: exclusion1
org_id: '1234567890'
google_logging_organization_exclusion.logging-exclusion["exclusion2"]:
description: exclusion2 (Terraform-managed).
disabled: null
filter: severity=NOTICE
name: exclusion2
org_id: '1234567890'
counts:
google_logging_organization_exclusion: 2

View File

@@ -0,0 +1,53 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_org_policy_policy.default["iam.disableServiceAccountKeyCreation"]:
name: organizations/1234567890/policies/iam.disableServiceAccountKeyCreation
parent: organizations/1234567890
spec:
- inherit_from_parent: null
reset: null
rules:
- allow_all: null
condition: []
deny_all: null
enforce: 'TRUE'
values: []
timeouts: null
google_org_policy_policy.default["iam.disableServiceAccountKeyUpload"]:
name: organizations/1234567890/policies/iam.disableServiceAccountKeyUpload
parent: organizations/1234567890
spec:
- inherit_from_parent: null
reset: null
rules:
- allow_all: null
condition: []
deny_all: null
enforce: 'FALSE'
values: []
- allow_all: null
condition:
- description: test condition
expression: resource.matchTagId(aa, bb)
location: xxx
title: condition
deny_all: null
enforce: 'TRUE'
values: []
timeouts: null
counts:
google_org_policy_policy: 2

View File

@@ -0,0 +1,37 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_org_policy_custom_constraint.constraint["custom.dataprocNoMoreThan10Workers"]:
action_type: DENY
condition: resource.config.workerConfig.numInstances + resource.config.secondaryWorkerConfig.numInstances > 10
method_types:
- CREATE
- UPDATE
name: custom.dataprocNoMoreThan10Workers
parent: organizations/1234567890
resource_types:
- dataproc.googleapis.com/Cluster
google_org_policy_custom_constraint.constraint["custom.gkeEnableAutoUpgrade"]:
action_type: ALLOW
condition: resource.management.autoUpgrade == true
method_types:
- CREATE
name: custom.gkeEnableAutoUpgrade
parent: organizations/1234567890
resource_types:
- container.googleapis.com/NodePool
counts:
google_org_policy_custom_constraint: 2

View File

@@ -3,6 +3,7 @@ org_policies = {
deny = { all = true }
}
"iam.allowedPolicyMemberDomains" = {
inherit_from_parent = true
allow = {
values = ["C0xxxxxxx", "C0yyyyyyy"]
}

View File

@@ -0,0 +1,85 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_org_policy_policy.default["compute.restrictLoadBalancerCreationForTypes"]:
name: organizations/1234567890/policies/compute.restrictLoadBalancerCreationForTypes
parent: organizations/1234567890
spec:
- inherit_from_parent: null
reset: null
rules:
- allow_all: null
condition: []
deny_all: null
enforce: null
values:
- allowed_values: null
denied_values:
- in:EXTERNAL
- allow_all: null
condition:
- description: test condition
expression: resource.matchTagId(aa, bb)
location: xxx
title: condition
deny_all: null
enforce: null
values:
- allowed_values:
- EXTERNAL_1
denied_values: null
- allow_all: 'TRUE'
condition:
- description: test condition2
expression: resource.matchTagId(cc, dd)
location: xxx
title: condition2
deny_all: null
enforce: null
values: []
timeouts: null
google_org_policy_policy.default["compute.vmExternalIpAccess"]:
name: organizations/1234567890/policies/compute.vmExternalIpAccess
parent: organizations/1234567890
spec:
- inherit_from_parent: null
reset: null
rules:
- allow_all: null
condition: []
deny_all: 'TRUE'
enforce: null
values: []
timeouts: null
google_org_policy_policy.default["iam.allowedPolicyMemberDomains"]:
name: organizations/1234567890/policies/iam.allowedPolicyMemberDomains
parent: organizations/1234567890
spec:
- inherit_from_parent: true
reset: null
rules:
- allow_all: null
condition: []
deny_all: null
enforce: null
values:
- allowed_values:
- C0xxxxxxx
- C0yyyyyyy
denied_values: null
timeouts: null
counts:
google_org_policy_policy: 3

View File

@@ -1,3 +1,8 @@
network_tags = {
net_environment = {
network = "foobar"
}
}
tags = {
foo = {}
bar = {

View File

@@ -0,0 +1,76 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
values:
google_tags_tag_key.default["bar"]:
description: Managed by the Terraform organization module.
parent: organizations/1234567890
purpose: null
purpose_data: null
short_name: bar
google_tags_tag_key.default["foo"]:
description: Managed by the Terraform organization module.
parent: organizations/1234567890
purpose: null
purpose_data: null
short_name: foo
google_tags_tag_key.default["foobar"]:
description: Foobar tag.
parent: organizations/1234567890
purpose: null
purpose_data: null
short_name: foobar
google_tags_tag_key.default["net_environment"]:
description: Managed by the Terraform organization module.
parent: organizations/1234567890
purpose: GCE_FIREWALL
purpose_data:
network: foobar
short_name: net_environment
google_tags_tag_key_iam_binding.default["foobar:roles/resourcemanager.tagAdmin"]:
condition: []
members:
- user:user1@example.com
- user:user2@example.com
role: roles/resourcemanager.tagAdmin
google_tags_tag_value.default["foobar/one"]:
description: Managed by the Terraform organization module.
short_name: one
google_tags_tag_value.default["foobar/three"]:
description: Foobar 3.
short_name: three
google_tags_tag_value.default["foobar/two"]:
description: Foobar 2.
short_name: two
google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagAdmin"]:
condition: []
members:
- user:user4@example.com
role: roles/resourcemanager.tagAdmin
google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagViewer"]:
condition: []
members:
- user:user3@example.com
role: roles/resourcemanager.tagViewer
google_tags_tag_value_iam_binding.default["foobar/two:roles/resourcemanager.tagViewer"]:
condition: []
members:
- user:user3@example.com
role: roles/resourcemanager.tagViewer
counts:
google_tags_tag_key: 4
google_tags_tag_key_iam_binding: 1
google_tags_tag_value: 3
google_tags_tag_value_iam_binding: 3

View File

@@ -1,62 +0,0 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
def test_audit_config(plan_runner):
"Test audit config."
iam_audit_config = '{allServices={DATA_READ=[], DATA_WRITE=["user:me@example.org"]}}'
_, resources = plan_runner(iam_audit_config=iam_audit_config)
assert len(resources) == 1
log_types = set(
r['log_type'] for r in resources[0]['values']['audit_log_config'])
assert log_types == set(['DATA_READ', 'DATA_WRITE'])
def test_iam(plan_runner):
"Test IAM."
group_iam = (
'{'
'"owners@example.org" = ["roles/owner", "roles/resourcemanager.folderAdmin"],'
'"viewers@example.org" = ["roles/viewer"]'
'}')
iam = ('{'
'"roles/owner" = ["user:one@example.org", "user:two@example.org"],'
'"roles/browser" = ["domain:example.org"]'
'}')
_, resources = plan_runner(group_iam=group_iam, iam=iam)
roles = sorted([(r['values']['role'], sorted(r['values']['members']))
for r in resources
if r['type'] == 'google_organization_iam_binding'])
assert roles == [
('roles/browser', ['domain:example.org']),
('roles/owner', [
'group:owners@example.org', 'user:one@example.org',
'user:two@example.org'
]),
('roles/resourcemanager.folderAdmin', ['group:owners@example.org']),
('roles/viewer', ['group:viewers@example.org']),
]
def test_iam_additive_members(plan_runner):
"Test IAM additive members."
iam = ('{"user:one@example.org" = ["roles/owner"],'
'"user:two@example.org" = ["roles/owner", "roles/editor"]}')
_, resources = plan_runner(iam_additive_members=iam)
roles = set((r['values']['role'], r['values']['member'])
for r in resources
if r['type'] == 'google_organization_iam_member')
assert roles == set([('roles/owner', 'user:one@example.org'),
('roles/owner', 'user:two@example.org'),
('roles/editor', 'user:two@example.org')])

View File

@@ -1,130 +0,0 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
_FACTORY = '''
{
cidr_file = "data/firewall-cidrs.yaml"
policy_name = "factory-1"
rules_file = "data/firewall-rules.yaml"
}
'''
_POLICIES = '''
{
policy1 = {
allow-ingress = {
description = ""
direction = "INGRESS"
action = "allow"
priority = 100
ranges = ["10.0.0.0/8"]
ports = {
tcp = ["22"]
}
target_service_accounts = null
target_resources = null
logging = false
}
deny-egress = {
description = ""
direction = "EGRESS"
action = "deny"
priority = 200
ranges = ["192.168.0.0/24"]
ports = {
tcp = ["443"]
}
target_service_accounts = null
target_resources = null
logging = false
}
}
policy2 = {
allow-ingress = {
description = ""
direction = "INGRESS"
action = "allow"
priority = 100
ranges = ["10.0.0.0/8"]
ports = {
tcp = ["22"]
}
target_service_accounts = null
target_resources = null
logging = false
}
}
}
'''
def test_custom(plan_runner):
'Test custom firewall policies.'
_, resources = plan_runner(firewall_policies=_POLICIES)
assert len(resources) == 5
policies = [r for r in resources
if r['type'] == 'google_compute_firewall_policy']
rules = [r for r in resources
if r['type'] == 'google_compute_firewall_policy_rule']
assert set(r['index'] for r in policies) == set([
'policy1', 'policy2'
])
assert set(r['index'] for r in rules) == set([
'policy1-deny-egress', 'policy2-allow-ingress', 'policy1-allow-ingress'
])
def test_factory(plan_runner):
'Test firewall policy factory.'
_, resources = plan_runner(firewall_policy_factory=_FACTORY)
assert len(resources) == 3
policies = [r for r in resources
if r['type'] == 'google_compute_firewall_policy']
rules = [r for r in resources
if r['type'] == 'google_compute_firewall_policy_rule']
assert set(r['index'] for r in policies) == set([
'factory-1'
])
assert set(r['index'] for r in rules) == set([
'factory-1-allow-admins', 'factory-1-allow-ssh-from-iap'
])
def test_factory_name(plan_runner):
'Test firewall policy factory default name.'
factory = _FACTORY.replace('"factory-1"', 'null')
_, resources = plan_runner(firewall_policy_factory=factory)
assert len(resources) == 3
policies = [r for r in resources
if r['type'] == 'google_compute_firewall_policy']
assert set(r['index'] for r in policies) == set([
'factory'
])
def test_combined(plan_runner):
'Test combined rules.'
_, resources = plan_runner(firewall_policies=_POLICIES,
firewall_policy_factory=_FACTORY)
assert len(resources) == 8
policies = [r for r in resources
if r['type'] == 'google_compute_firewall_policy']
rules = [r for r in resources
if r['type'] == 'google_compute_firewall_policy_rule']
assert set(r['index'] for r in policies) == set([
'factory-1', 'policy1', 'policy2'
])
assert set(r['index'] for r in rules) == set([
'factory-1-allow-admins', 'factory-1-allow-ssh-from-iap',
'policy1-deny-egress', 'policy2-allow-ingress', 'policy1-allow-ingress'
])

View File

@@ -1,126 +0,0 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import Counter
def test_sinks(plan_runner):
"Test folder-level sinks."
tfvars = 'test.logging-sinks.tfvars'
_, resources = plan_runner(tf_var_file=tfvars)
assert len(resources) == 8
resource_types = Counter([r["type"] for r in resources])
assert resource_types == {
"google_logging_organization_sink": 4,
"google_bigquery_dataset_iam_member": 1,
"google_project_iam_member": 1,
"google_pubsub_topic_iam_member": 1,
"google_storage_bucket_iam_member": 1,
}
sinks = [
r for r in resources if r["type"] == "google_logging_organization_sink"
]
assert sorted([r["index"] for r in sinks]) == [
"debug",
"info",
"notice",
"warning",
]
values = [(
r["index"],
r["values"]["filter"],
r["values"]["destination"],
r["values"]["include_children"],
) for r in sinks]
assert sorted(values) == [
(
"debug",
"severity=DEBUG",
"logging.googleapis.com/projects/myproject/locations/global/buckets/mybucket",
False,
),
(
"info",
"severity=INFO",
"bigquery.googleapis.com/projects/myproject/datasets/mydataset",
True,
),
(
"notice",
"severity=NOTICE",
"pubsub.googleapis.com/projects/myproject/topics/mytopic",
False,
),
("warning", "severity=WARNING", "storage.googleapis.com/mybucket", True),
]
bindings = [r for r in resources if "member" in r["type"]]
values = [(r["index"], r["type"], r["values"]["role"]) for r in bindings]
assert sorted(values) == [
("debug", "google_project_iam_member", "roles/logging.bucketWriter"),
("info", "google_bigquery_dataset_iam_member",
"roles/bigquery.dataEditor"),
("notice", "google_pubsub_topic_iam_member", "roles/pubsub.publisher"),
("warning", "google_storage_bucket_iam_member",
"roles/storage.objectCreator"),
]
exclusions = [(r["index"], r["values"]["exclusions"]) for r in sinks]
assert sorted(exclusions) == [
(
"debug",
[
{
"description": None,
"disabled": False,
"filter": "logName:compute",
"name": "no-compute",
},
{
"description": None,
"disabled": False,
"filter": "logName:container",
"name": "no-container",
},
],
),
("info", []),
("notice", []),
("warning", []),
]
def test_exclusions(plan_runner):
"Test folder-level logging exclusions."
logging_exclusions = ("{"
'exclusion1 = "resource.type=gce_instance", '
'exclusion2 = "severity=NOTICE", '
"}")
_, resources = plan_runner(logging_exclusions=logging_exclusions)
assert len(resources) == 2
exclusions = [
r for r in resources
if r["type"] == "google_logging_organization_exclusion"
]
assert sorted([r["index"] for r in exclusions]) == [
"exclusion1",
"exclusion2",
]
values = [(r["index"], r["values"]["filter"]) for r in exclusions]
assert sorted(values) == [
("exclusion1", "resource.type=gce_instance"),
("exclusion2", "severity=NOTICE"),
]

View File

@@ -14,46 +14,32 @@
import pathlib
from .validate_policies import validate_policy_boolean, validate_policy_list, validate_policy_custom_constraints
import pytest
_params = ['boolean', 'list']
def test_policy_boolean(plan_runner):
"Test boolean org policy."
tfvars = 'test.orgpolicies-boolean.tfvars'
_, resources = plan_runner(tf_var_file=tfvars)
validate_policy_boolean(resources)
def test_policy_list(plan_runner):
"Test list org policy."
tfvars = 'test.orgpolicies-list.tfvars'
_, resources = plan_runner(tf_var_file=tfvars)
validate_policy_list(resources)
def test_policy_custom_constraints(plan_runner):
"Test org policy custom constraints."
tfvars = 'test.orgpolicy-custom-constraints.tfvars'
_, resources = plan_runner(tf_var_file=tfvars)
validate_policy_custom_constraints(resources)
def test_factory_policy_boolean(plan_runner, tfvars_to_yaml, tmp_path):
@pytest.mark.parametrize('policy_type', _params)
def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type):
dest = tmp_path / 'policies.yaml'
tfvars_to_yaml('test.orgpolicies-boolean.tfvars', dest, 'org_policies')
_, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"')
validate_policy_boolean(resources)
tfvars_to_yaml(f'org_policies_{policy_type}.tfvars', dest, 'org_policies')
tfvars_plan = plan_summary(
'modules/organization',
tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars'])
yaml_plan = plan_summary('modules/organization',
tf_var_files=['common.tfvars'],
org_policies_data_path=f'{tmp_path}')
assert tfvars_plan.values == yaml_plan.values
def test_factory_policy_list(plan_runner, tfvars_to_yaml, tmp_path):
dest = tmp_path / 'policies.yaml'
tfvars_to_yaml('test.orgpolicies-list.tfvars', dest, 'org_policies')
_, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"')
validate_policy_list(resources)
def test_factory_policy_custom_constraints(plan_runner, tfvars_to_yaml, tmp_path):
def test_custom_constraint_factory(plan_summary, tfvars_to_yaml, tmp_path):
dest = tmp_path / 'constraints.yaml'
tfvars_to_yaml('test.orgpolicy-custom-constraints.tfvars', dest, 'org_policy_custom_constraints')
_, resources = plan_runner(org_policy_custom_constraints_data_path=f'"{tmp_path}"')
validate_policy_custom_constraints(resources)
tfvars_to_yaml(f'org_policies_custom_constraints.tfvars', dest,
'org_policy_custom_constraints')
tfvars_plan = plan_summary(
'modules/organization',
tf_var_files=['common.tfvars', f'org_policies_custom_constraints.tfvars'])
yaml_plan = plan_summary(
'modules/organization', tf_var_files=['common.tfvars'],
org_policy_custom_constraints_data_path=f'{tmp_path}')
assert tfvars_plan.values == yaml_plan.values

View File

@@ -1,52 +0,0 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
def test_resource_tags(plan_runner):
'Test resource tags.'
_, resources = plan_runner(tf_var_file='test.resource_tags.tfvars')
assert len(resources) == 10
resource_values = {}
for r in resources:
resource_values.setdefault(r['type'], []).append(r['values'])
assert len(resource_values['google_tags_tag_key']) == 3
assert len(resource_values['google_tags_tag_value']) == 3
result = [
r['role'] for r in resource_values['google_tags_tag_value_iam_binding']
]
expected = [
'roles/resourcemanager.tagAdmin',
'roles/resourcemanager.tagViewer',
'roles/resourcemanager.tagViewer'
]
assert result == expected
def test_network_tags(plan_runner):
'Test network tags.'
_, resources = plan_runner(tf_var_file='test.network_tags.tfvars')
assert len(resources) == 1
resource_values = {}
for r in resources:
resource_values.setdefault(r['type'], []).append(r['values'])
google_tags_tag_key = resource_values['google_tags_tag_key'][0]
assert google_tags_tag_key['purpose'] == "GCE_FIREWALL"
assert google_tags_tag_key['purpose_data']['network'] == "foobar"
def test_bindings(plan_runner):
'Test tag bindings.'
tag_bindings = '{foo = "tagValues/123456789012"}'
_, resources = plan_runner(tag_bindings=tag_bindings)
assert len(resources) == 1

View File

@@ -0,0 +1,35 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module: modules/organization
common_tfvars:
- common.tfvars
tests:
audit_config:
iam:
iam_additive:
logging:
logging_exclusions:
org_policies_list:
org_policies_boolean:
org_policies_custom_constraints:
tags:
firewall_policies:
firewall_policies_factory:
firewall_policies_factory_combined:
tfvars:
- firewall_policies.tfvars
- firewall_policies_factory.tfvars

View File

@@ -31,13 +31,14 @@ def test_policy_list(plan_runner):
def test_factory_policy_boolean(plan_runner, tfvars_to_yaml, tmp_path):
dest = tmp_path / 'policies.yaml'
tfvars_to_yaml('test.orgpolicies-boolean.tfvars', dest, 'org_policies')
tfvars_to_yaml('fixture/test.orgpolicies-boolean.tfvars', dest,
'org_policies')
_, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"')
validate_policy_boolean(resources)
def test_factory_policy_list(plan_runner, tfvars_to_yaml, tmp_path):
dest = tmp_path / 'policies.yaml'
tfvars_to_yaml('test.orgpolicies-list.tfvars', dest, 'org_policies')
tfvars_to_yaml('fixture/test.orgpolicies-list.tfvars', dest, 'org_policies')
_, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"')
validate_policy_list(resources)

View File

@@ -1,6 +1,6 @@
pytest>=6.2.5
PyYAML>=6.0
tftest>=1.7.6
tftest>=1.8.1
marko>=1.2.0
deepdiff>=5.7.0
python-hcl2>=3.0.5

44
tools/plan_summary.py Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python3
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import click
import sys
import yaml
from pathlib import Path
BASEDIR = Path(__file__).parents[1]
sys.path.append(str(BASEDIR / 'tests'))
import fixtures
@click.command()
@click.argument('module', type=click.Path(), nargs=1)
@click.argument('tfvars', type=click.Path(exists=True), nargs=-1)
def main(module, tfvars):
module = BASEDIR / module
summary = fixtures.plan_summary(module, Path(), tfvars)
print(yaml.dump({'values': summary.values}))
print(yaml.dump({'counts': summary.counts}))
outputs = {
k: v.get('value', '__missing__') for k, v in summary.outputs.items()
}
print(yaml.dump({'outputs': outputs}))
if __name__ == '__main__':
main()