Skip to content

Commit

Permalink
✨ Check versions of IaaC tools (#8)
Browse files Browse the repository at this point in the history
Versions are available after initialization.
Terragrunt CLI redesign is used when version allows it. Giving compatibility with older versions.

Co-authored-by: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com>
  • Loading branch information
github-actions[bot] and ChristophShyper authored Feb 26, 2025
1 parent 86f4a27 commit 62586a6
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 37 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ Framework is written in Python and can be installed as a package.
```
3. Install other dependencies:
* Install Python if not installed yet - required.
* Install Terragrunt, and Terraform or OpenTofu - required for IaaC operations.
* Install `hcledit` - required for updating `.hcl` files.
* Install `direnv` if not installed yet - highly suggested for managing environments.
* Install `direnv` or similar solution - highly suggested for managing environments.

It can be installed, e.g. by running:
```sh
brew install python
brew install terraform
brew install terragrunt
brew install direnv
brew install minamijoyo/hcledit/hcledit
```
Expand Down Expand Up @@ -131,12 +134,10 @@ velez -tg plan aws/dev-account

## Configuration

Velez expects following environment variables to be set if corresponding values are not hardcoded in the root Terragrunt
configuration file:
Velez expects following environment variables to be set:

* `VELEZ_ROOT_HCL` - relative path to the Terragrunt configuration file, e.g. `root.hcl`.
* `VELEZ_TEMP_CONFIG` - absolute path to a temporary file created to render Terragrunt configuration, e.g.
`/tmp/terragrunt.hcl`.
* `VELEZ_ROOT_HCL` - relative path to the Terragrunt configuration file (defaults to `root.hcl`, as per the current recommendations).
* `VELEZ_TEMP_CONFIG` - absolute path to a temporary file created to render Terragrunt configuration (defaults to `/tmp/terragrunt.hcl`).

For the convenience, these variables can be set in a `.env` file in the project directory and use the `direnv` (mentioned above) to load
them automatically for every project separately.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='velez',
version='0.2.0',
version='0.2.1',
packages=find_packages(),
install_requires=[
'pick',
Expand Down
39 changes: 18 additions & 21 deletions velez/terragrunt_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ def list_folders_to_ignore() -> list:
return ['.terragrunt-cache', '.terraform-plugin-cache']


def list_not_wait_for() -> list:
"""
List of commands that can be run without waiting for user input.
:return: list
"""
return ['render-json']


class TerragruntOperations:
"""
Class for Terragrunt operations.
Expand All @@ -39,7 +47,7 @@ def list_folders(self, base_dir: str=None) -> list:
folders = []
for item in sorted(os.listdir(base_dir)):
item_path = os.path.join(base_dir, item)
if os.path.isdir(item_path) and item not in list_folders_to_ignore():
if os.path.isdir(item_path) and item not in list_folders_to_ignore() and not item.startswith('.'):
folders.append(item_path)
return folders

Expand Down Expand Up @@ -521,43 +529,32 @@ def unlock_action(self) -> None:
self.run_terragrunt(['run', 'force-unlock'])
self.action_menu()

def run_terragrunt(self, arguments: list) -> None:
def run_terragrunt(self, arguments: list, quiet: bool = False) -> None:
"""
Run Terragrunt command.
:param arguments: list of arguments to pass to Terragrunt
:param quiet: if True, suppress output and errors
:return: None
"""
# Commands that be run without waiting for user input
not_wait_for = ['render-json']
args = [i for i in arguments if i is not None or i != '']
# Building the full command
command = ['terragrunt'] + args
if 'run' in args:
command += ['--tf-forward-stdout', '--experiment', 'cli-redesign']
# check if cli-redesign is possible
if self.velez.terragrunt_version >= '0.73.0':
if 'run' in args:
command += ['--tf-forward-stdout', '--experiment', 'cli-redesign']
if self.module:
command += ['--working-dir', f'{self.module}']
print(f"Running command: {' '.join(command)}")
try:
result = subprocess.run(
command,
capture_output=True,
text=True
)
print(result.stdout)
if result.stderr:
print(result.stderr)
if not any(i in args for i in not_wait_for):
input("Press Enter when ready to continue...")
except Exception as e:
print(f"An error occurred: {e}")
out, err = run_command(command, quiet=quiet)
if not any(i in args for i in list_not_wait_for()):
input("Press Enter when ready to continue...")

def load_terragrunt_config(self) -> dict:
"""
Load Terragrunt module configuration from running Terragrunt.
:return: dict
"""
self.run_terragrunt(['render-json', '--out', self.velez.temp_config])
run_command(['terragrunt', 'render-json', '--out', self.velez.temp_config], quiet=True)
return load_json_file(self.velez.temp_config)

def update_self(self, module_path: str) -> None:
Expand Down
89 changes: 84 additions & 5 deletions velez/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re
import shutil
import subprocess


Expand Down Expand Up @@ -34,17 +36,94 @@
str_clean_files = "🧹 Clean temporary files"


def run_command(command: list[str]) -> tuple:
def run_command(command: list[str], quiet: bool = False) -> tuple:
"""
Run a command.
:param command: command to run
:param quiet: if True, suppress output and errors
:return: tuple with stdout and stderr
"""
print(f"Running command: {command}")
# check if command is recognizable by the system
if not shutil.which(command[0]):
if not quiet:
print(f"Error: Command not found: {command[0]}")
return '', f"Command not found: {command[0]}"
if not quiet:
print(f"Running command: {' '.join(command)}")

try:
cmd = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, universal_newlines=True)
cmd = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
out, err = cmd.communicate()
if not quiet:
if out:
print(out)
if err:
print(err)
return out, err
except Exception as e:
print(f"Error running command: {e}")
return None, e
if not quiet:
print(f"\n\nError running command: {e}\n\n")
return '', str(e)

def get_terraform_version(quiet: bool = False) -> str:
"""
Check Terraform version.
:param quiet: if True, suppress output and errors
:return: Terraform version as a string
"""
terraform_version = ''
try:
output, err = run_command(['terraform', '-version'], quiet=quiet)
if output:
first_line = output.splitlines()[0]
match = re.search(r'Terraform v(\d+\.\d+\.\d+)', first_line)
if match:
terraform_version = match.group(1)
if not quiet:
print(f'Terraform version: {terraform_version}')
except Exception as e:
if not quiet:
print(f'Error checking Terraform version: {e}')
return terraform_version

def get_opentofu_version(quiet: bool = False) -> str:
"""
Check OpenTofu version.
:param quiet: if True, suppress output and errors
:return: OpenTofu version as a string
"""
opentofu_version = ''
try:
output, err = run_command(['opentofu', '--version'], quiet=quiet)
if output:
first_line = output.splitlines()[0]
match = re.search(r'OpenTofu v(\d+\.\d+\.\d+)', first_line)
if match:
opentofu_version = match.group(1)
if not quiet:
print(f'OpenTofu version: {opentofu_version}')
except Exception as e:
if not quiet:
print(f'Error checking OpenTofu version: {e}')
return opentofu_version

def get_terragrunt_version(quiet: bool = False) -> str:
"""
Check Terragrunt version.
:param quiet: if True, suppress output and errors
:return: Terragrunt version as a string
"""
terragrunt_version = ''
try:
output, err = run_command(['terragrunt', '-v'], quiet=quiet)
if output:
first_line = output.splitlines()[0]
match = re.search(r'terragrunt version (\d+\.\d+\.\d+)', first_line)
if match:
terragrunt_version = match.group(1)
if not quiet:
print(f'Terragrunt version: {terragrunt_version}')
except Exception as e:
if not quiet:
print(f'Error checking Terragrunt version: {e}')
return terragrunt_version
19 changes: 15 additions & 4 deletions velez/velez.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from pick import pick
from velez.file_ops import FileOperations
from velez.terragrunt_ops import TerragruntOperations
from velez.utils import str_terragrunt_menu, str_file_menu, str_exit
from velez.utils import str_terragrunt_menu, str_file_menu, str_exit, get_terragrunt_version, get_terraform_version, \
get_opentofu_version


class Velez:
Expand All @@ -24,6 +25,9 @@ def __init__(self, base_dir=None):
self.s3_bucket_name = '' # S3 backend bucket name, will be updated for each module separately
self.s3_state_key = '' # S3 tfstate key, will be updated for each module separately
self.s3_state_path = '' # Full S3 tfstate path, will be updated for each module separately
self.terragrunt_version = '' # Terragrunt version
self.terraform_version = '' # Terraform version
self.opentofu_version = '' # OpenTofu version
self.terragrunt_ops = TerragruntOperations(self)
self.file_ops = FileOperations(self)

Expand Down Expand Up @@ -51,18 +55,26 @@ def main_menu(self) -> None:

def run(self, terragrunt: bool=False, file: bool=False, **kwargs: dict) -> None:
"""
Run the framework pasisng the arguments.
Run the framework passing the arguments.
:param terragrunt: run Terragrunt operations
:param file: run file operations
:param kwargs: additional arguments
:return: None
"""
self.terragrunt_version = get_terragrunt_version(quiet=True)
self.terraform_version = get_terraform_version(quiet=True)
self.opentofu_version = get_opentofu_version(quiet=True)
if not self.terraform_version and not self.opentofu_version:
print("Please install Terraform or OpenTofu to continue properly.")
if not self.terragrunt_version:
print("Please install Terragrunt to continue properly.")

if terragrunt and kwargs.get('pos_args'):
pos_args = kwargs.get('pos_args')
option = pos_args[0]
module = pos_args[1]
additional_args = pos_args[2:]
self.terragrunt_ops.run_terragrunt(module=module, arguments=[option] + additional_args)
self.terragrunt_ops.run_terragrunt(arguments=[option] + additional_args)
elif terragrunt:
self.terragrunt_ops.folder_menu()
elif file:
Expand All @@ -80,7 +92,6 @@ def main() -> None:
parser.add_argument('-tg', '--terragrunt', action='store_true', help='Run Terragrunt operations')
parser.add_argument('-f', '--file', action='store_true', help='Run file operations')
parser.add_argument('pos_args', nargs=argparse.REMAINDER, help='Arguments to pass further')

args = parser.parse_args()

framework = Velez()
Expand Down

0 comments on commit 62586a6

Please sign in to comment.