Skip to content

Commit f6c549c

Browse files
committed
Add output task with stdout capture.
1 parent 54e597d commit f6c549c

File tree

4 files changed

+205
-6
lines changed

4 files changed

+205
-6
lines changed

src/infrablocks/invoke_terraform/task_factory.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,35 @@ class InitConfiguration:
1616
reconfigure: bool
1717

1818

19+
@dataclass
20+
class OutputConfiguration:
21+
json: bool
22+
23+
1924
@dataclass
2025
class Configuration:
26+
init_configuration: InitConfiguration
27+
output_configuration: OutputConfiguration
28+
2129
source_directory: str
2230
variables: tf.Variables
2331
workspace: str | None
24-
init_configuration: InitConfiguration
25-
environment: Environment | None = None
2632
auto_approve: bool = True
2733

34+
capture_stdout: bool = False
35+
environment: Environment | None = None
36+
2837
@staticmethod
2938
def create_empty():
3039
return Configuration(
31-
source_directory="",
32-
variables={},
33-
workspace=None,
3440
init_configuration=InitConfiguration(
3541
backend_config={}, reconfigure=False
3642
),
43+
output_configuration=OutputConfiguration(json=False),
44+
source_directory="",
45+
variables={},
46+
workspace=None,
47+
capture_stdout=False,
3748
environment={},
3849
)
3950

