-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a "builder" class for constructing HCL files from Python
- Loading branch information
1 parent
1ad6758
commit 8652de4
Showing
8 changed files
with
166 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
"""A utility class for constructing HCL documents from Python code.""" | ||
|
||
from typing import List | ||
from typing_extensions import Self | ||
|
||
|
||
class Builder: | ||
def __init__(self, attributes: dict = {}): | ||
self.blocks = {} | ||
self.attributes = attributes | ||
|
||
def block( | ||
self, block_type: str, labels: List[str] = [], **attributes: dict | ||
) -> Self: | ||
"""Create a block within this HCL document.""" | ||
block = Builder(attributes) | ||
|
||
# initialize a holder for blocks of that type | ||
if block_type not in self.blocks: | ||
self.blocks[block_type] = [] | ||
|
||
# store the block in the document | ||
self.blocks[block_type].append((labels.copy(), block)) | ||
|
||
return block | ||
|
||
def build(self): | ||
"""Return the Python dictionary for this HCL document.""" | ||
body = { | ||
"__start_line__": -1, | ||
"__end_line__": -1, | ||
**self.attributes, | ||
} | ||
|
||
for block_type, blocks in self.blocks.items(): | ||
|
||
# initialize a holder for blocks of that type | ||
if block_type not in body: | ||
body[block_type] = [] | ||
|
||
for labels, block_builder in blocks: | ||
# build the sub-block | ||
block = block_builder.build() | ||
|
||
# apply any labels | ||
labels.reverse() | ||
for label in labels: | ||
block = {label: block} | ||
|
||
# store it in the body | ||
body[block_type].append(block) | ||
|
||
return body |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,4 @@ block { | |
|
||
block "label" { | ||
b = 2 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
block "block_with_newlines" { | ||
a = "line1\nline2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
locals { | ||
terraform = { | ||
channels = local.running_in_ci ? local.ci_channels : local.local_channels | ||
channels = (local.running_in_ci ? local.ci_channels : local.local_channels) | ||
authentication = [] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
"""Test building an HCL file from scratch""" | ||
|
||
import json | ||
from pathlib import Path | ||
from unittest import TestCase | ||
|
||
import hcl2 | ||
import hcl2.builder | ||
|
||
|
||
HELPERS_DIR = Path(__file__).absolute().parent.parent / "helpers" | ||
HCL2_DIR = HELPERS_DIR / "terraform-config" | ||
JSON_DIR = HELPERS_DIR / "terraform-config-json" | ||
HCL2_FILES = [str(file.relative_to(HCL2_DIR)) for file in HCL2_DIR.iterdir()] | ||
|
||
|
||
class TestBuilder(TestCase): | ||
"""Test building a variety of hcl files""" | ||
|
||
# print any differences fully to the console | ||
maxDiff = None | ||
|
||
def test_build_a_tf(self): | ||
builder = hcl2.Builder() | ||
|
||
builder.block("block", a=1) | ||
builder.block("block", ["label"], b=2) | ||
|
||
self.compare_filenames(builder, "a.tf") | ||
|
||
def test_build_escapes_tf(self): | ||
builder = hcl2.Builder() | ||
|
||
builder.block("block", ["block_with_newlines"], a="line1\nline2") | ||
|
||
self.compare_filenames(builder, "escapes.tf") | ||
|
||
def test_locals_embdedded_condition_tf(self): | ||
builder = hcl2.Builder() | ||
|
||
builder.block( | ||
"locals", | ||
terraform={ | ||
"channels": "${local.running_in_ci ? local.ci_channels : local.local_channels}", | ||
"authentication": [], | ||
}, | ||
) | ||
|
||
self.compare_filenames(builder, "locals_embedded_condition.tf") | ||
|
||
def test_locals_embedded_function_tf(self): | ||
builder = hcl2.Builder() | ||
|
||
builder.block( | ||
"locals", | ||
function_test='${var.basename}-${var.forwarder_function_name}_${md5("${var.vpc_id}${data.aws_region.current.name}")}', | ||
) | ||
|
||
self.compare_filenames(builder, "locals_embedded_function.tf") | ||
|
||
def test_locals_embedded_interpolation_tf(self): | ||
builder = hcl2.Builder() | ||
|
||
builder.block( | ||
"locals", | ||
embedded_interpolation='${module.special_constants.aws_accounts["aaa-${local.foo}-${local.bar}"]}/us-west-2/key_foo', | ||
) | ||
|
||
self.compare_filenames(builder, "locals_embedded_interpolation.tf") | ||
|
||
def test_provider_function_tf(self): | ||
builder = hcl2.Builder() | ||
|
||
builder.block( | ||
"locals", | ||
name2='${provider::test2::test("a")}', | ||
name3='${test("a")}', | ||
) | ||
|
||
self.compare_filenames(builder, "provider_function.tf") | ||
|
||
def compare_filenames(self, builder: hcl2.Builder, filename: str): | ||
hcl_dict = builder.build() | ||
hcl_ast = hcl2.reverse_transform(hcl_dict) | ||
hcl_content_built = hcl2.writes(hcl_ast) | ||
|
||
hcl_path = (HCL2_DIR / filename).absolute() | ||
with hcl_path.open("r") as hcl_file: | ||
hcl_file_content = hcl_file.read() | ||
self.assertMultiLineEqual( | ||
hcl_content_built, | ||
hcl_file_content, | ||
f"file {filename} does not match its programmatically built version.", | ||
) |