tgwrap 0.7.16__tar.gz → 0.7.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tgwrap
3
- Version: 0.7.16
3
+ Version: 0.7.18
4
4
  Summary: A (terragrunt) wrapper around a (terraform) wrapper around ....
5
5
  Home-page: https://gitlab.com/lunadata/tgwrap
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "tgwrap"
3
- version = "0.7.16"
3
+ version = "0.7.18"
4
4
  description = "A (terragrunt) wrapper around a (terraform) wrapper around ...."
5
5
  authors = ["Gerco Grandia <gerco.grandia@4synergy.nl>", "Pascal Alma <pascal.alma@4synergy.nl>"]
6
6
  license = "MIT"
@@ -22,7 +22,7 @@ entry_points = \
22
22
 
23
23
  setup_kwargs = {
24
24
  'name': 'tgwrap',
25
- 'version': '0.7.16',
25
+ 'version': '0.7.18',
26
26
  'description': 'A (terragrunt) wrapper around a (terraform) wrapper around ....',
27
27
  'long_description': '# tg-wrap\n\nThis app simply wraps terragrunt (which is a wrapper around terraform, which is a wrapper around cloud APIs, which is...).\n\nWait, why on earth do we need a wrapper for a wrapper (for a wrapper)?\n\nWell, first of all it is pretty opinionated so what works for us, doesn\'t necessarily work for you.\n\nBut our reasoning for creating this is as follows:\n\n## 1. Less typing\n\nterraform is great, and in combination with terragrunt even greater! But let\'s face it, terragrunt does not excel in conciseness! The options are pretty long, which leads to lots of typing. We don\'t like typing!\n\n## 2. Testing modules locally\n\nHowever, more importantly, we are heavily utilising [TERRAGRUNT_SOURCE](https://terragrunt.gruntwork.io/docs/features/execute-terraform-commands-on-multiple-modules-at-once/#testing-multiple-modules-locally) when developing.\n\nThe thing is that as long as you use `run-all` you can use one setting for that variable (and conveniently set it as an environment variable), while if you run a regular command, you need to specify the full path. Which is obviously different for each project.\n\nWhich leads to (even) more typing, and worse: a higher chance for errors.\n\nLuckily you can use `run-all` and add the appriopriate flags to ensure it behaves like a regular plan|apply|destroy etc. But again, more typing.\n\nNothing a [bunch a aliases](https://gitlab.com/lunadata/terragrunt-utils/-/blob/main/tg-shell.sh) can\'t solve though!\n\n## 3. But the original reason was: Errors when using run-all are challenging\n\nOne of the main boons of terragrunt is the ability to break up large projects in smaller steps while still retaining the inter-dependencies. However, when working on such a large project and something goes wrong somewhere in the middle is pretty challenging.\n\nterragrunt\'s error messages are pretty massive, and this is extrapolated with every individual project in your dependency chain.\n\nAnd if it fails somewhere at the front, it keeps on trying until the last one, blowing up your terminal in the process.\n\nSo we wanted a possibility to run the projects step by step, using the dependency graph of terragrunt and have a bit more control over it.\n\nAnd this was not something a bunch of aliases could solve, hence this wrapper was born. And while we we\'re at it, replacing the aliases with this was then pretty straightforward next step as well.\n\n## 4. Analyzing plan files\n\nWhen using the run-all, analyzing what is about to be changed is not going to be easier. Hence we created the `tgwrap analyze` function that lists all the planned changes and (if a config file is availabe) calculates a drift score and runs a [terrasafe](https://pypi.org/project/terrasafe/) style validation check.\n\nIt needs a config file as follows:\n\n```yaml\n---\n#\n# Critically of resources as interpreted by \'tgwrap analyze\'.\n# It uses it for a \'terrasafe\' like validation if resources can safely be deleted.\n# On top of that it tries to analyze and quantify the drift impact of the changes,\n# so that this can be monitored.\n#\nlow:\n # defaults:\n # terrasafe_level: ignore_deletions\n # drift_impact:\n # default: minor\n # delete: medium\n azuread_application.: {} # if we you want to use the defaults\n azuread_app_role_assignment: # or if you want to override these\n drift_impact:\n delete: minor\n # and so on, and so forth\nmedium:\n # defaults:\n # terrasafe_level: ignore_deletion_if_recreation\n # drift_impact:\n # default: medium\n # delete: major\n azurerm_data_factory_linked_service_key_vault.: {}\n # and so on, and so forth\nhigh:\n # defaults:\n # terrasafe_level: unauthorized_deletion\n # drift_impact:\n # default: major\n # update: medium\n azuread_group.:\n drift_impact:\n create: minor\n update: minor\n azurerm_application_insights.: {}\n # and so on, and so forth\n```\n\n### Speeding up the performance of analyze\n\nThis `analyze` function turned out to be pretty slow, where most of the time went into the `terragrunt show` function that is executed for each individual module. \n\nThis was a bit surprising as the plan file is already available on the file system, but it turns out that terragrunt is taking quite a bit of time for managing the depdencies. Even when you\'re excluding the external dependencies and are located in a particular module.\n\nSo, if you add the following to your root `terragrunt.hcl`:\n\n```hcl\nterraform {\n after_hook "link_to_current_module" {\n commands = ["init", "plan", "apply", "validate", "destroy"]\n execute = ["bash", "-c", "ln -sf $(pwd) ${get_terragrunt_dir()}/.terragrunt-cache/current"]\n }\n}\n```\n\nThe directory where the plan file is stored (including the other resources that terraform needs) becomes predictable and it becomes possible to run a native `terraform show` (instead `terragrunt show`) which dramatically speed up things.\n\nJust set the proper value as an environment variable:\n\n```console\nexport TGWRAP_PLANFILE_DIR=".terragrunt-cache/current"\n```\n\nOr pass it along with the `--planfile-dir|-P` option and it will use that.\n\n## Usage\n\n```console\n# general help\ntgwrap --help\n\ntgwrap run -h\ntgwrap run-all -h\n\n# run a plan\ntgwrap plan # which is the same as tgwrap run plan\n\n# run-all a plan\ntgwrap run-all plan\n\n# run-all with excluding a particular directory\ntgwrap run-all plan -E \'excluded-dir/*\'\n# or a directory somewhere further down in the path\ntgwrap run-all plan -E \'**/excluded-dir/**\'\n\n# or do the same in step-by-step mode\ntgwrap run-all plan -s\n\n# or excluding (aka ignoring) external dependencies\ntgwrap run-all plan -sx\n\n# if you want to add additional arguments it is recommended to use -- as separator (although it *might* work without)\ntgwrap output -- -json\n```\n\n> Note: special precautions are needed when passing on parameters that contain quotes. For instance, if you want to move state like below, escape the double quote in the staate address:\n\n`tgwrap state mv \'azuread_group.this[\\"viewers\\"]\' \'azuread_group.this[\\"readers\\"]\'`\n\n## A word about escaping inputs\n\nYour shell is escaping special characters such as `*` and `"` before passing it to the program (`tgwrap` in this case). So some inputs **need** to be escaped in order to function properly.\n\nFor example:\n\n```console\n# to exclude certain modules from an action (such as analyze):\ntgwrap analyze -E \'integrations/\\*/\\*\'\n\n# to import a resource that has a " in its address:\ntgwrap import -a \'azuread_group.this[\\"my_group\\"]\' -i ${GROUP_ID}\n```\n\n## Deploy manifests\n\nIn order to easily deploy a new version of the terraform (and associated terragrunt) modules, we include a manifest file in the root of the landing zone:\n\n```yaml\n---\ngit_repository: ssh://git@gitlab.com/my-org/my-terraform-modules-repo.git\nbase_path: terragrunt/my-platform # path where the terragrunt modules are stored \nconfig_path: terragrunt/config/platform-dev # path (relative to base path) where the config files are stored\n\ndeploy: # which modules do you want to deploy\n dtap:\n applies_to_stages:\n - dev\n - tst\n - acc\n - prd\n source_stage: dev\n source_dir: platform # optional, if the source modules are not directly in the stage dir, but in <stage>/<source_dir> directory\n base_dir: platform # optional, if you want to deploy the base modules in its own dir, side by side with substacks\n config_dir: ../../../config # this is relative to where you run the deploy\n config_files:\n - my-config.hcl\n - ../my-config.hcl\n exclude_modules: # these modules will always be excluded, can be omitted\n - my-specific-module\n include_modules: {} # omit or use an empty dict for all of them\n # or specify your modules as follows\n # base: {} # just a simple include\n # networking-connected: # or a bit more complicated\n # - source: networking\n # - target: networking-connected\n\nsub_stacks:\n is01:\n source: shared-integration/intsvc01\n target: integration/is01\n exclude_modules: # a list of modules that will always be excluded, can be omitted\n - my-specific-module\n is02:\n source: shared-integration/intsvc01\n target: integration/is02\n\n# global configuration files that are deployed as well\n# note that these files are typically applicable to all landing zones and stages!\n# this might lead to unexpected behaviour\n# note that you can exclude syncing these files with a command line switch\nglobal_config_files:\n root-terragrunt:\n source: ../../terragrunt.hcl # relative to base_path\n target: ../../terragrunt.hcl # can be omitted, then it is same as source path\n terrasafe-config:\n source: ../../terrasafe-config.json\n```\n\n## Generating change logs\n\ntgwrap can generate a change log by running:\n\n```console\ntgwrap change-log [--changelog-file ./CHANGELOG.md]\n```\n\nThe (optional) change log file will be, if passed along. tgwrap then checks the file for the existance of a start and end markers, in the following format:\n\n```python\n start_marker = \'<!-- BEGINNING OF OF TGWRAP CHANGELOG SECTION -->\'\n end_marker = \'<!-- END OF TGWRAP CHANGELOG SECTION -->\'\n```\n\nIf they exist, everything between these lines will be replaced by the new change log.\n\n## Development\n\nIn order to develop, you need to apply it to your terragrunt projects. For that you can use the `--terragrunt-working-dir` option and just run it from the poetry directory. Alternatively you can use the [tgwrap-dev](./tgwrap-dev) script and invoke that from your terragrunt directories. Either put it in your `PATH` or create an alias for convenience.\n',
28
28
  'author': 'Gerco Grandia',
@@ -774,8 +774,12 @@ def clean(verbose, working_dir):
774
774
  @click.option('--working-dir', '-w', default=None,
775
775
  help='Working directory, when omitted the current directory is used',
776
776
  )
777
+ @click.option('--include-nbr-of-releases', '-i',
778
+ type=int, default=20, show_default=True,
779
+ help='Max number of releases to include',
780
+ )
777
781
  @click.version_option(version=__version__)
778
- def change_log(changelog_file, verbose, working_dir):
782
+ def change_log(changelog_file, verbose, working_dir, include_nbr_of_releases):
779
783
  """ Experimental! Generate a change log """
780
784
 
781
785
  check_latest_version(verbose)
@@ -784,6 +788,7 @@ def change_log(changelog_file, verbose, working_dir):
784
788
  tgwrap.change_log(
785
789
  changelog_file=changelog_file,
786
790
  working_dir=working_dir,
791
+ include_nbr_of_releases=include_nbr_of_releases,
787
792
  )
788
793
 
789
794
  # this is needed for the vscode debugger to work
@@ -110,7 +110,7 @@ class TgWrap():
110
110
  }
