Add a pytest fixture to convert tfvars to yaml (#950)

* wip

* conftest and tests

* backport cross-module implementation test
This commit is contained in:
Ludovico Magnocavallo 2022-11-04 18:37:23 +01:00 committed by GitHub
parent fae5654e33
commit d655833c0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 340 additions and 309 deletions

View File

@ -12,9 +12,45 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import pathlib
import hcl2
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():
def converter(source, dest, from_var, to_var=None):
p_fixture = pathlib.Path(inspect.stack()[1].filename).parent / 'fixture'
p_source = p_fixture / source
if not p_source.exists():
raise ValueError(f"tfvars '{source}' not found")
try:
with p_source.open() as f:
data = hcl2.load(f)
except Exception as e:
raise ValueError(f'error decoding tfvars: {e.args[0]}')
if from_var not in data:
raise ValueError(f"variable '{from_var}' not in tfvars")
if to_var is None:
data_yaml = data[from_var]
else:
data_yaml = {to_var: data[from_var]}
p_dest = pathlib.Path(dest) if not isinstance(dest, pathlib.Path) else dest
try:
with p_dest.open('w') as f:
data_yaml = yaml.dump(data_yaml, f)
except yaml.YAMLError as e:
raise ValueError(f'error encoding data to yaml: {e.args[0]}')
except (IOError, OSError) as e:
raise ValueError(f"error writing '{dest}': {e.args[0]}")
return converter

View File

@ -0,0 +1,19 @@
org_policies = {
"iam.disableServiceAccountKeyCreation" = {
enforce = true
}
"iam.disableServiceAccountKeyUpload" = {
enforce = false
rules = [
{
condition = {
expression = "resource.matchTagId(aa, bb)"
title = "condition"
description = "test condition"
location = "xxx"
}
enforce = true
}
]
}
}

View File

@ -0,0 +1,37 @@
org_policies = {
"compute.vmExternalIpAccess" = {
deny = { all = true }
}
"iam.allowedPolicyMemberDomains" = {
allow = {
values = ["C0xxxxxxx", "C0yyyyyyy"]
}
}
"compute.restrictLoadBalancerCreationForTypes" = {
deny = { values = ["in:EXTERNAL"] }
rules = [
{
condition = {
expression = "resource.matchTagId(aa, bb)"
title = "condition"
description = "test condition"
location = "xxx"
}
allow = {
values = ["EXTERNAL_1"]
}
},
{
condition = {
expression = "resource.matchTagId(cc, dd)"
title = "condition2"
description = "test condition2"
location = "xxx"
}
allow = {
all = true
}
}
]
}
}

View File

@ -12,327 +12,34 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import difflib
from pathlib import Path
import pathlib
import hcl2
import yaml
BOOLEAN_POLICIES = '''{
"iam.disableServiceAccountKeyCreation" = {
enforce = true
}
"iam.disableServiceAccountKeyUpload" = {
enforce = false
rules = [
{
condition = {
expression = "resource.matchTagId(aa, bb)"
title = "condition"
description = "test condition"
location = "xxx"
}
enforce = true
}
]
}
}'''
LIST_POLICIES = '''{
"compute.vmExternalIpAccess" = {
deny = { all = true }
}
"iam.allowedPolicyMemberDomains" = {
allow = {
values = ["C0xxxxxxx", "C0yyyyyyy"]
}
}
"compute.restrictLoadBalancerCreationForTypes" = {
deny = { values = ["in:EXTERNAL"] }
rules = [
{
condition = {
expression = "resource.matchTagId(aa, bb)"
title = "condition"
description = "test condition"
location = "xxx"
}
allow = {
values = ["EXTERNAL_1"]
}
},
{
condition = {
expression = "resource.matchTagId(cc, dd)"
title = "condition2"
description = "test condition2"
location = "xxx"
}
allow = {
all = true
}
}
]
}
}'''
from .validate_policies import validate_policy_boolean, validate_policy_list
def test_policy_boolean(plan_runner):
"Test boolean org policy."
_, resources = plan_runner(org_policies=BOOLEAN_POLICIES)
validate_policy_boolean_resources(resources)
tfvars = 'test.orgpolicies-boolean.tfvars'
_, resources = plan_runner(extra_files=[tfvars], tf_var_file=tfvars)
validate_policy_boolean(resources)
def test_policy_list(plan_runner):
"Test list org policy."
_, resources = plan_runner(org_policies=LIST_POLICIES)
validate_policy_list_resources(resources)
tfvars = 'test.orgpolicies-list.tfvars'
_, resources = plan_runner(extra_files=[tfvars], tf_var_file=tfvars)
validate_policy_list(resources)
def test_policy_boolean_factory(plan_runner, tmp_path):
# convert hcl policies to yaml
hcl_policies = f'p = {BOOLEAN_POLICIES}'
yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p'])
yaml_file = tmp_path / 'policies.yaml'
yaml_file.write_text(yaml_policies)
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')
_, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"')
validate_policy_boolean_resources(resources)
validate_policy_boolean(resources)
def test_policy_list_factory(plan_runner, tmp_path):
# convert hcl policies to yaml
hcl_policies = f'p = {LIST_POLICIES}'
yaml_policies = yaml.dump(hcl2.loads(hcl_policies)['p'])
yaml_file = tmp_path / 'policies.yaml'
yaml_file.write_text(yaml_policies)
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(resources)
def validate_policy_boolean_resources(resources):
assert len(resources) == 2
policies = [r for r in resources if r['type'] == 'google_org_policy_policy']
assert len(policies) == 2
assert all(
x['values']['parent'] == 'organizations/1234567890' for x in policies)
p1 = [
r['values']['spec'][0]
for r in policies
if r['index'] == 'iam.disableServiceAccountKeyCreation'
][0]
assert p1['inherit_from_parent'] is None
assert p1['reset'] is None
assert p1['rules'] == [{
'allow_all': None,
'condition': [],
'deny_all': None,
'enforce': 'TRUE',
'values': []
}]
p2 = [
r['values']['spec'][0]
for r in policies
if r['index'] == 'iam.disableServiceAccountKeyUpload'
][0]
assert p2['inherit_from_parent'] is None
assert p2['reset'] is None
assert len(p2['rules']) == 2
assert p2['rules'][0] == {
'allow_all': None,
'condition': [],
'deny_all': None,
'enforce': 'FALSE',
'values': []
}
assert p2['rules'][1] == {
'allow_all': None,
'condition': [{
'description': 'test condition',
'expression': 'resource.matchTagId(aa, bb)',
'location': 'xxx',
'title': 'condition'
}],
'deny_all': None,
'enforce': 'TRUE',
'values': []
}
def validate_policy_list_resources(resources):
assert len(resources) == 3
policies = [r for r in resources if r['type'] == 'google_org_policy_policy']
assert len(policies) == 3
assert all(
x['values']['parent'] == 'organizations/1234567890' for x in policies)
p1 = [
r['values']['spec'][0]
for r in policies
if r['index'] == 'compute.vmExternalIpAccess'
][0]
assert p1['inherit_from_parent'] is None
assert p1['reset'] is None
assert p1['rules'] == [{
'allow_all': None,
'condition': [],
'deny_all': 'TRUE',
'enforce': None,
'values': []
}]
p2 = [
r['values']['spec'][0]
for r in policies
if r['index'] == 'iam.allowedPolicyMemberDomains'
][0]
assert p2['inherit_from_parent'] is None
assert p2['reset'] is None
assert p2['rules'] == [{
'allow_all':
None,
'condition': [],
'deny_all':
None,
'enforce':
None,
'values': [{
'allowed_values': [
'C0xxxxxxx',
'C0yyyyyyy',
],
'denied_values': None
}]
}]
p3 = [
r['values']['spec'][0]
for r in policies
if r['index'] == 'compute.restrictLoadBalancerCreationForTypes'
][0]
assert p3['inherit_from_parent'] is None
assert p3['reset'] is None
assert len(p3['rules']) == 3
assert p3['rules'][0] == {
'allow_all': None,
'condition': [],
'deny_all': None,
'enforce': None,
'values': [{
'allowed_values': None,
'denied_values': ['in:EXTERNAL']
}]
}
assert p3['rules'][1] == {
'allow_all': None,
'condition': [{
'description': 'test condition',
'expression': 'resource.matchTagId(aa, bb)',
'location': 'xxx',
'title': 'condition'
}],
'deny_all': None,
'enforce': None,
'values': [{
'allowed_values': ['EXTERNAL_1'],
'denied_values': None
}]
}
assert p3['rules'][2] == {
'allow_all': 'TRUE',
'condition': [{
'description': 'test condition2',
'expression': 'resource.matchTagId(cc, dd)',
'location': 'xxx',
'title': 'condition2'
}],
'deny_all': None,
'enforce': None,
'values': []
}
def test_policy_implementation(plan_runner):
'''Verify org policy implementation is the same (except minor
differences) in the organization, folder and project modules.'''
modules_path = Path(__file__).parents[3] / 'modules'
lines = {}
for module in ['project', 'folder', 'organization']:
path = modules_path / module / 'organization-policies.tf'
lines[module] = path.open().readlines()
diff1 = difflib.unified_diff(lines['project'], lines['folder'])
assert list(diff1) == [
'--- \n',
'+++ \n',
'@@ -14,7 +14,7 @@\n',
' * limitations under the License.\n',
' */\n',
' \n',
'-# tfdoc:file:description Project-level organization policies.\n',
'+# tfdoc:file:description Folder-level organization policies.\n',
' \n',
' locals {\n',
' _factory_data_raw = (\n',
'@@ -69,8 +69,8 @@\n',
' org_policies = {\n',
' for k, v in local._org_policies :\n',
' k => merge(v, {\n',
'- name = "projects/${local.project.project_id}/policies/${k}"\n',
'- parent = "projects/${local.project.project_id}"\n',
'+ name = "${local.folder.name}/policies/${k}"\n',
'+ parent = local.folder.name\n',
' \n',
' is_boolean_policy = v.allow == null && v.deny == null\n',
' has_values = (\n',
]
diff2 = difflib.unified_diff(lines['folder'], lines['organization'])
assert list(diff2) == [
'--- \n',
'+++ \n',
'@@ -14,7 +14,7 @@\n',
' * limitations under the License.\n',
' */\n',
' \n',
'-# tfdoc:file:description Folder-level organization policies.\n',
'+# tfdoc:file:description Organization-level organization policies.\n',
' \n',
' locals {\n',
' _factory_data_raw = (\n',
'@@ -69,8 +69,8 @@\n',
' org_policies = {\n',
' for k, v in local._org_policies :\n',
' k => merge(v, {\n',
'- name = "${local.folder.name}/policies/${k}"\n',
'- parent = local.folder.name\n',
'+ name = "${var.organization_id}/policies/${k}"\n',
'+ parent = var.organization_id\n',
' \n',
' is_boolean_policy = v.allow == null && v.deny == null\n',
' has_values = (\n',
'@@ -143,4 +143,12 @@\n',
' }\n',
' }\n',
' }\n',
'+\n',
'+ depends_on = [\n',
'+ google_organization_iam_audit_config.config,\n',
'+ google_organization_iam_binding.authoritative,\n',
'+ google_organization_iam_custom_role.roles,\n',
'+ google_organization_iam_member.additive,\n',
'+ google_organization_iam_policy.authoritative,\n',
'+ ]\n',
' }\n',
]
validate_policy_list(resources)

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.
import difflib
import pathlib
def test_policy_implementation():
'''Verify org policy implementation is the same (except minor
differences) in the organization, folder and project modules.'''
modules_path = pathlib.Path(__file__).parents[3] / 'modules'
lines = {}
for module in ['project', 'folder', 'organization']:
path = modules_path / module / 'organization-policies.tf'
lines[module] = path.open().readlines()
diff1 = difflib.unified_diff(lines['project'], lines['folder'])
assert list(diff1) == [
'--- \n',
'+++ \n',
'@@ -14,7 +14,7 @@\n',
' * limitations under the License.\n',
' */\n',
' \n',
'-# tfdoc:file:description Project-level organization policies.\n',
'+# tfdoc:file:description Folder-level organization policies.\n',
' \n',
' locals {\n',
' _factory_data_raw = (\n',
'@@ -69,8 +69,8 @@\n',
' org_policies = {\n',
' for k, v in local._org_policies :\n',
' k => merge(v, {\n',
'- name = "projects/${local.project.project_id}/policies/${k}"\n',
'- parent = "projects/${local.project.project_id}"\n',
'+ name = "${local.folder.name}/policies/${k}"\n',
'+ parent = local.folder.name\n',
' \n',
' is_boolean_policy = v.allow == null && v.deny == null\n',
' has_values = (\n',
]
diff2 = difflib.unified_diff(lines['folder'], lines['organization'])
assert list(diff2) == [
'--- \n',
'+++ \n',
'@@ -14,7 +14,7 @@\n',
' * limitations under the License.\n',
' */\n',
' \n',
'-# tfdoc:file:description Folder-level organization policies.\n',
'+# tfdoc:file:description Organization-level organization policies.\n',
' \n',
' locals {\n',
' _factory_data_raw = (\n',
'@@ -69,8 +69,8 @@\n',
' org_policies = {\n',
' for k, v in local._org_policies :\n',
' k => merge(v, {\n',
'- name = "${local.folder.name}/policies/${k}"\n',
'- parent = local.folder.name\n',
'+ name = "${var.organization_id}/policies/${k}"\n',
'+ parent = var.organization_id\n',
' \n',
' is_boolean_policy = v.allow == null && v.deny == null\n',
' has_values = (\n',
'@@ -143,4 +143,12 @@\n',
' }\n',
' }\n',
' }\n',
'+\n',
'+ depends_on = [\n',
'+ google_organization_iam_audit_config.config,\n',
'+ google_organization_iam_binding.authoritative,\n',
'+ google_organization_iam_custom_role.roles,\n',
'+ google_organization_iam_member.additive,\n',
'+ google_organization_iam_policy.authoritative,\n',
'+ ]\n',
' }\n',
]

View File

@ -0,0 +1,140 @@
# 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 validate_policy_boolean(resources):
assert len(resources) == 2
assert all(
r['values']['parent'] == 'organizations/1234567890' for r in resources)
policies = {
r['index']: r['values']['spec'][0]
for r in resources
if r['type'] == 'google_org_policy_policy'
}
assert len(policies) == 2
p1 = policies['iam.disableServiceAccountKeyCreation']
assert p1['inherit_from_parent'] is None
assert p1['reset'] is None
assert p1['rules'] == [{
'allow_all': None,
'condition': [],
'deny_all': None,
'enforce': 'TRUE',
'values': []
}]
p2 = policies['iam.disableServiceAccountKeyUpload']
assert p2['inherit_from_parent'] is None
assert p2['reset'] is None
assert len(p2['rules']) == 2
assert p2['rules'][0] == {
'allow_all': None,
'condition': [],
'deny_all': None,
'enforce': 'FALSE',
'values': []
}
assert p2['rules'][1] == {
'allow_all': None,
'condition': [{
'description': 'test condition',
'expression': 'resource.matchTagId(aa, bb)',
'location': 'xxx',
'title': 'condition'
}],
'deny_all': None,
'enforce': 'TRUE',
'values': []
}
def validate_policy_list(resources):
assert len(resources) == 3
assert all(
r['values']['parent'] == 'organizations/1234567890' for r in resources)
policies = {
r['index']: r['values']['spec'][0]
for r in resources
if r['type'] == 'google_org_policy_policy'
}
assert len(policies) == 3
p1 = policies['compute.vmExternalIpAccess']
assert p1['inherit_from_parent'] is None
assert p1['reset'] is None
assert p1['rules'] == [{
'allow_all': None,
'condition': [],
'deny_all': 'TRUE',
'enforce': None,
'values': []
}]
p2 = policies['iam.allowedPolicyMemberDomains']
assert p2['inherit_from_parent'] is None
assert p2['reset'] is None
assert p2['rules'] == [{
'allow_all':
None,
'condition': [],
'deny_all':
None,
'enforce':
None,
'values': [{
'allowed_values': [
'C0xxxxxxx',
'C0yyyyyyy',
],
'denied_values': None
}]
}]
p3 = policies['compute.restrictLoadBalancerCreationForTypes']
assert p3['inherit_from_parent'] is None
assert p3['reset'] is None
assert len(p3['rules']) == 3
assert p3['rules'][0] == {
'allow_all': None,
'condition': [],
'deny_all': None,
'enforce': None,
'values': [{
'allowed_values': None,
'denied_values': ['in:EXTERNAL']
}]
}
assert p3['rules'][1] == {
'allow_all': None,
'condition': [{
'description': 'test condition',
'expression': 'resource.matchTagId(aa, bb)',
'location': 'xxx',
'title': 'condition'
}],
'deny_all': None,
'enforce': None,
'values': [{
'allowed_values': ['EXTERNAL_1'],
'denied_values': None
}]
}
assert p3['rules'][2] == {
'allow_all': 'TRUE',
'condition': [{
'description': 'test condition2',
'expression': 'resource.matchTagId(cc, dd)',
'location': 'xxx',
'title': 'condition2'
}],
'deny_all': None,
'enforce': None,
'values': []
}