@@ -62,10 +73,15 @@ def create(
6273
apply_task = invoke_factory.create_task(
6374
self._create_apply(pre_task_function), task_parameters
6475
)
76+
output_task = invoke_factory.create_task(
77+
self._create_output(pre_task_function), task_parameters
78+
)
6579

6680
# TODO: investigate type issue
6781
collection.add_task(plan_task) # pyright: ignore[reportUnknownMemberType]
6882
collection.add_task(apply_task) # pyright: ignore[reportUnknownMemberType]
83+
collection.add_task(output_task) # pyright: ignore[reportUnknownMemberType]
84+
6985
return collection
7086

7187
def _create_plan(
@@ -101,6 +117,35 @@ def apply(context: Context, arguments: invoke_factory.Arguments):
101117

102118
return apply
103119

120+
def _create_output(
121+
self, pre_task_function: PreTaskFunction
122+
) -> invoke_factory.BodyCallable[str | None]:
123+
def output(
124+
context: Context, arguments: invoke_factory.Arguments
125+
) -> str | None:
126+
(terraform, configuration) = self._pre_command_setup(
127+
pre_task_function, context, arguments
128+
)
129+
130+
capture: tf.StreamNames | None = None
131+
if configuration.capture_stdout:
132+
capture = {"stdout"}
133+
134+
result = terraform.output(
135+
chdir=configuration.source_directory,
136+
capture=capture,
137+
json=configuration.output_configuration.json,
138+
environment=configuration.environment,
139+
)
140+
141+
if configuration.capture_stdout and result.stdout is not None:
142+
output = result.stdout.read()
143+
return output.strip()
144+
145+
return None
146+
147+
return output
148+
104149
def _pre_command_setup(
105150
self,
106151
pre_task_function: PreTaskFunction,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from .terraform import BackendConfig, ConfigurationValue, Variables
22
from .terraform import Environment as Environment
33
from .terraform import Executor as Executor
4+
from .terraform import Result as Result
5+
from .terraform import StreamName as StreamName
6+
from .terraform import StreamNames as StreamNames
47
from .terraform import Terraform as Terraform
58

69
__all__ = [
710
"BackendConfig",
811
"ConfigurationValue",
912
"Environment",
1013
"Executor",
14+
"Result",
1115
"Terraform",
1216
"Variables",
1317
]

src/infrablocks/invoke_terraform/terraform/terraform.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020

2121

2222
class Result:
23-
def __init__(self, stdout: IO[str] | None, stderr: IO[str] | None):
23+
def __init__(
24+
self, stdout: IO[str] | None = None, stderr: IO[str] | None = None
25+
):
2426
self.stdout = stdout
2527
self.stderr = stderr
2628

tests/unit/infrablocks/invoke_terraform/test_task_factory.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from io import StringIO
12
from typing import cast
23
from unittest.mock import Mock
34

@@ -261,3 +262,150 @@ def pre_task_function(_context, _, configuration: Configuration):
261262
autoapprove=True,
262263
environment=environment,
263264
)
265+
266+
def test_creates_output_task(self):
267+
pre_task_function_mock = Mock()
268+
269+
collection = TaskFactory().create(
270+
"collection", [], pre_task_function_mock
271+
)
272+
273+
assert collection.tasks["output"] is not None
274+
275+
def test_output_invokes_init_and_output(self):
276+
terraform = Mock(spec=tf.Terraform)
277+
task_factory = TaskFactory(
278+
terraform_factory=TerraformFactory(terraform)
279+
)
280+
source_directory = "/some/path"
281+
backend_config: tf.BackendConfig = {"path": "state_file.tfstate"}
282+
283+
def pre_task_function(_context, _, configuration: Configuration):
284+
configuration.source_directory = source_directory
285+
configuration.init_configuration.backend_config = backend_config
286+
287+
collection = task_factory.create("collection", [], pre_task_function)
288+
output: Task = cast(Task, collection.tasks["output"])
289+
290+
output(Context())
291+
292+
terraform.init.assert_called_once_with(
293+
chdir=source_directory,
294+
backend_config=backend_config,
295+
reconfigure=False,
296+
environment={},
297+
)
298+
terraform.output.assert_called_once_with(
299+
chdir=source_directory,
300+
capture=None,
301+
json=False,
302+
environment={},
303+
)
304+
305+
def test_output_uses_workspace(self):
306+
terraform = Mock(spec=tf.Terraform)
307+
task_factory = TaskFactory(
308+
terraform_factory=TerraformFactory(terraform)
309+
)
310+
workspace = "workspace"
311+
source_directory = "/some/path"
312+
313+
def pre_task_function(_context, _, configuration: Configuration):
314+
configuration.source_directory = source_directory
315+
configuration.workspace = workspace
316+
317+
collection = task_factory.create("collection", [], pre_task_function)
318+
output: Task = cast(Task, collection.tasks["output"])
319+
320+
output(Context())
321+
322+
terraform.select_workspace.assert_called_once_with(
323+
workspace, chdir=source_directory, or_create=True, environment={}
324+
)
325+
326+
def test_output_uses_json(self):
327+
terraform = Mock(spec=tf.Terraform)
328+
task_factory = TaskFactory(
329+
terraform_factory=TerraformFactory(terraform)
330+
)
331+
workspace = "workspace"
332+
source_directory = "/some/path"
333+
334+
def pre_task_function(_context, _, configuration: Configuration):
335+
configuration.source_directory = source_directory
336+
configuration.workspace = workspace
337+
configuration.output_configuration.json = True
338+
339+
collection = task_factory.create("collection", [], pre_task_function)
340+
output: Task = cast(Task, collection.tasks["output"])
341+
342+
output(Context())
343+
344+
terraform.output.assert_called_once_with(
345+
chdir=source_directory,
346+
capture=None,
347+
json=True,
348+
environment={},
349+
)
350+
351+
def test_output_uses_environment_in_all_commands_when_set(self):
352+
terraform = Mock(spec=tf.Terraform)
353+
task_factory = TaskFactory(
354+
terraform_factory=TerraformFactory(terraform)
355+
)
356+
source_directory = "/some/path"
357+
environment = {"ENV_VAR": "value"}
358+
workspace = "workspace"
359+
360+
def pre_task_function(_context, _, configuration: Configuration):
361+
configuration.source_directory = source_directory
362+
configuration.environment = environment
363+
configuration.workspace = workspace
364+
365+
collection = task_factory.create("collection", [], pre_task_function)
366+
output: Task = cast(Task, collection.tasks["output"])
367+
368+
output(Context())
369+
370+
terraform.init.assert_called_once_with(
371+
chdir=source_directory,
372+
backend_config={},
373+
reconfigure=False,
374+
environment=environment,
375+
)
376+
377+
terraform.select_workspace.assert_called_once_with(
378+
"workspace",
379+
chdir=source_directory,
380+
or_create=True,
381+
environment=environment,
382+
)
383+
384+
terraform.output.assert_called_once_with(
385+
chdir=source_directory,
386+
capture=None,
387+
json=False,
388+
environment=environment,
389+
)
390+
391+
def test_output_returns_standard_output_when_capture_stdout_true(self):
392+
terraform = Mock(spec=tf.Terraform)
393+
task_factory = TaskFactory(
394+
terraform_factory=TerraformFactory(terraform)
395+
)
396+
source_directory = "/some/path"
397+
398+
terraform.output.return_value = tf.Result(
399+
stdout=StringIO("output_value\n"), stderr=None
400+
)
401+
402+
def pre_task_function(_context, _, configuration: Configuration):
403+
configuration.source_directory = source_directory
404+
configuration.capture_stdout = True
405+
406+
collection = task_factory.create("collection", [], pre_task_function)
407+
output: Task = cast(Task, collection.tasks["output"])
408+
409+
output_value = output(Context())
410+
411+
assert output_value == "output_value"

0 commit comments

Comments
 (0)