111
111
 
112
112
  lock_stmt = ''
113
- if no_lock and command in ['init', 'validate', 'validate-inputs', 'plan', 'info', 'output', 'show']:
113
+ if no_lock and command in ['init', 'validate', 'validate-inputs', 'plan', 'info', 'output', 'show', 'state']:
114
114
  lock_stmt = '-lock=false'
115
115
  self.printer.warning('Terraform state will NOT be locked')
116
116
  elif no_lock:
@@ -958,6 +958,9 @@ class TgWrap():
958
958
  include_dirs, exclude_dirs, planfile_dir, terragrunt_args):
959
959
  """ Analyzes the plan files """
960
960
 
961
+ def calculate_score(major: int, medium: int, minor: int) -> float :
962
+ return major * 10 + medium + minor / 10
963
+
961
964
  self.printer.verbose("Attempting to 'analyze'")
962
965
  if terragrunt_args:
963
966
  self.printer.verbose(f"- with additional parameters: {' '.join(terragrunt_args)}")
@@ -1080,14 +1083,25 @@ class TgWrap():
1080
1083
  "major": 0,
1081
1084
  "unknown": 0,
1082
1085
  "total": 0,
1086
+ "score": 0,
1083
1087
  }
1084
1088
 
1085
1089
  for key, value in changes.items():
