Skip to content

Add block_types support in terraform resource schema #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
major_changes:
- add support to read and sanitize `block_types` inside terraform provider schema (https://github.com/ansible-collections/cloud.terraform/pull/35).
21 changes: 21 additions & 0 deletions plugins/module_utils/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,28 @@ def from_json(cls, json: TJsonObject) -> "TerraformAttributeSpec":
)


@dataclass
class TerraformBlockTypeSpec:
attributes: Dict[str, TerraformAttributeSpec]
nesting_mode: Union[str, List[str]]

@classmethod
def from_json(cls, json: TJsonObject) -> "TerraformBlockTypeSpec":
return cls(
attributes={
attr_name: TerraformAttributeSpec.from_json(attr_value)
for attr_name, attr_value in json.get("block", {}).get("attributes", {}).items()
},
nesting_mode=json["nesting_mode"],
)


@dataclass
class TerraformResourceSchema:
version: int
# this de-nests the "block" subelement
attributes: Dict[str, TerraformAttributeSpec]
block_types: Dict[str, TerraformBlockTypeSpec]

@classmethod
def from_json(cls, json: TJsonObject) -> "TerraformResourceSchema":
Expand All @@ -150,6 +167,10 @@ def from_json(cls, json: TJsonObject) -> "TerraformResourceSchema":
attr_name: TerraformAttributeSpec.from_json(attr_value)
for attr_name, attr_value in json.get("block", {}).get("attributes", {}).items()
},
block_types={
block_name: TerraformBlockTypeSpec.from_json(block_value)
for block_name, block_value in json.get("block", {}).get("block_types", {}).items()
},
)


Expand Down
1 change: 1 addition & 0 deletions plugins/module_utils/terraform_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ def workspace_list(self) -> TerraformWorkspaceContext:
continue
elif stripped_item.startswith("* "):
current_workspace = stripped_item.replace("* ", "")
all_workspaces.append(current_workspace)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unrelated to the PR, but I noticed that the current_workspace doesn't get added to all_workspaces.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. It would be nice to at least have a unit test for this.

else:
all_workspaces.append(stripped_item)
return TerraformWorkspaceContext(current=current_workspace, all=all_workspaces)
68 changes: 46 additions & 22 deletions plugins/modules/terraform.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,7 @@
from ansible.module_utils.six import integer_types
from ansible.module_utils.basic import AnsibleModule

from ansible_collections.cloud.terraform.plugins.module_utils.types import (
AnyJsonType,
TJsonBareValue,
)
from ansible_collections.cloud.terraform.plugins.module_utils.types import AnyJsonType, TJsonBareValue
from ansible_collections.cloud.terraform.plugins.module_utils.models import (
TerraformWorkspaceContext,
TerraformShow,
Expand Down Expand Up @@ -324,6 +321,21 @@ def is_attribute_sensitive_in_providers_schema(
if resource_schema_name == resource.type:
sensitive = resource_schema.attributes[attribute].sensitive
return sensitive

return False


def is_blocktype_sensitive_in_providers_schema(
schemas: TerraformProviderSchemaCollection, resource: TerraformRootModuleResource, blocktype: str, subattribute: str
) -> bool:
for provider_schema in schemas.provider_schemas:
resource_schemas = schemas.provider_schemas[provider_schema].resource_schemas
for resource_schema_name, resource_schema in resource_schemas.items():
if resource_schema_name == resource.type:
Comment on lines +331 to +334
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking at this and realizing there's a bug in the existing code that is replicated here. The logic in this function and in is_attribute_sensitive_in_providers_schema() don't consider which provider a resource comes from. For example, provider_a.attribute_foo would also match provider_b.attribute_foo if both providers are used in the same config. Whichever provider schema showed up first would win. This should generally be pretty rare, since most providers namespace their resource names already, but it's possible.

I'm not sure there's a need to loop through provider schemas anyways, as the resource already has the provider schema name. The same goes for resource schemas. You should be able to just do:

resource_schemas = schemas.provider_schemas[resource.provider_name].resource_schemas
resource_schema = resource_schemas[resource.type]

for block_type in resource_schema.block_types:
if blocktype == block_type:
sensitive = resource_schema.block_types[blocktype].attributes[subattribute].sensitive
return sensitive
return False


Expand All @@ -332,18 +344,37 @@ def is_attribute_in_sensitive_values(resource: TerraformRootModuleResource, attr


def filter_resource_attributes(
state_contents: TerraformShow, provider_schemas: TerraformProviderSchemaCollection
state_contents: TerraformShow, provider_schemas_collection: TerraformProviderSchemaCollection
) -> TerraformShow:
# using .get() in case there is no existing .tfstate before apply

# Aggregate all block types
block_types = set()
for provider_schema in provider_schemas_collection.provider_schemas:
resource_schemas = provider_schemas_collection.provider_schemas[provider_schema].resource_schemas
for resource_schema_name, resource_schema in resource_schemas.items():
for block_type in resource_schema.block_types:
block_types.add(block_type)

for resource in state_contents.values.root_module.resources:
attributes_to_remove = []
for attribute in resource.values:
if is_attribute_sensitive_in_providers_schema(
provider_schemas, resource, attribute
) or is_attribute_in_sensitive_values(resource, attribute):
attributes_to_remove.append(attribute)
for attribute in attributes_to_remove:
resource.values[attribute] = None
for attr_name, attr_values in resource.values.items():
# Distringuish between attributes and block_types
if attr_name in block_types:
# If attribute is not sensitive, check for its sensitive subattributes
if is_attribute_in_sensitive_values(resource, attr_name):
resource.values[attr_name] = None
else:
for attr_values in resource.values[attr_name]:
for subattr_name in attr_values:
if is_blocktype_sensitive_in_providers_schema(
provider_schemas_collection, resource, attr_name, subattr_name
):
resource.values[attr_name][subattr_name] = None
else:
if is_attribute_sensitive_in_providers_schema(
provider_schemas_collection, resource, attr_name
) or is_attribute_in_sensitive_values(resource, attr_name):
resource.values[attr_name] = None
return state_contents


Expand All @@ -360,10 +391,7 @@ def filter_outputs(state_contents: TerraformShow) -> TerraformShow:
return state_contents


def sanitize_state(
show_state: TerraformShow,
provider_schemas: TerraformProviderSchemaCollection,
) -> TerraformShow:
def sanitize_state(show_state: TerraformShow, provider_schemas: TerraformProviderSchemaCollection) -> TerraformShow:
show_state = filter_resource_attributes(show_state, provider_schemas)
show_state = filter_outputs(show_state)
return show_state
Expand Down Expand Up @@ -487,11 +515,7 @@ def main() -> None:
if force_init:
if overwrite_init or not os.path.isfile(os.path.join(project_path, ".terraform", "terraform.tfstate")):
terraform.init(
backend_config or {},
backend_config_files or [],
init_reconfigure,
provider_upgrade,
plugin_paths or [],
backend_config or {}, backend_config_files or [], init_reconfigure, provider_upgrade, plugin_paths or []
)

try:
Expand Down
6 changes: 4 additions & 2 deletions tests/unit/plugins/modules/test_terraform.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ def provider_schemas():
"description": "Generates a local file with the given content.",
"description_kind": "plain",
},
block_types={},
),
"local_sensitive_file": TerraformResourceSchema(
version=0,
Expand Down Expand Up @@ -240,9 +241,10 @@ def provider_schemas():
"description": "Generates a local file with the given sensitive content.",
"description_kind": "plain",
},
block_types={},
),
},
),
}
)
},
)

Expand Down