diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..46889c9fc0057c14161f2a0b403d437749b23c04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +~* +*~ +*.sw[op] +*.py[cod] +.DS_Store +__pycache__/ +__pypackages__/ +.vagrant/ +vendor/ +Thumbs.db +*.retry +.svn/ +.sass-cache/ +*.log +*.out +*.so +node_modules/ +.npm/ +nbproject/ +*.ipynb +.idea/ +*.egg-info/ +*.[ao] +.classpath +.cache/ +bower_components/ +*.class +*.[ewj]ar +secring.* +.*.kate-swp +.swp.* +.directory +.Trash-* +build/ +_build/ +dist/ +.tox/ +*.pdf +*.exe +*.dll +*.gz +*.tgz +*.tar +*.rar +*.zip +*.xz +*.pid +*.lock +*.env +.bundle/ +!Pipfile.lock +!pdm.lock +!Gemfile.lock +.terraform +.terraform.* +!.terraform.lock.hcl +tfplan +*.tfstate* +*.venv +reports/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..ebf3774c62fd1726967205b3833019eb5c1b825b --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,21 @@ +--- +include: + - project: shore/ci-stuff + file: templates/pre-commit.yml + - project: shore/ci-stuff + file: templates/python.yml + +build-wheel: + extends: .python3 + stage: build + script: + - python3 -m build + after_script: + - &twine_check twine check dist/*.whl + artifacts: + paths: + - dist/*.tar.gz + - dist/*.whl + +install-wheel: + extends: .install-wheel diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c274ad4aa27672616b67490cfde8208ec2eb30d9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,59 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v4.0.1 + hooks: + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + exclude: \.diff$ + + - repo: https://github.com/codespell-project/codespell.git + rev: v2.1.0 + hooks: + - id: codespell + + - repo: https://github.com/amperser/proselint.git + rev: 0.10.2 + hooks: + - id: proselint + types: [plain-text] + exclude: LICENSE + + - repo: https://gitlab.com/devopshq/gitlab-ci-linter.git + rev: v1.0.2 + hooks: + - id: gitlab-ci-linter + args: + - "--server" + - https://git.shore.co.il + + - repo: https://git.shore.co.il/nimrod/yamltool.git + rev: v0.1.2 + hooks: + - id: yamltool + + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.26.3 + hooks: + - id: yamllint + + - repo: https://github.com/Lucas-C/pre-commit-hooks-markup.git + rev: v1.0.1 + hooks: + - id: rst-linter + + - repo: https://github.com/myint/rstcheck.git + rev: v6.0.0.post1 + hooks: + - id: rstcheck + + - repo: https://github.com/ambv/black.git + rev: 22.3.0 + hooks: + - id: black + args: + - | + --line-length=79 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..9410b2874ea3ef47fc6d79c107d3ee7905d75c54 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Adar Nimrod + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..ef44faf83d96125b92bd78b5233b0f2087b50620 --- /dev/null +++ b/README.rst @@ -0,0 +1,66 @@ +tf +## + +.. image:: https://git.shore.co.il/nimrod/tf/badges/main/pipeline.svg + :target: https://git.shore.co.il/nimrod/tf/-/commits/main + :alt: pipeline status + +A simple Terraform wrapper to use variable definition files if they match the +workspace name. + +Rationale +--------- + +With workspaces, one can use the same module in different environments with +minor changes like different domain name, size of a cluster, instance type. +One can use variable definition files to store the values for each workspace in +a dedicated file. This wrapper replaces the following: + +.. code:: shell + + terraform workspace select prod + terraform plan -var-files=prod.tfvars -out tfplan + +to: + +.. code:: shell + + terraform workspace select prod + tf plan -out tfplan + +Installation +------------ + +.. code:: shell + + python3 -m pip install terraformation + +The wrapper is a single Python3 script with no external dependencies. If you +prefer, you can download the :code:`tf.py` and use that instead. + +Usage +----- + +Replace :code:`terraform` with :code:`tf`. In case there's a variable +definitions file (that ends with :code:`.tfvars`) that matches the current +workspace name (if the current workspace name is :code:`prod` and a file named +:code:`prod.tfvars` exists) than a :code:`-var-file=prod.tfvars` argument is +added to the relevant commands (like :code:`plan` and :code:`import`). All +other arguments are kept as they were. Similarly, if a directory exists with +the same name as the workspace, for all the files inside that directory that +end with :code:`.tfvars`, a :code:`-var-file` argument is added. For example: +:code:`-var-file=prod/a.tfvars` and :code:`-var-file=prod/b.tfvars`. + +License +------- + +This software is licensed under the MIT license (see the :code:`LICENSE.txt` +file). + +Author +------ + +Nimrod Adar, `contact me <nimrod@shore.co.il>`_ or visit my `website +<https://www.shore.co.il/>`_. Patches are welcome via `git send-email +<http://git-scm.com/book/en/v2/Git-Commands-Email>`_. The repository is located +at: https://git.shore.co.il/nimrod/. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..a2b0562c602c7a969d555b00f903a457e5b91edf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools >= 61.0.0", + "wheel", +] + +[project] +authors = [ + { name = "Nimrod Adar", email = "nimrod@shore.co.il"}, +] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", +] +dependencies = [ +] +description = "A simple Terraform wrapper to use variable definition files if they match the workspace name." +dynamic = [ + "version", +] +keywords = [ + "terraform", +] +license = {file = "LICENSE.txt"} +name = "terraformation" +readme = "README.rst" + +[project.scripts] +tf = "tf:wrapper" + +[project.urls] +homepage = "https://git.shore.co.il/nimrod/tf" + +[tool.setuptools.dynamic] +version = {attr = "tf.__version__"} diff --git a/tf.py b/tf.py new file mode 100644 index 0000000000000000000000000000000000000000..7ecdbd76135f9a7a324ad69cb61a0792bb9a926e --- /dev/null +++ b/tf.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""A simple Terraform wrapper to use variable definition files if they match +the workspace name.""" + +import glob +import os +import pathlib +import subprocess # nosec +import sys + +__version__ = "0.1.0" + +TF_COMMANDS_TO_MODIFY = ["plan", "console", "import", "refresh"] + + +def get_workspace(): + """Return the Terraform workspace.""" + proc = subprocess.run( # nosec + ["terraform", "workspace", "show"], + capture_output=True, + check=True, + text=True, + ) + return proc.stdout.strip() + + +def is_debug_set(): + """Check if the debug environment variable is set.""" + debug = os.getenv("TF_DEBUG", "").lower() + return debug in ["1", "true"] + + +def wrapper(): + # In case tf was run with no arguments. + if len(sys.argv) == 1: + os.execlp("terraform", "terraform") # nosec + + # Build a new argument list. + args = sys.argv[:] + args[0] = "terraform" + + # Check if the Terraform command is one that we need to modify (include + # tfvars files). If not, execute Terraform with the same arguments. + for command in TF_COMMANDS_TO_MODIFY: + if command in sys.argv: + break + else: + os.execvp("terraform", args) # nosec + + # We need to add the var files after the Terraform command (if we add it + # before Terraform doesn't accept them) but not at the end (not to modify + # other argument that accept optional values). So we add them right after + # the Terraform command. + command_index = args.index(command) + workspace = get_workspace() + var_file = pathlib.Path(f"{workspace}.tfvars") + var_dir = pathlib.Path(workspace) + if var_file.exists() and var_file.is_file(): + args.insert(command_index + 1, f"-var-file={var_file}") + elif var_dir.exists() and var_dir.is_dir(): + for var_file in glob.glob(f"{var_dir}/*.tfvars"): + args.insert(command_index + 1, f"-var-file={var_file}") + + # Print the new argument list to stderr if debugging is enabled. + if is_debug_set(): + print(args, file=sys.stderr) + + os.execvp("terraform", args) # nosec + + +if __name__ == "__main__": + wrapper()