1086
1090
  for type in ["minor", "medium", "major", "unknown", "total"]:
1087
1091
  total_drifts[type] += value["drifts"][type]
1092
+
1093
+ # the formula below is just a way to achieve a numeric results that is coming from the various drift categories
1094
+ value['drifts']['score'] = calculate_score(
1095
+ major = value['drifts']['major'],
1096
+ medium = value['drifts']['medium'],
1097
+ minor = value['drifts']['minor'],
1098
+ )
1099
+ value['drifts']['score'] = value['drifts']['major'] * 10 + value['drifts']['medium'] + value['drifts']['minor'] / 10
1088
1100
 
1089
1101
  # the formula below is just a way to achieve a numeric results that is coming from the various drift categories
1090
1102
  total_drift_score = total_drifts['major'] * 10 + total_drifts['medium'] + total_drifts['minor'] / 10
1103
+ total_drifts['score'] = total_drift_score
1104
+
1091
1105
  self.printer.header(f"Drift score: {total_drift_score} ({total_drifts['major']}.{total_drifts['medium']}.{total_drifts['minor']})")
1092
1106
  if total_drifts["unknown"] > 0:
1093
1107
  self.printer.warning(f"For {total_drifts['unknown']} resources, drift score is not configured, please update configuration!")
@@ -1096,15 +1110,22 @@ class TgWrap():
1096
1110
  for m in value["unknowns"]:
