Skip to content

Commit

Permalink
feat: Run Terraform
Browse files Browse the repository at this point in the history
Give Cally the ability to run terraform directly, so that there is no need to mess
around with outputs or stacks in the same directory conflicting with each other.
  • Loading branch information
techman83 committed Dec 1, 2024
1 parent 1c4aece commit efe9614
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/coverage-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ jobs:
with:
python-version: "3.11"
cache: pip
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- name: Export Terraform Path
run: echo "CALLY_TERRAFORM_PATH=$(which terraform)" >> $GITHUB_ENV
- name: Install Provider Pacakges
run: pip install build/**/*.tar.gz
- name: Install Cally test dependencies
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,7 @@ select = [
"S404", # `subprocess` module is possibly insecure - used for 'cdktf get'
"S602" # `subprocess` call with `shell=True` identified - required for 'cdktf get'
]
"src/cally/cli/tools/terraform.py" = [
"S404", # `subprocess` module is possibly insecure - used for 'terraform x'
"S603", # `subprocess` call: check for execution of untrusted input - CLI wrapper is expected to take these inputs
]
29 changes: 29 additions & 0 deletions src/cally/cli/commands/tf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import sys
from pathlib import Path
from typing import Tuple

import click

Expand Down Expand Up @@ -32,5 +34,32 @@ def write_template(config: CallyStackServiceConfig, output: Path):
click.secho(f'Template written to {output}')


@click.command(
name='run',
cls=CallyStackServiceCommand(),
context_settings={'ignore_unknown_options': True},
)
@click.option(
'--terraform-path',
envvar='CALLY_TERRAFORM_PATH',
type=click.Path(file_okay=True, dir_okay=False, executable=True),
default='/usr/bin/terraform',
help='Path to the terraform binary',
)
@click.argument('args', nargs=-1, type=click.UNPROCESSED)
@click.pass_obj
def run_terraform(
config: CallyStackServiceConfig, terraform_path: str, args: Tuple[str, ...]
):
tf_cmd = terraform.Command(terraform_path=terraform_path, arguments=args)
with terraform.Action(service=config.config) as action:
action.synth_stack()
if not tf_cmd.success:
click.secho(message=tf_cmd.stderr, fg='red')
sys.exit(tf_cmd.returncode)
click.secho(tf_cmd.stdout)


tf.add_command(print_template)
tf.add_command(run_terraform)
tf.add_command(write_template)
39 changes: 38 additions & 1 deletion src/cally/cli/tools/terraform.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import os
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
from types import TracebackType
from typing import Type
from typing import Tuple, Type

from cally.cdk import stacks

Expand Down Expand Up @@ -53,3 +54,39 @@ def synth_stack(
def print(self) -> str:
self.synth_stack()
return self.output_file.read_text()


class Command:
arguments: Tuple[str, ...]
terraform_path: str
_result: subprocess.CompletedProcess

def __init__(self, terraform_path: str, arguments: Tuple[str, ...]) -> None:
self.terraform_path = terraform_path
self.arguments = arguments

@property
def result(self) -> subprocess.CompletedProcess:
if getattr(self, '_result', None) is None:
self._result = subprocess.run(
args=[self.terraform_path, *self.arguments],
capture_output=True,
check=False,
)
return self._result

@property
def success(self) -> bool:
return self.result.returncode == 0

@property
def returncode(self) -> int:
return self.result.returncode

@property
def stderr(self) -> int:
return self.result.stderr

@property
def stdout(self) -> int:
return self.result.stdout
32 changes: 30 additions & 2 deletions tests/cli/test_tf.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import json
import os
from pathlib import Path
from unittest import mock
from unittest import mock, skipUnless

from click.testing import CliRunner

from cally.cli.commands.tf import tf

from .. import CallyTestHarness

SKIP_TESTS = Path(os.environ.get('CALLY_TERRAFORM_PATH', '.not-set')).is_file()

class TfTests(CallyTestHarness):

class TfActionTests(CallyTestHarness):

@mock.patch.dict(os.environ, {"CALLY_STACK_TYPE": "CallyStack"})
def test_empty_print(self):
Expand Down Expand Up @@ -46,3 +48,29 @@ def test_empty_write(self):
json.loads(data.read_text(encoding='utf8')).get('terraform'),
testdata,
)


@skipUnless(SKIP_TESTS, "CALLY_TERRAFORM_PATH must be set and valid")
@mock.patch.dict(
os.environ,
{
"CALLY_TERRAFORM_PATH": os.environ.get('CALLY_TERRAFORM_PATH', '.not-set'),
"CALLY_STACK_TYPE": "CallyStack",
},
)
class TfCommandTests(CallyTestHarness):
def test_terraform_version(self):
result = CliRunner().invoke(
tf, ['run', 'version', '--environment', 'test', '--service', 'test']
)
self.assertEqual(result.exit_code, 0)
self.assertTrue(result.output.startswith('Terraform v'))

def test_terraform_error(self):
result = CliRunner().invoke(
tf, ['run', 'invalid-foo', '--environment', 'test', '--service', 'test']
)
self.assertEqual(result.exit_code, 1)
self.assertTrue(
result.output.startswith('Terraform has no command named "invalid-foo".')
)

0 comments on commit efe9614

Please sign in to comment.