1097
1111
  self.printer.warning(f' -> {m}')
1098
1112
 
1113
+
1114
+
1099
1115
  if out:
1100
1116
  # in the output we convert the dict of dicts to a list of dicts as it makes processing
1101
1117
  # (e.g. by telegraph) easier.
1102
- changes_for_output = []
1118
+ output = {
1119
+ "changes": [],
1120
+ "summary": {},
1121
+ }
1103
1122
  for key, value in changes.items():
1104
1123
  value['module'] = key
1105
- changes_for_output.append(value)
1124
+ output["changes"].append(value)
1106
1125
 
1107
- print(json.dumps(changes_for_output, indent=4))
1126
+ output["summary"] = total_drifts
1127
+
1128
+ print(json.dumps(output, indent=4))
1108
1129
 
1109
1130
  if not ts_validation_successful:
1110
1131
  self.printer.error("Analysis detected unauthorised deletions, please check your configuration!!!")
@@ -1626,7 +1647,7 @@ Note:
1626
1647
  self.printer.verbose(rc)
1627
1648
  self.printer.normal("Cleaned the temporary files")
1628
1649
 
1629
- def change_log(self, changelog_file, working_dir):
1650
+ def change_log(self, changelog_file, working_dir, include_nbr_of_releases):
1630
1651
  """ Generate a change log """
1631
1652
 
1632
1653
  # Run 'git log' to capture the Git log as a string
@@ -1637,8 +1658,13 @@ Note:
1637
1658
  cwd=working_dir if working_dir else None,
1638
1659
  )
1639
1660
 
1640
- # Define a regular expression pattern to extract release tags (e.g., vYYYY.MM.DD(.n))
1641
- release_pattern = r'\b(v\d{4}\.\d{1,2}\.\d{1,2}(\.\d+)?)\b'
1661
+ # Define a regular expression pattern to extract release tags
1662
+ # The following formats are supported:
1663
+ # vYYYY.MM.DD
1664
+ # vYYYYMMDD
1665
+ # vYYYY.MM.DD.01
1666
+ # vYYYYMMDD.01
1667
+ release_pattern = r'\b(v\d{4}\.?\d{1,2}\.?\d{1,2}(\.\d+)?)\b'
1642
1668
 
1643
1669
  # Split the Git log into individual commit entries
1644
1670
  release_commits = {}
@@ -1654,18 +1680,18 @@ Note:
1654
1680
  release_commits[current_release] = []
1655
1681
  elif current_release:
1656
1682
  release_commits[current_release].append(entry)
1657
-
1658
1683
  # Print the grouped commits to the console (you can write them to a file as needed)
1659
1684
  changelog = ""
1685
+ counter = 0
1660
1686
  for release, commits in release_commits.items():
1661
- changelog = changelog + f"\nRelease: {release}\n - "
1662
- # print(f"Release: {release}")
1663
- # for commit in commits:
1664
- # changelog = changelog + f"\n - {commit}"
1665
- # print(f" - {commit}")
1666
- changelog = changelog + '\n - '.join(commits) + '\n'
1667
- # print()
1668
- # changelog =
1687
+ if len(commits) > 0:
1688
+ counter += 1
1689
+
1690
+ changelog = changelog + f"\nRelease: {release}\n - "
1691
+ changelog = changelog + '\n - '.join(commits) + '\n'
1692
+
1693
+ if include_nbr_of_releases and counter > include_nbr_of_releases:
1694
+ break
1669
1695
 
1670
1696
  # do we need to update an existingchange log file?
1671
1697
  if changelog_file:
File without changes
File without changes
File without changes
File without changes
File without changes