├── .bazelignore ├── .envrc ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── BUILD.bazel ├── LICENSE ├── README.md ├── WORKSPACE ├── defs.bzl ├── deps.bzl ├── flake.lock ├── flake.nix ├── internal ├── BUILD.bazel ├── backend.bzl ├── download.bzl ├── modules.bzl ├── starlark │ ├── BUILD.bazel │ ├── main.go │ └── util.bzl ├── tests.bzl └── variables.bzl ├── shell.nix └── test ├── .gitignore ├── 0_12_31 ├── BUILD.bazel └── main.tf ├── 1_1_2 ├── BUILD.bazel └── main.tf ├── BUILD.bazel ├── WORKSPACE ├── common ├── BUILD.bazel └── config.star ├── hello_ec2 ├── BUILD.bazel ├── backend.star └── main.tf ├── tf_state_bootstrap ├── BUILD.bazel ├── backend.star ├── locals.star └── main.tf ├── time_module ├── BUILD.bazel └── main.tf └── vpc ├── BUILD.bazel ├── backend.star └── main.tf /.bazelignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | eval "$(lorri direnv)" 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Install bazel via bazelisk 17 | run: | 18 | curl -LO "https://github.com/bazelbuild/bazelisk/releases/download/v1.11.0/bazelisk-linux-amd64" 19 | mkdir -p "${GITHUB_WORKSPACE}/bin/" 20 | mv bazelisk-linux-amd64 "${GITHUB_WORKSPACE}/bin/bazel" 21 | chmod +x "${GITHUB_WORKSPACE}/bin/bazel" 22 | 23 | # Disabled until there is actually something to test here 24 | # - name: Test root 25 | # run: | 26 | # "${GITHUB_WORKSPACE}/bin/bazel" test //... 27 | 28 | - name: Test test Terraform 29 | run: | 30 | cd test && "${GITHUB_WORKSPACE}/bin/bazel" test --test_output=errors //... 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bazel-* 2 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_gazelle//:def.bzl", "gazelle") 2 | 3 | # Run with bazel run //:gazelle 4 | 5 | # gazelle:prefix github.com/jdreaver/rules_terraform 6 | gazelle(name = "gazelle") 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Reaver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rules_terraform 2 | 3 | This is a WIP set of [Bazel](https://bazel.build/) rules for Terraform. 4 | 5 | ## Usage 6 | 7 | (TODO: Flesh this out once API is more stable) 8 | 9 | - Add incantations to your `WORKSPACE` file to declare which Terraform versions 10 | and Terraform provider versions you are using. 11 | - Put a `BUILD`/`BUILD.bazel` file in each Terraform module directory. 12 | - Add a `terraform_module` rule for each module, `terraform_root_module` for 13 | each root module, and `terraform_*_test` for each kind of test you want to run 14 | on your modules. 15 | 16 | ## Features 17 | 18 | - Automatically download Terraform and provider binaries and cache them via 19 | bazel. 20 | - Cache test results like `terraform fmt -check` and `terraform validate` and 21 | only re-run them as needed. 22 | - Hermetically build Terraform dependencies, like external modules and providers. 23 | - Be explicit about Terraform module dependencies and use `bazel query` to build 24 | a DAG of module dependencies. 25 | - Use [Starlark](https://github.com/bazelbuild/starlark) to generate `backend` 26 | blocks, `terraform_remote_state`, and local variables. 27 | 28 | ## TODO 29 | 30 | - Implement arbitrary `.tf.json` generation via Starlark and consider how to DRY 31 | that with existing codegen rules. 32 | - Maybe generating `locals`, `backend`, and `terraform_remote_state` can be 33 | done in Starlark instead of in separate rules. Then we can do all of a 34 | module's codegen in a single Starlark file and reduce the API surface area. 35 | - Maybe we even do this in the `terraform_module` rule? (Composing 36 | finer-grained rules via macros seems better though, maybe). We could add 37 | starlark files to `srcs` or add a `starlark_srcs` and just transform those 38 | starlark files into `.tf.json`. 39 | - If we had a single `terraform_starlark_json` rule then we just need to 40 | return a dict that will turn into the `.tf.json`, and embedding locals, 41 | backend, etc is as simple as just adding those to the dict. 42 | - To implement `terraform_remote_state`, we could just parse a generated 43 | `.tf.json` and pluck out the backend block. 44 | - Set up 45 | [buildifier](https://github.com/bazelbuild/buildtools/blob/master/buildifier/README.md) 46 | in this repo and add a CI test for ensuring everything is formatted correctly 47 | and there are no lint warnings. 48 | - Figure out how to make DAG of terraform roots 49 | - For example, make it clear that the state S3 bucket and DynamoDB table are 50 | created in `tf_bootstrap_state` so that shows up as a dependency of the 51 | other modules. Do we have to reify the backend config somehow in 52 | `tf_bootstrap_state`'s BUILD file, and then read that as a target in other 53 | files? 54 | - Maybe "making a DAG of roots" is the wrong way to think about it. The real 55 | problem we want to solve is given some set of files that changed, what do we 56 | need to deploy. That might involve an `rdeps` query filtered on 57 | `terraform_root_module` rules. It also means we might want to reify configs 58 | in `BUILD` files so they get a label. 59 | - This feels like a fool's errand without a more specific goal. Maybe we have 60 | to settle for an 80/20 solution of explicitly annotating dependencies, kind 61 | of like in Terragrunt's `dependencies` blocks? 62 | - Investigate auto generating BUILD files for existing roots. Gazelle perhaps? 63 | Read `.terraform` structure? 64 | - Try implementing toolchain again so we can pick a default Terraform version 65 | - In the real world we probably want to be explicit, but for the `terraform 66 | fmt` test we can use whatever. 67 | - Simulate sharing values/config between Terraform and a separate dummy CLI tool 68 | (YAML files?) like at work to iron it out 69 | - https://github.com/bazelbuild/bazel/issues/13300 70 | - Rule of thumb: values that need to be shared but are known ahead of time, 71 | like S3 bucket names, VPC CIDRs, DNS names, etc, are great candidates for 72 | things that could be shared via `.bzl` files. However, it is not clear what 73 | we should do with "generated identifiers" (VPC IDs, load balancer DNS 74 | endpoints, etc). This could be queried from Terraform state, queried at 75 | runtime, etc. Also, even if we query them at runtime, we then might need to 76 | "join" them with other values, like VPC CIDRs. Not sure. 77 | - Document everything, refactor everything, etc. Make this presentable. 78 | - Add a top-level test asserting all S3 backend keys are unique. Duplicating 79 | keys because of a copy/paste error is really common. 80 | - Consider using 81 | [genquery](https://docs.bazel.build/versions/main/be/general.html#genquery) 82 | for common queries, like number of terraform roots on each version 83 | - Make sure to re-enable `bazel test /...` in root workspace in CI once there is 84 | something to test 85 | 86 | ## Why wrap Terraform in Bazel? 87 | 88 | At work we use [Bazel](https://bazel.build/) to build all most of our code into 89 | artifacts like tarballs of compiled binaries and container images. Bazel has 90 | been great because it gives us consistency, speed, and reproducibility in our 91 | builds, even at such a huge scale. 92 | 93 | Unfortunately, our infrastructure as code tooling at work (which includes 94 | Terraform) is _not_ currently as nice as our Bazel builds for other languages. 95 | We have _thousands_ of Terraform roots written by thousands of engineers, with 96 | all kinds external references to other Terraform roots and modules, YAML files 97 | with common variable values (extracted with an external script at runtime), 98 | references to/from external tooling that we use just for ASGs and security 99 | groups, etc. Our Terraform tests are slow because we have to run `terraform 100 | init` in thousands of Terraform root modules just so we can run `terraform 101 | validate`. 102 | 103 | We already have lots of experience with Bazel, and we know our current usage of 104 | Terraform won't scale, so this repo is an experiment in wrapping Terraform in 105 | Bazel to see if we can solve lots of our infrastructure as code problems. 106 | 107 | ### Cache downloads, builds, and tests 108 | 109 | Bazel aggressively caches all nodes in the build graph. That means downloaded 110 | Terraform binaries, downloaded Terraform providers, builds of `.terraform`, and 111 | test executions are all cached. This means that incremental runs of `terraform 112 | init` and any tests are as fast as possible in CI; you won't rebuild 113 | `.terraform` or rerun a test unless some upstream dependency actually changed. 114 | 115 | #### (TODO) Cache providers centrally for all roots 116 | 117 | (The TODO here is ensuring providers aren't copied between roots. This might 118 | only be possible for versions >= 0.13.2 and with `filesystem_mirror`. This is 119 | fantastic because we might be able to store thousands of roots in a single 120 | tarball with minimal space; it is almost entirely a bunch of symlinks.) 121 | 122 | ### Build a DAG of Terraform root modules so we can reason about downstream/upstream changes 123 | 124 | Bazel requires you to be extremely explicit about dependencies. Bazel actions 125 | are executed in sandboxes that are as isolated as possible from the host system, 126 | and only declared dependencies are brought into the sandbox. There are all kinds 127 | of caveats with this, but the relevant bit for Terraform is if you leave out a 128 | dependency on a module, provider, or some other Bazel file, then Bazel will 129 | complain very loudly. 130 | 131 | This specificity allows us to reason about the dependencies between Terraform 132 | and even external code that interfaces with Terraform. We can use Bazel's 133 | [extensive query language](https://docs.bazel.build/versions/main/query.html) to 134 | inspect the dependency graph. For example, to view which terraform roots depend 135 | on a given module, we can do: 136 | 137 | ``` 138 | $ bazel query "kind(terraform_root_module, rdeps(//terraform/..., //terraform/time_module:module))" --output package 139 | terraform/0_12_31 140 | terraform/1_1_2 141 | ``` 142 | 143 | Also, how many Terraform modules are using a given Terraform version? 144 | 145 | ``` 146 | $ bazel query "attr(terraform, @terraform_1_1_2//:terraform, //...)" --output package 147 | terraform/1_1_2 148 | terraform/time_module 149 | ``` 150 | 151 | How about just root modules? 152 | 153 | ``` 154 | $ bazel query "kind(terraform_root_module, attr(terraform, @terraform_1_1_2//:terraform, //...))" --output package 155 | terraform/1_1_2 156 | ``` 157 | 158 | How about a count of root modules on each version? 159 | 160 | ``` 161 | $ bazel query "labels(terraform, kind(terraform_root_module, //...))" | sort | uniq -c 162 | 1 @terraform_0_12_31//:terraform 163 | 1 @terraform_1_1_2//:terraform 164 | ``` 165 | 166 | ### Share variables between Terraform and external tooling 167 | 168 | (TODO: This is trivial if we put shared variables in `.bzl` files, which 169 | honestly is not a bad idea. We might want to figure out a migration path from 170 | YAML files first though.) 171 | 172 | ### (TODO) Generate boilerplate Terraform code for dependencies and references from Bazel 173 | 174 | (TODO: We might want to trivially import Starlark or YAML values like ints, 175 | strings, structs, etc into Terraform data structures for ease of use. This is 176 | likely possible with some simple build rule to spit out a Starlark value into a 177 | `.tf` file that populates a `local` variable.) 178 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "rules_terraform") 2 | 3 | load(":deps.bzl", "rules_terraform_repositories") 4 | rules_terraform_repositories() 5 | 6 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 7 | http_archive( 8 | name = "io_bazel_rules_go", 9 | sha256 = "2b1641428dff9018f9e85c0384f03ec6c10660d935b750e3fa1492a281a53b0f", 10 | urls = [ 11 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", 12 | "https://github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", 13 | ], 14 | ) 15 | 16 | http_archive( 17 | name = "bazel_gazelle", 18 | sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb", 19 | urls = [ 20 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", 21 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", 22 | ], 23 | ) 24 | 25 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") 26 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") 27 | 28 | go_rules_dependencies() 29 | 30 | # On NixOS, need to use host toolchain 31 | # https://github.com/bazelbuild/rules_go/issues/1376 32 | go_register_toolchains(version = "host") 33 | # go_register_toolchains(version = "1.17.1") 34 | 35 | gazelle_dependencies() 36 | -------------------------------------------------------------------------------- /defs.bzl: -------------------------------------------------------------------------------- 1 | load( 2 | "//internal:download.bzl", 3 | _download_terraform_versions = "download_terraform_versions", 4 | _download_terraform_provider_versions = "download_terraform_provider_versions", 5 | _terraform_binary = "terraform_binary", 6 | _terraform_provider = "terraform_provider", 7 | ) 8 | load( 9 | "//internal:modules.bzl", 10 | _terraform_module = "terraform_module", 11 | _terraform_root_module = "terraform_root_module", 12 | ) 13 | load( 14 | "//internal:backend.bzl", 15 | _terraform_backend = "terraform_backend", 16 | _terraform_remote_state = "terraform_remote_state", 17 | ) 18 | load( 19 | "//internal:tests.bzl", 20 | _terraform_validate_test = "terraform_validate_test", 21 | _terraform_format_test = "terraform_format_test", 22 | ) 23 | load( 24 | "//internal:variables.bzl", 25 | _terraform_locals = "terraform_locals", 26 | ) 27 | 28 | download_terraform_versions = _download_terraform_versions 29 | download_terraform_provider_versions = _download_terraform_provider_versions 30 | terraform_binary = _terraform_binary 31 | terraform_provider = _terraform_provider 32 | 33 | terraform_module = _terraform_module 34 | terraform_root_module = _terraform_root_module 35 | 36 | terraform_backend = _terraform_backend 37 | terraform_remote_state = _terraform_remote_state 38 | 39 | terraform_validate_test = _terraform_validate_test 40 | terraform_format_test = _terraform_format_test 41 | 42 | terraform_locals = _terraform_locals 43 | -------------------------------------------------------------------------------- /deps.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | 3 | def rules_terraform_repositories(): 4 | pass 5 | 6 | def _maybe(repo_rule, name, **kwargs): 7 | if name not in native.existing_rules(): 8 | repo_rule(name = name, **kwargs) 9 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs-unstable": { 4 | "locked": { 5 | "lastModified": 1642635915, 6 | "narHash": "sha256-vabPA32j81xBO5m3+qXndWp5aqepe+vu96Wkd9UnngM=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "6d8215281b2f87a5af9ed7425a26ac575da0438f", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs-unstable": "nixpkgs-unstable" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; 4 | }; 5 | 6 | outputs = { self, nixpkgs-unstable }: 7 | let 8 | pkgs = import nixpkgs-unstable { system = "x86_64-linux"; config = { allowUnfree = true; }; }; 9 | in { 10 | devShell.x86_64-linux = pkgs.mkShell { 11 | buildInputs = with pkgs; [ 12 | bazel_4 13 | jdk11 # Needed to run some bazel commands 14 | graphviz # To visualize bazel graph output 15 | 16 | # Downloaded go binaries from rules_go don't work on NixOS 17 | # (https://github.com/bazelbuild/rules_go/issues/1376) 18 | go 19 | ]; 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /internal/BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdreaver/rules_terraform/e460befbb3d3204e132085020e6b565a224b838e/internal/BUILD.bazel -------------------------------------------------------------------------------- /internal/backend.bzl: -------------------------------------------------------------------------------- 1 | load("//internal/starlark:util.bzl", "run_starlark_executor") 2 | 3 | TerraformBackendInfo = provider( 4 | "Provider for terraform_backend rule.", 5 | fields = { 6 | "src": "Source starlark file", 7 | "deps": "Dependencies of starlark file", 8 | }) 9 | 10 | def _terraform_backend_impl(ctx): 11 | output = ctx.actions.declare_file(ctx.label.name + "_backend.tf.json") 12 | 13 | run_starlark_executor( 14 | ctx, 15 | output, 16 | ctx.file.src, 17 | ctx.files.deps, 18 | ctx.executable._starlark_executor, 19 | """ 20 | # Create a .tf.json backend block 21 | def wrap_backend(backend_type, config): 22 | assert_type(backend_type, "string") 23 | assert_type(config, "dict") 24 | 25 | return { 26 | "terraform": { 27 | "backend": { 28 | backend_type: config 29 | } 30 | } 31 | } 32 | """, 33 | "encode_indent(wrap_backend(**main()))", 34 | ) 35 | 36 | 37 | return [ 38 | DefaultInfo(files = depset([output])), 39 | TerraformBackendInfo( 40 | src = ctx.file.src, 41 | deps = ctx.files.deps, 42 | ) 43 | ] 44 | 45 | terraform_backend = rule( 46 | implementation = _terraform_backend_impl, 47 | doc = """Creates a .tf.json file defining terraform_backend 48 | 49 | You can't use variables in Terraform backend blocks 50 | (https://github.com/hashicorp/terraform/issues/13022), so it is important 51 | for us to generate them if we want to use external variables. Also, this 52 | rule can be used as an input to the `terraform_remote_state` rule to 53 | generate a `terraform_remote_state` block for another root. 54 | """, 55 | attrs = { 56 | "src": attr.label( 57 | doc = "Source Starlark file to execute", 58 | mandatory = True, 59 | allow_single_file = True, 60 | ), 61 | "deps": attr.label_list( 62 | doc = "Files needed to execute Starlark", 63 | ), 64 | "_starlark_executor": attr.label( 65 | default = Label("//internal/starlark"), 66 | allow_single_file = True, 67 | executable = True, 68 | cfg = "exec", 69 | ), 70 | }, 71 | ) 72 | 73 | def _terraform_remote_state_impl(ctx): 74 | backend = ctx.attr.backend[TerraformBackendInfo] 75 | output = ctx.actions.declare_file(ctx.label.name + "_remote_state.tf.json") 76 | 77 | run_starlark_executor( 78 | ctx, 79 | output, 80 | backend.src, 81 | backend.deps, 82 | ctx.executable._starlark_executor, 83 | """ 84 | # Create a .tf.json terraform_remote_state block 85 | def wrap_backend_remote_state(backend_type, config, variable_name): 86 | assert_type(backend_type, "string") 87 | assert_type(config, "dict") 88 | assert_type(variable_name, "string") 89 | 90 | return { 91 | "data": { 92 | "terraform_remote_state": { 93 | variable_name: { 94 | "backend": backend_type, 95 | "config": config, 96 | }, 97 | } 98 | } 99 | } 100 | """, 101 | "encode_indent(wrap_backend_remote_state(variable_name = '{}', **main()))".format(ctx.attr.variable_name), 102 | ) 103 | 104 | 105 | return [DefaultInfo(files = depset([output]))] 106 | 107 | terraform_remote_state = rule( 108 | implementation = _terraform_remote_state_impl, 109 | doc = "Creates a .tf.json file defining terraform_remote_state", 110 | attrs = { 111 | "backend": attr.label( 112 | providers = [TerraformBackendInfo], 113 | doc = "Label for terraform_backend to reference.", 114 | ), 115 | "variable_name": attr.string( 116 | mandatory = True, 117 | doc = "Terraform variable name to use for this remote state block", 118 | ), 119 | "_starlark_executor": attr.label( 120 | default = Label("//internal/starlark"), 121 | allow_single_file = True, 122 | executable = True, 123 | cfg = "exec", 124 | ), 125 | }, 126 | ) 127 | -------------------------------------------------------------------------------- /internal/download.bzl: -------------------------------------------------------------------------------- 1 | hashicorp_base_url = "https://releases.hashicorp.com" 2 | 3 | def _terraform_download_impl(ctx): 4 | platform = _detect_platform(ctx) 5 | version = ctx.attr.version 6 | 7 | # First get SHA256SUMS file so we can get all of the individual zip SHAs 8 | ctx.report_progress("Downloading and extracting SHA256SUMS file") 9 | sha256sums_url = "{base}/terraform/{version}/terraform_{version}_SHA256SUMS".format( 10 | base = hashicorp_base_url, 11 | version = version, 12 | ) 13 | ctx.download( 14 | url = sha256sums_url, 15 | sha256 = ctx.attr.sha256, 16 | output = "terraform_sha256sums", 17 | ) 18 | sha_content = ctx.read("terraform_sha256sums") 19 | sha_by_zip = _parse_sha_file(sha_content) 20 | 21 | # Terraform does not provide darwin_arm64 binaries before version 22 | # 1.0.2 or so. Also, many provider versions do not provide 23 | # darwin_arm64. Therefore, if the current platform is darwin_arm64 24 | # and we can't find a SHA for that platform, we fall back to 25 | # darwin_amd64 and depend on Rosetta. 26 | zip = "terraform_{version}_{platform}.zip".format( 27 | version = version, 28 | platform = platform, 29 | ) 30 | if platform == "darwin_arm64" and zip not in sha_by_zip: 31 | platform = "darwin_amd64" 32 | zip = "terraform_{version}_{platform}.zip".format( 33 | version = version, 34 | platform = platform, 35 | ) 36 | sha256 = sha_by_zip[zip] 37 | 38 | url = "{base}/terraform/{version}/{zip}".format( 39 | base = hashicorp_base_url, 40 | version = version, 41 | zip = zip, 42 | ) 43 | 44 | # Now download actual Terraform zip 45 | ctx.report_progress("Downloading and extracting Terraform") 46 | ctx.download_and_extract( 47 | url = url, 48 | sha256 = sha256, 49 | output = "terraform", 50 | type = "zip", 51 | ) 52 | 53 | # Put a BUILD file here so we can use the resulting binary in other bazel 54 | # rules. 55 | ctx.file("BUILD.bazel", 56 | """load("@rules_terraform//:defs.bzl", "terraform_binary") 57 | 58 | terraform_binary( 59 | name = "terraform", 60 | binary = "terraform/terraform", 61 | version = "{version}", 62 | visibility = ["//visibility:public"], 63 | ) 64 | """.format( 65 | version = version, 66 | ), 67 | executable=False 68 | ) 69 | 70 | def _detect_platform(ctx): 71 | if ctx.os.name == "linux": 72 | os = "linux" 73 | elif ctx.os.name == "mac os x": 74 | os = "darwin" 75 | else: 76 | fail("Unsupported operating system: " + ctx.os.name) 77 | 78 | uname_res = ctx.execute(["uname", "-m"]) 79 | if uname_res.return_code == 0: 80 | uname = uname_res.stdout.strip() 81 | if uname == "x86_64": 82 | arch = "amd64" 83 | elif uname == "arm64": 84 | arch = "arm64" 85 | else: 86 | fail("Unable to determing processor architecture.") 87 | else: 88 | fail("Unable to determing processor architecture.") 89 | 90 | return "{}_{}".format(os, arch) 91 | 92 | def _parse_sha_file(file_content): 93 | """Parses terraform SHA256SUMS file and returns map from zip to SHA. 94 | 95 | Args: 96 | file_content: Content of a SHA256SUMS file (see example below) 97 | 98 | Returns: 99 | A dict from a TF zip (e.g. terraform_1.1.2_darwin_amd64.zip) to zip SHA 100 | 101 | Here is an example couple lines from a SHA256SUMS file: 102 | 103 | 214da2e97f95389ba7557b8fcb11fe05a23d877e0fd67cd97fcbc160560078f1 terraform_1.1.2_darwin_amd64.zip 104 | 734efa82e2d0d3df8f239ce17f7370dabd38e535d21e64d35c73e45f35dfa95c terraform_1.1.2_linux_amd64.zip 105 | """ 106 | 107 | sha_by_zip = {} 108 | for line in file_content.splitlines(): 109 | sha, _, zip = line.partition(" ") 110 | sha_by_zip[zip] = sha 111 | 112 | return sha_by_zip 113 | 114 | terraform_download = repository_rule( 115 | implementation = _terraform_download_impl, 116 | attrs = { 117 | "sha256": attr.string( 118 | mandatory = True, 119 | doc = "Expected SHA-256 sum of the downloaded archive", 120 | ), 121 | "version": attr.string( 122 | mandatory = True, 123 | doc = "Version of Terraform", 124 | ), 125 | }, 126 | doc = "Downloads a Terraform binary", 127 | ) 128 | 129 | TerraformBinaryInfo = provider( 130 | "Provider for the terraform_binary rule", 131 | fields={ 132 | "binary": "Path to Terraform binary", 133 | "version": "Version of Terraform for binary", 134 | }) 135 | 136 | def _terraform_binary_impl(ctx): 137 | """Wraps a downloaded Terraform binary as an executable. 138 | 139 | Copies the terraform binary so we can declare it as an output and mark it as 140 | executable. We can't just mark the existing binary as executable, 141 | unfortunately. 142 | """ 143 | output = ctx.actions.declare_file("terraform_{}".format(ctx.attr.version)) 144 | ctx.actions.run_shell( 145 | command = "cp '{}' '{}'".format(ctx.file.binary.path, output.path), 146 | tools = [ctx.file.binary], 147 | outputs = [output], 148 | ) 149 | 150 | return [ 151 | DefaultInfo( 152 | files = depset([output]), 153 | executable = output, 154 | ), 155 | TerraformBinaryInfo( 156 | binary = output, 157 | version = ctx.attr.version, 158 | ), 159 | ] 160 | 161 | terraform_binary = rule( 162 | implementation = _terraform_binary_impl, 163 | attrs = { 164 | "binary": attr.label( 165 | mandatory = True, 166 | allow_single_file = True, 167 | executable = True, 168 | cfg = "host", 169 | doc = "Path to downloaded Terraform binary", 170 | ), 171 | "version": attr.string( 172 | mandatory = True, 173 | doc = "Version of Terraform", 174 | ), 175 | }, 176 | executable = True, 177 | ) 178 | 179 | def download_terraform_versions(versions): 180 | """Downloads multiple terraform versions. 181 | 182 | Args: 183 | versions: dict from terraform version to sha256 of SHA56SUMS file for that version. 184 | """ 185 | for version, sha in versions.items(): 186 | version_str = version.replace(".", "_") 187 | terraform_download( 188 | name = "terraform_{}".format(version_str), 189 | version = version, 190 | sha256 = sha, 191 | ) 192 | 193 | def _terraform_provider_download_impl(ctx): 194 | name = ctx.attr.provider_name 195 | platform = _detect_platform(ctx) 196 | version = ctx.attr.version 197 | 198 | # First get SHA256SUMS file so we can get all of the individual zip SHAs 199 | ctx.report_progress("Downloading and extracting SHA256SUMS file") 200 | sha256sums_url = "{base}/terraform-provider-{name}/{version}/terraform-provider-{name}_{version}_SHA256SUMS".format( 201 | base = hashicorp_base_url, 202 | name = name, 203 | version = version, 204 | ) 205 | ctx.download( 206 | url = sha256sums_url, 207 | sha256 = ctx.attr.sha256, 208 | output = "terraform_provider_sha256sums", 209 | ) 210 | sha_content = ctx.read("terraform_provider_sha256sums") 211 | sha_by_zip = _parse_sha_file(sha_content) 212 | 213 | # Terraform does not provide darwin_arm64 binaries before version 214 | # 1.0.2 or so. Also, many provider versions do not provide 215 | # darwin_arm64. Therefore, if the current platform is darwin_arm64 216 | # and we can't find a SHA for that platform, we fall back to 217 | # darwin_amd64 and depend on Rosetta. 218 | zip = "terraform-provider-{name}_{version}_{platform}.zip".format( 219 | name = name, 220 | version = version, 221 | platform = platform, 222 | ) 223 | if platform == "darwin_arm64" and zip not in sha_by_zip: 224 | platform = "darwin_amd64" 225 | zip = "terraform-provider-{name}_{version}_{platform}.zip".format( 226 | name = name, 227 | version = version, 228 | platform = platform, 229 | ) 230 | sha256 = sha_by_zip[zip] 231 | 232 | url = "{base}/terraform-provider-{name}/{version}/{zip}".format( 233 | base = hashicorp_base_url, 234 | name = name, 235 | version = version, 236 | zip = zip, 237 | ) 238 | 239 | # Now download actual Terraform zip 240 | ctx.report_progress("Downloading and extracting Terraform provider {}".format(name)) 241 | ctx.download_and_extract( 242 | url = url, 243 | sha256 = sha256, 244 | type = "zip", 245 | ) 246 | 247 | # Put a BUILD file here so we can use the resulting binary in other bazel 248 | # rules. 249 | ctx.file("BUILD.bazel", 250 | """load("@rules_terraform//:defs.bzl", "terraform_provider") 251 | 252 | terraform_provider( 253 | name = "provider", 254 | provider = glob(["terraform-provider-{name}_v{version}_x*"])[0], 255 | provider_name = "{name}", 256 | version = "{version}", 257 | source = "{source}", 258 | sha = "{sha}", 259 | platform = "{platform}", 260 | visibility = ["//visibility:public"] 261 | ) 262 | """.format( 263 | name = name, 264 | version = version, 265 | source = ctx.attr.source, 266 | sha = sha256, 267 | platform = platform, 268 | ), 269 | executable=False 270 | ) 271 | 272 | terraform_provider_download = repository_rule( 273 | implementation = _terraform_provider_download_impl, 274 | attrs = { 275 | "provider_name": attr.string( 276 | mandatory = True, 277 | ), 278 | "sha256": attr.string( 279 | mandatory = True, 280 | doc = "Expected SHA-256 sum of the downloaded archive", 281 | ), 282 | "version": attr.string( 283 | mandatory = True, 284 | doc = "Version of the Terraform provider", 285 | ), 286 | "source": attr.string( 287 | mandatory = True, 288 | doc = "Source for provider used in required_providers block", 289 | ), 290 | }, 291 | doc = "Downloads a Terraform provider", 292 | ) 293 | 294 | TerraformProviderInfo = provider( 295 | "Provider for the terraform_provider rule", 296 | fields={ 297 | "provider": "Path to Terraform provider", 298 | "provider_name": "Name of provider", 299 | "version": "Version of Terraform provider", 300 | "source": "Source for provider used in required_providers block", 301 | "sha": "SHA of Terraform provider binary", 302 | "platform": "Platform of Terraform provider binary, like linux_amd64", 303 | }) 304 | 305 | def _terraform_provider_impl(ctx): 306 | return [ 307 | DefaultInfo( 308 | files = depset([ctx.file.provider]), 309 | ), 310 | TerraformProviderInfo( 311 | provider = ctx.file.provider, 312 | provider_name = ctx.attr.provider_name, 313 | version = ctx.attr.version, 314 | source = ctx.attr.source, 315 | sha = ctx.attr.sha, 316 | platform = ctx.attr.platform, 317 | ), 318 | ] 319 | 320 | terraform_provider = rule( 321 | implementation = _terraform_provider_impl, 322 | attrs = { 323 | "provider": attr.label( 324 | mandatory = True, 325 | allow_single_file = True, 326 | doc = "Path to downloaded Terraform provider", 327 | ), 328 | "provider_name": attr.string( 329 | mandatory = True, 330 | doc = "Name of Terraform provider", 331 | ), 332 | "version": attr.string( 333 | mandatory = True, 334 | doc = "Version of Terraform provider", 335 | ), 336 | "source": attr.string( 337 | mandatory = True, 338 | doc = "Source for provider used in required_providers block", 339 | ), 340 | "sha": attr.string( 341 | mandatory = True, 342 | doc = "SHA of Terraform provider binary", 343 | ), 344 | "platform": attr.string( 345 | mandatory = True, 346 | doc = "Platform of Terraform provider binary, like linux_amd64", 347 | ), 348 | }, 349 | ) 350 | 351 | def download_terraform_provider_versions(provider_name, source, versions): 352 | """Downloads multiple terraform provider versions. 353 | 354 | Args: 355 | provider_name: string name for provider 356 | source: Source for provider, like registry.terraform.io/hashicorp/local 357 | versions: dict from terraform version to sha256 of SHA56SUMS file for that version. 358 | """ 359 | for version, sha in versions.items(): 360 | version_str = version.replace(".", "_") 361 | terraform_provider_download( 362 | name = "terraform_provider_{name}_{version_str}".format( 363 | name = provider_name, 364 | version_str = version_str, 365 | ), 366 | provider_name = provider_name, 367 | version = version, 368 | source = source, 369 | sha256 = sha, 370 | ) 371 | -------------------------------------------------------------------------------- /internal/modules.bzl: -------------------------------------------------------------------------------- 1 | load( 2 | ":download.bzl", 3 | "TerraformBinaryInfo", 4 | "TerraformProviderInfo", 5 | ) 6 | 7 | TerraformModuleInfo = provider( 8 | "Provider for the terraform_module rule", 9 | fields={ 10 | "source_files": "depset of source Terraform files", 11 | "modules": "depset of modules", 12 | "providers": "depset of providers", 13 | }) 14 | 15 | def _terraform_module_impl(ctx): 16 | source_files = [] 17 | 18 | # Symlink all non-generated files so they are stored alongside any generated 19 | # files. Terraform runs for a given directory and the files need to be in 20 | # their correct positions, so we can't just reference the different input 21 | # files if they are in different directories in the bazel sandbox. 22 | for src in ctx.files.srcs: 23 | if src.is_source: 24 | src_symlink = ctx.actions.declare_file(src.basename) 25 | ctx.actions.symlink(output = src_symlink, target_file = src) 26 | source_files.append(src_symlink) 27 | else: 28 | source_files.append(src) 29 | 30 | # Generate required_providers block based on any provider inputs to this 31 | # rule. 32 | if ctx.attr.generate_required_providers and ctx.attr.providers: 33 | required_providers = {} 34 | for provider in ctx.attr.providers: 35 | provider = provider[TerraformProviderInfo] 36 | required_providers[provider.provider_name] = { 37 | "source": provider.source, 38 | "version": "= " + provider.version, 39 | } 40 | 41 | required_providers_struct = struct( 42 | terraform = struct ( 43 | required_providers = required_providers 44 | ), 45 | ) 46 | 47 | required_providers_file = ctx.actions.declare_file("__bazel_required_providers.tf.json") 48 | ctx.actions.write( 49 | required_providers_file, 50 | json.encode_indent(required_providers_struct, indent = ' '), 51 | is_executable = False, 52 | ) 53 | source_files.append(required_providers_file) 54 | 55 | return [ 56 | DefaultInfo(files = depset(source_files)), 57 | TerraformModuleInfo( 58 | source_files = depset( 59 | source_files, 60 | transitive = [dep[TerraformModuleInfo].source_files for dep in ctx.attr.deps] 61 | ), 62 | modules = depset( 63 | ctx.attr.deps, 64 | transitive = [dep[TerraformModuleInfo].modules for dep in ctx.attr.deps] 65 | ), 66 | providers = depset( 67 | ctx.attr.providers, 68 | transitive = [dep[TerraformModuleInfo].providers for dep in ctx.attr.deps] 69 | ), 70 | ) 71 | ] 72 | 73 | terraform_module = rule( 74 | implementation = _terraform_module_impl, 75 | doc = """Collects files and dependencies for a Terraform module. 76 | 77 | This rules does nothing by itself really, but its output of this rule is used 78 | in other rules like terraform_root_module or for tests. 79 | """, 80 | attrs = { 81 | "srcs": attr.label_list( 82 | mandatory = True, 83 | allow_files = True, 84 | ), 85 | "providers": attr.label_list( 86 | providers = [TerraformProviderInfo], 87 | ), 88 | "deps": attr.label_list( 89 | providers = [TerraformModuleInfo], 90 | ), 91 | "generate_required_providers": attr.bool( 92 | default = True, 93 | doc = "Generate a required_providers block with provider versions", 94 | ), 95 | }, 96 | ) 97 | 98 | TerraformRootModuleInfo = provider( 99 | "Provider for the terraform_root_module rule", 100 | fields={ 101 | "terraform_wrapper": "Terraform wrapper script to run terraform in this rule's output directory", 102 | "runfiles": "depset of collected files needed to run", 103 | }) 104 | 105 | def _terraform_root_module_impl(ctx): 106 | terraform_info = ctx.attr.terraform[TerraformBinaryInfo] 107 | terraform_binary = terraform_info.binary 108 | terraform_version = terraform_info.version 109 | 110 | module = ctx.attr.module[TerraformModuleInfo] 111 | 112 | runfiles = [terraform_binary] + module.source_files.to_list() 113 | 114 | modules_list = module.modules.to_list() 115 | providers_list = [p[TerraformProviderInfo] for p in module.providers.to_list()] 116 | provider_files = [p.provider for p in providers_list] 117 | 118 | # Create a plugin cache dir. 119 | plugin_cache_dir = "plugin_cache" 120 | for provider in providers_list: 121 | output = ctx.actions.declare_file("{}/{}/{}".format( 122 | plugin_cache_dir, 123 | provider.platform, 124 | provider.provider.basename, 125 | )) 126 | ctx.actions.symlink( 127 | output = output, 128 | target_file = provider.provider, 129 | ) 130 | runfiles.append(output) 131 | 132 | # Special filesystem mirror format 133 | if terraform_version >= "0.13.2": 134 | output = ctx.actions.declare_file("{}/{}/{}/{}/{}".format( 135 | plugin_cache_dir, 136 | provider.source, 137 | provider.version, 138 | provider.platform, 139 | provider.provider.basename, 140 | )) 141 | ctx.actions.symlink( 142 | output = output, 143 | target_file = provider.provider, 144 | ) 145 | runfiles.append(output) 146 | 147 | # Create terraformrc 148 | terraformrc = ctx.actions.declare_file(ctx.label.name + "_terraformrc.tfrc") 149 | runfiles.append(terraformrc) 150 | terraformrc_content = """ 151 | plugin_cache_dir = "{}" 152 | """.format(plugin_cache_dir) 153 | 154 | # Use and explicit filesystem_mirror block if terraform_version >= 0.13.2 155 | if terraform_version >= "0.13.2": 156 | terraformrc_content += """ 157 | provider_installation {{ 158 | filesystem_mirror {{ 159 | path = "{plugin_cache_dir}" 160 | include = ["*/*/*"] 161 | }} 162 | }} 163 | """.format(plugin_cache_dir = plugin_cache_dir) 164 | 165 | ctx.actions.write( 166 | output = terraformrc, 167 | content = terraformrc_content, 168 | is_executable = False, 169 | ) 170 | 171 | # Create a wrapper script that runs terraform in a bazel run directory with 172 | # all of the necessary files symlinked. 173 | wrapper = ctx.actions.declare_file(ctx.label.name + "_run_wrapper") 174 | runfiles.append(wrapper) 175 | ctx.actions.write( 176 | output = wrapper, 177 | is_executable = True, 178 | content = """ 179 | set -eu 180 | 181 | terraform="$(pwd)/{terraform}" 182 | 183 | cd "{package}" 184 | 185 | # If TF_DATA_DIR is unset, set it to a special directory under the workspace 186 | # root. This env var _is_ set in e.g. tests so they can do terraform init 187 | # without affecting users' .terraform files. 188 | # 189 | # We can't store .terraform as a bazel file because there is lots of mutable 190 | # state in there, and we can't mutate it if it is written from a bazel rule. For 191 | # example, the S3 backend requires initialization with valid AWS credentials, 192 | # which we can't provide during a bazel build. 193 | # 194 | # TODO: Try to more intelligently cache parts of .terraform, like the providers/ 195 | # directory. We should ideally make installing those as fast as possible. 196 | # 197 | export TF_DATA_DIR="${{TF_DATA_DIR:-$BUILD_WORKSPACE_DIRECTORY/{package}/.terraform}}" 198 | 199 | export TF_CLI_CONFIG_FILE="{terraformrc}" 200 | 201 | exec "$terraform" $@ 202 | """.format( 203 | package = ctx.label.package, 204 | terraform = terraform_binary.short_path, 205 | terraformrc = terraformrc.basename, 206 | ), 207 | ) 208 | 209 | return [ 210 | DefaultInfo( 211 | runfiles = ctx.runfiles(files = runfiles), 212 | executable = wrapper, 213 | ), 214 | TerraformRootModuleInfo( 215 | terraform_wrapper = wrapper, 216 | runfiles = ctx.runfiles(files = runfiles), 217 | ) 218 | ] 219 | 220 | terraform_root_module = rule( 221 | implementation = _terraform_root_module_impl, 222 | doc = """Provides runnable Terraform wrapper script and providers for a root module. 223 | 224 | This rule builds an executable wrapper script that runs Terraform for the root module 225 | with all of the necessary bits in place from the dependent module. 226 | """, 227 | attrs = { 228 | "module": attr.label( 229 | mandatory = True, 230 | providers = [TerraformModuleInfo], 231 | ), 232 | "terraform": attr.label( 233 | allow_single_file = True, 234 | executable = True, 235 | cfg = "host", 236 | providers = [TerraformBinaryInfo], 237 | ), 238 | }, 239 | executable = True, 240 | ) 241 | -------------------------------------------------------------------------------- /internal/starlark/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | 3 | go_library( 4 | name = "starlark_lib", 5 | srcs = ["main.go"], 6 | importpath = "github.com/jdreaver/rules_terraform/internal/starlark", 7 | visibility = ["//visibility:private"], 8 | deps = [ 9 | "@net_starlark_go//starlark:go_default_library", 10 | "@net_starlark_go//starlarkjson:go_default_library", 11 | ], 12 | ) 13 | 14 | go_binary( 15 | name = "starlark", 16 | embed = [":starlark_lib"], 17 | # TODO: Do we have to make this public to use it in rules? Any way to re-export? 18 | # visibility = ["//:__subpackages__"], 19 | visibility = ["//visibility:public"], 20 | ) 21 | -------------------------------------------------------------------------------- /internal/starlark/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | 10 | "go.starlark.net/starlark" 11 | "go.starlark.net/starlarkjson" 12 | ) 13 | 14 | var input = flag.String("input", "", "input Starlark file path") 15 | var output = flag.String("output", "", "output JSON file path") 16 | var expr = flag.String("expr", "encode_indent(main())", "Starlark expression to call to produce output") 17 | var lib = flag.String("lib", "", "Starlark helper code to support running expr") 18 | 19 | func main() { 20 | flag.Parse() 21 | if *input == "" || *output == "" { 22 | flag.Usage() 23 | os.Exit(1) 24 | } 25 | 26 | // Add JSON module to globals 27 | starlark.Universe["json"] = starlarkjson.Module 28 | 29 | // Resolve input Starlark program 30 | thread := makeThreadForFile(*input, MakeLoad()) 31 | globals, err := starlark.ExecFile(thread, *input, nil, nil) 32 | if err != nil { 33 | panic(fmt.Sprintf("failed to exec input file: %v", err)) 34 | } 35 | 36 | // Resolve library code 37 | processLibrary(globals, thread, internalLib) 38 | processLibrary(globals, thread, *lib) 39 | 40 | // Run wrapper that calls main and encodes as JSON 41 | val, err := starlark.Eval(thread, "eval_wrapper.star", *expr, globals) 42 | if err != nil { 43 | panic(fmt.Sprintf("failed to eval wrapper: %v", err)) 44 | } 45 | 46 | // Ensure we got a String 47 | jsonString, ok := val.(starlark.String) 48 | if !ok { 49 | panic(fmt.Sprintf("expected String output, but got %T", val)) 50 | } 51 | 52 | // Write JSON to output file 53 | err = ioutil.WriteFile(*output, []byte(jsonString.GoString()), 0644) 54 | if err != nil { 55 | panic(fmt.Sprintf("error writing to output file %s: %v", *output, err)) 56 | } 57 | } 58 | 59 | func processLibrary(globals starlark.StringDict, thread *starlark.Thread, libraryCode string) { 60 | libGlobals, err := starlark.ExecFile(thread, "internal_lib.star", libraryCode, globals) 61 | if err != nil { 62 | panic(fmt.Sprintf("failed to execute internal lib file: %v", err)) 63 | } 64 | for key, val := range libGlobals { 65 | globals[key] = val 66 | } 67 | } 68 | 69 | // // Small helper functions to make execution easier 70 | const internalLib = ` 71 | def encode_indent(x): 72 | return json.indent(json.encode(x), indent=' ') 73 | 74 | def assert_type(x, expected_type): 75 | if type(x) != expected_type: 76 | fail("expected type", expected_type, "but got", type(x)) 77 | ` 78 | 79 | func makeThreadForFile(modulePath string, load func(thread *starlark.Thread, module string) (starlark.StringDict, error)) *starlark.Thread { 80 | thread := &starlark.Thread{Name: "exec " + modulePath, Load: load} 81 | // Current directory is stored in thread local variable 82 | // so we can resolve relative imports. 83 | thread.SetLocal("_source_dir", path.Dir(modulePath)) 84 | return thread 85 | } 86 | 87 | // MakeLoad returns a simple sequential implementation of module loading 88 | // suitable for use in the REPL. 89 | // Each function returned by MakeLoad accesses a distinct private cache. 90 | func MakeLoad() func(thread *starlark.Thread, module string) (starlark.StringDict, error) { 91 | type entry struct { 92 | globals starlark.StringDict 93 | err error 94 | } 95 | 96 | var cache = make(map[string]*entry) 97 | 98 | return func(thread *starlark.Thread, module string) (starlark.StringDict, error) { 99 | e, ok := cache[module] 100 | if e == nil { 101 | if ok { 102 | // request for package whose loading is in progress 103 | return nil, fmt.Errorf("cycle in load graph") 104 | } 105 | 106 | // Add a placeholder to indicate "load in progress". 107 | cache[module] = nil 108 | 109 | // Current directory is stored in thread local variable 110 | // so we can resolve relative imports. 111 | sourceDirInterface := thread.Local("_source_dir") 112 | sourceDir, ok := sourceDirInterface.(string) 113 | if !ok { 114 | panic("internal error: couldn't find _source_dir thread local") 115 | } 116 | modulePath := path.Join(sourceDir, module) 117 | 118 | // Load it 119 | thread := makeThreadForFile(modulePath, thread.Load) 120 | globals, err := starlark.ExecFile(thread, modulePath, nil, nil) 121 | e = &entry{globals, err} 122 | 123 | // Update the cache. 124 | cache[module] = e 125 | } 126 | return e.globals, e.err 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /internal/starlark/util.bzl: -------------------------------------------------------------------------------- 1 | def run_starlark_executor(ctx, output, src, deps, executor, lib, expr): 2 | ctx.actions.run( 3 | outputs = [output], 4 | inputs = [src] + deps, 5 | executable = executor, 6 | arguments = [ 7 | "-input", src.path, 8 | "-output", output.path, 9 | "-expr", expr, 10 | "-lib", lib, 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /internal/tests.bzl: -------------------------------------------------------------------------------- 1 | load( 2 | ":download.bzl", 3 | "TerraformBinaryInfo", 4 | ) 5 | load( 6 | ":modules.bzl", 7 | "TerraformModuleInfo", 8 | "TerraformRootModuleInfo", 9 | ) 10 | 11 | def _terraform_validate_test_impl(ctx): 12 | root = ctx.attr.root_module[TerraformRootModuleInfo] 13 | 14 | # Call the wrapper script from the root module and just run validate 15 | exe = ctx.actions.declare_file(ctx.label.name + "_validate_test_wrapper") 16 | ctx.actions.write( 17 | output = exe, 18 | is_executable = True, 19 | content = """ 20 | set -eu 21 | 22 | export TF_DATA_DIR=.terraform 23 | 24 | # Avoids having the test suite complain about the aws provider plugin changing. 25 | # Suggested here: https://github.com/hashicorp/terraform/issues/16017 26 | export TF_SKIP_PROVIDER_VERIFY=1 27 | 28 | "{terraform}" init -backend=false 29 | 30 | exec "{terraform}" validate""".format( 31 | terraform = root.terraform_wrapper.short_path, 32 | ), 33 | ) 34 | 35 | return [DefaultInfo( 36 | runfiles = root.runfiles, 37 | executable = exe, 38 | )] 39 | 40 | terraform_validate_test = rule( 41 | implementation = _terraform_validate_test_impl, 42 | attrs = { 43 | "root_module": attr.label( 44 | mandatory = True, 45 | providers = [TerraformRootModuleInfo], 46 | ), 47 | }, 48 | test = True, 49 | ) 50 | 51 | def _terraform_format_test_impl(ctx): 52 | module = ctx.attr.module[TerraformModuleInfo] 53 | terraform_info = ctx.attr.terraform[TerraformBinaryInfo] 54 | terraform_binary = terraform_info.binary 55 | 56 | # Call terraform fmt inside the module directory 57 | exe = ctx.actions.declare_file(ctx.label.name + "_format_test_wrapper") 58 | ctx.actions.write( 59 | output = exe, 60 | is_executable = True, 61 | content = """ 62 | set -eu 63 | 64 | terraform="$(pwd)/{terraform}" 65 | 66 | cd "{module_path}" 67 | 68 | set +e 69 | output=$("$terraform" fmt -check -recursive) 70 | if [ $? -ne 0 ]; then 71 | echo "Terraform format test failed! The following files need 'terraform fmt' to be run:\n$output" 72 | exit 1 73 | fi 74 | """.format( 75 | terraform = terraform_binary.short_path, 76 | module_path = ctx.attr.module.label.package, 77 | ), 78 | ) 79 | 80 | return [DefaultInfo( 81 | runfiles = ctx.runfiles([terraform_binary] + module.source_files.to_list()), 82 | executable = exe, 83 | )] 84 | 85 | terraform_format_test = rule( 86 | implementation = _terraform_format_test_impl, 87 | attrs = { 88 | "module": attr.label( 89 | mandatory = True, 90 | providers = [TerraformModuleInfo], 91 | ), 92 | "terraform": attr.label( 93 | allow_single_file = True, 94 | executable = True, 95 | cfg = "host", 96 | providers = [TerraformBinaryInfo], 97 | ), 98 | }, 99 | test = True, 100 | ) 101 | -------------------------------------------------------------------------------- /internal/variables.bzl: -------------------------------------------------------------------------------- 1 | load("//internal/starlark:util.bzl", "run_starlark_executor") 2 | 3 | def _terraform_locals_impl(ctx): 4 | output = ctx.actions.declare_file(ctx.label.name + "_locals.tf.json") 5 | 6 | run_starlark_executor( 7 | ctx, 8 | output, 9 | ctx.file.src, 10 | ctx.files.deps, 11 | ctx.executable._starlark_executor, 12 | """ 13 | # Create local variable definitions for .tf.json file 14 | def wrap_locals(x): 15 | assert_type(x, "dict") 16 | 17 | return { "locals": x } 18 | """, 19 | "encode_indent(wrap_locals(main()))", 20 | ) 21 | 22 | return [DefaultInfo(files = depset([output]))] 23 | 24 | terraform_locals = rule( 25 | implementation = _terraform_locals_impl, 26 | doc = "Creates a .tf.json file defining local variables from a Starlark dict", 27 | attrs = { 28 | "src": attr.label( 29 | doc = "Source Starlark file to execute", 30 | mandatory = True, 31 | allow_single_file = True, 32 | ), 33 | "deps": attr.label_list( 34 | doc = "Files needed to execute Starlark", 35 | ), 36 | "_starlark_executor": attr.label( 37 | default = Label("//internal/starlark"), 38 | allow_single_file = True, 39 | executable = True, 40 | cfg = "exec", 41 | ), 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem }: 2 | 3 | (builtins.getFlake (toString ./.)).devShell.${system} 4 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | /bazel-* 2 | .terraform/ 3 | -------------------------------------------------------------------------------- /test/0_12_31/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load( 2 | "@rules_terraform//:defs.bzl", 3 | "terraform_format_test", 4 | "terraform_module", 5 | "terraform_root_module", 6 | "terraform_validate_test", 7 | ) 8 | 9 | TERRAFORM = "@terraform_0_12_31//:terraform" 10 | 11 | terraform_module( 12 | name = "module", 13 | srcs = glob(["**/*.tf"]), 14 | providers = [ 15 | "@terraform_provider_local_2_1_0//:provider", 16 | ], 17 | deps = [ 18 | "//time_module:module", 19 | ], 20 | ) 21 | 22 | terraform_root_module( 23 | name = "root_module", 24 | module = ":module", 25 | terraform = TERRAFORM, 26 | ) 27 | 28 | terraform_validate_test( 29 | name = "validate", 30 | root_module = ":root_module", 31 | ) 32 | 33 | terraform_format_test( 34 | name = "format", 35 | module = ":module", 36 | terraform = TERRAFORM, 37 | ) 38 | -------------------------------------------------------------------------------- /test/0_12_31/main.tf: -------------------------------------------------------------------------------- 1 | # local provider 2 | resource "local_file" "hello" { 3 | content = "Hello, world!" 4 | filename = "/tmp/rules_terraform/hello.txt" 5 | } 6 | 7 | module "time" { 8 | source = "../time_module" 9 | } 10 | -------------------------------------------------------------------------------- /test/1_1_2/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load( 2 | "@rules_terraform//:defs.bzl", 3 | "terraform_format_test", 4 | "terraform_module", 5 | "terraform_root_module", 6 | "terraform_validate_test", 7 | ) 8 | 9 | TERRAFORM = "@terraform_1_1_2//:terraform" 10 | 11 | terraform_module( 12 | name = "module", 13 | srcs = glob(["**/*.tf"]), 14 | providers = [ 15 | "@terraform_provider_local_2_1_0//:provider", 16 | ], 17 | deps = [ 18 | "//time_module:module", 19 | ], 20 | ) 21 | 22 | terraform_root_module( 23 | name = "root_module", 24 | module = ":module", 25 | terraform = TERRAFORM, 26 | ) 27 | 28 | terraform_validate_test( 29 | name = "validate", 30 | root_module = ":root_module", 31 | ) 32 | 33 | terraform_format_test( 34 | name = "format", 35 | module = ":module", 36 | terraform = TERRAFORM, 37 | ) 38 | -------------------------------------------------------------------------------- /test/1_1_2/main.tf: -------------------------------------------------------------------------------- 1 | # local provider 2 | resource "local_file" "hello" { 3 | content = "Hello, world!" 4 | filename = "/tmp/rules_terraform/hello.txt" 5 | } 6 | 7 | module "time" { 8 | source = "../time_module" 9 | } 10 | -------------------------------------------------------------------------------- /test/BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdreaver/rules_terraform/e460befbb3d3204e132085020e6b565a224b838e/test/BUILD.bazel -------------------------------------------------------------------------------- /test/WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "test_rules_terraform") 2 | 3 | # rules_terraform 4 | local_repository( 5 | name = "rules_terraform", 6 | path = "../", 7 | ) 8 | 9 | load("@rules_terraform//:deps.bzl", "rules_terraform_repositories") 10 | rules_terraform_repositories() 11 | 12 | load( 13 | "@rules_terraform//:defs.bzl", 14 | "download_terraform_versions", 15 | "download_terraform_provider_versions", 16 | ) 17 | 18 | download_terraform_versions({ 19 | # These are SHAs of the SHA265SUM file for a given version. They can be 20 | # found with: 21 | # curl https://releases.hashicorp.com/terraform/{version}/terraform_{version}_SHA256SUMS | sha256sum 22 | "0.12.31": "f9a95c24c77091a1ae0ca2539f39ccfb2639c59934858fada6f4950541386fad", 23 | "1.1.2": "20e4115a8c6aff07421ebc6645056f9a6605ab5a196475ab46a65fea71b6b090", 24 | }) 25 | 26 | # Provider SHAs are from the SHA265SUM file for a given version. They can be 27 | # found with: 28 | # curl https://releases.hashicorp.com/terraform-provider-{name}/{version}/terraform-provider-{name}_{version}_SHA256SUMS | sha256sum 29 | download_terraform_provider_versions( 30 | "aws", 31 | "registry.terraform.io/hashicorp/aws", 32 | { 33 | "3.67.0": "28a434313ee86d8ed2721360e5741957bcc48b1a102d878f0fb34274f41aee81", 34 | }, 35 | ) 36 | 37 | download_terraform_provider_versions( 38 | "local", 39 | "registry.terraform.io/hashicorp/local", 40 | { 41 | "2.1.0": "dae594d82be6be5ee83f8d081cc8a05af45ac1bbf7fdb8bea16ab4c1d6032043", 42 | }, 43 | ) 44 | 45 | download_terraform_provider_versions( 46 | "time", 47 | "registry.terraform.io/hashicorp/time", 48 | { 49 | "0.7.0": "ccd73836657ce361f83a5f11f0359cd366f2a228a0c03c78db11baf47c5a2d94", 50 | }, 51 | ) 52 | 53 | # 54 | # Go dependencies for rules_terraform 55 | # 56 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 57 | http_archive( 58 | name = "io_bazel_rules_go", 59 | sha256 = "2b1641428dff9018f9e85c0384f03ec6c10660d935b750e3fa1492a281a53b0f", 60 | urls = [ 61 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", 62 | "https://github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", 63 | ], 64 | ) 65 | 66 | http_archive( 67 | name = "bazel_gazelle", 68 | sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb", 69 | urls = [ 70 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", 71 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", 72 | ], 73 | ) 74 | 75 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") 76 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") 77 | 78 | go_rules_dependencies() 79 | 80 | # On NixOS, need to use host toolchain 81 | # https://github.com/bazelbuild/rules_go/issues/1376 82 | go_register_toolchains(version = "host") 83 | # go_register_toolchains(version = "1.17.1") 84 | 85 | gazelle_dependencies() 86 | -------------------------------------------------------------------------------- /test/common/BUILD.bazel: -------------------------------------------------------------------------------- 1 | filegroup( 2 | name = "common", 3 | srcs = [ 4 | "config.star", 5 | ], 6 | visibility = ["//visibility:public"], 7 | ) 8 | -------------------------------------------------------------------------------- /test/common/config.star: -------------------------------------------------------------------------------- 1 | # Common config values used across multiple Terraform modules. 2 | 3 | state_s3_bucket = "jdreaver-rules-terraform-test-state" 4 | state_s3_region = "us-west-2" 5 | state_dynamodb_table = "terraform-statelock" 6 | 7 | def create_backend_config(key): 8 | return { 9 | "bucket": state_s3_bucket, 10 | "region": state_s3_region, 11 | "dynamodb_table": state_dynamodb_table, 12 | "key": key, 13 | } 14 | -------------------------------------------------------------------------------- /test/hello_ec2/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load( 2 | "@rules_terraform//:defs.bzl", 3 | "terraform_backend", 4 | "terraform_format_test", 5 | "terraform_module", 6 | "terraform_remote_state", 7 | "terraform_root_module", 8 | "terraform_validate_test", 9 | ) 10 | 11 | TERRAFORM = "@terraform_1_1_2//:terraform" 12 | 13 | terraform_backend( 14 | name = "backend", 15 | src = "backend.star", 16 | deps = ["//common"], 17 | ) 18 | 19 | terraform_remote_state( 20 | name = "vpc_remote_state", 21 | backend = "//vpc:backend", 22 | variable_name = "vpc", 23 | ) 24 | 25 | terraform_module( 26 | name = "module", 27 | srcs = [ 28 | "main.tf", 29 | ":backend", 30 | ":vpc_remote_state", 31 | ], 32 | providers = [ 33 | "@terraform_provider_aws_3_67_0//:provider", 34 | ], 35 | ) 36 | 37 | terraform_root_module( 38 | name = "root_module", 39 | module = ":module", 40 | terraform = TERRAFORM, 41 | ) 42 | 43 | terraform_validate_test( 44 | name = "validate", 45 | root_module = ":root_module", 46 | ) 47 | 48 | terraform_format_test( 49 | name = "format", 50 | module = ":module", 51 | terraform = TERRAFORM, 52 | ) 53 | -------------------------------------------------------------------------------- /test/hello_ec2/backend.star: -------------------------------------------------------------------------------- 1 | load("../common/config.star", "create_backend_config") 2 | 3 | def main(): 4 | return { 5 | "backend_type": "s3", 6 | "config": create_backend_config("hello_ec2"), 7 | } 8 | -------------------------------------------------------------------------------- /test/hello_ec2/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-west-2" 3 | } 4 | 5 | resource "aws_security_group" "hello_ec2" { 6 | name = "rules_terraform_hello_ec2" 7 | description = "hello_ec2 group for rules_terraform testing" 8 | vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id 9 | 10 | ingress { 11 | description = "HTTPS" 12 | from_port = 443 13 | to_port = 443 14 | protocol = "tcp" 15 | cidr_blocks = ["0.0.0.0/0"] 16 | } 17 | 18 | ingress { 19 | description = "HTTP" 20 | from_port = 80 21 | to_port = 80 22 | protocol = "tcp" 23 | cidr_blocks = ["0.0.0.0/0"] 24 | } 25 | 26 | ingress { 27 | description = "SSH" 28 | from_port = 22 29 | to_port = 22 30 | protocol = "tcp" 31 | cidr_blocks = ["0.0.0.0/0"] 32 | } 33 | 34 | ingress { 35 | description = "ICMP" 36 | from_port = -1 37 | to_port = -1 38 | protocol = "icmp" 39 | cidr_blocks = ["0.0.0.0/0"] 40 | } 41 | 42 | egress { 43 | from_port = 0 44 | to_port = 0 45 | protocol = "-1" 46 | cidr_blocks = ["0.0.0.0/0"] 47 | } 48 | } 49 | 50 | resource "aws_instance" "hello_ec2" { 51 | ami = data.aws_ami.ubuntu.id 52 | instance_type = "t2.micro" 53 | availability_zone = data.terraform_remote_state.vpc.outputs.public_subnet_az 54 | key_name = aws_key_pair.hello_ec2.key_name 55 | 56 | tags = { 57 | Name = "rules-terraform-test-hello-ec2" 58 | } 59 | 60 | network_interface { 61 | network_interface_id = aws_network_interface.hello_ec2.id 62 | device_index = 0 63 | } 64 | } 65 | 66 | resource "aws_key_pair" "hello_ec2" { 67 | key_name = "hello-ec2-key" 68 | # If you are reading this and want to try this out you should change to your 69 | # public key 70 | public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILwoAJNSGvmkNJ5SB+F3QO0gb4k4ml70/omPtbkR6g1V johndreaver@gmail.com" 71 | } 72 | 73 | resource "aws_network_interface" "hello_ec2" { 74 | subnet_id = data.terraform_remote_state.vpc.outputs.public_subnet_id 75 | security_groups = [ 76 | aws_security_group.hello_ec2.id 77 | ] 78 | 79 | tags = { 80 | Name = "rules-terraform-test-hello-ec2" 81 | } 82 | } 83 | 84 | resource "aws_eip" "hello_ec2" { 85 | vpc = true 86 | network_interface = aws_network_interface.hello_ec2.id 87 | } 88 | 89 | data "aws_ami" "ubuntu" { 90 | most_recent = true 91 | 92 | filter { 93 | name = "name" 94 | values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] 95 | } 96 | 97 | filter { 98 | name = "virtualization-type" 99 | values = ["hvm"] 100 | } 101 | 102 | owners = ["099720109477"] # Canonical 103 | } 104 | 105 | output "server_public_ip" { 106 | value = aws_eip.hello_ec2.public_ip 107 | } 108 | -------------------------------------------------------------------------------- /test/tf_state_bootstrap/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load( 2 | "@rules_terraform//:defs.bzl", 3 | "terraform_backend", 4 | "terraform_format_test", 5 | "terraform_locals", 6 | "terraform_module", 7 | "terraform_root_module", 8 | "terraform_validate_test", 9 | ) 10 | 11 | TERRAFORM = "@terraform_1_1_2//:terraform" 12 | 13 | # N.B. When this is first created, we need to comment out the backend block and 14 | # manually store the state in the local filesystem. Once the bucket exists, we 15 | # can store the state in the bucket by uncommenting this block: 16 | # 17 | # 1. Comment out backend 18 | # 2. $ terraform apply 19 | # 3. Uncomment backend 20 | # 4. $ terraform init 21 | # 5. $ terraform apply 22 | terraform_backend( 23 | name = "backend", 24 | src = "backend.star", 25 | deps = ["//common"], 26 | ) 27 | 28 | terraform_locals( 29 | name = "locals", 30 | src = "locals.star", 31 | deps = ["//common"], 32 | ) 33 | 34 | terraform_module( 35 | name = "module", 36 | srcs = [ 37 | "main.tf", 38 | ":backend", 39 | ":locals", 40 | ], 41 | providers = [ 42 | "@terraform_provider_aws_3_67_0//:provider", 43 | ], 44 | ) 45 | 46 | terraform_root_module( 47 | name = "root_module", 48 | module = ":module", 49 | terraform = TERRAFORM, 50 | ) 51 | 52 | terraform_validate_test( 53 | name = "validate", 54 | root_module = ":root_module", 55 | ) 56 | 57 | terraform_format_test( 58 | name = "format", 59 | module = ":module", 60 | terraform = TERRAFORM, 61 | ) 62 | -------------------------------------------------------------------------------- /test/tf_state_bootstrap/backend.star: -------------------------------------------------------------------------------- 1 | load("../common/config.star", "create_backend_config") 2 | 3 | def main(): 4 | return { 5 | "backend_type": "s3", 6 | "config": create_backend_config("tf_state_bootstrap"), 7 | } 8 | -------------------------------------------------------------------------------- /test/tf_state_bootstrap/locals.star: -------------------------------------------------------------------------------- 1 | load("../common/config.star", "state_dynamodb_table", "state_s3_bucket") 2 | 3 | def main(): 4 | return { 5 | "bucket_name": state_s3_bucket, 6 | "dynamodb_table_name": state_dynamodb_table, 7 | } 8 | -------------------------------------------------------------------------------- /test/tf_state_bootstrap/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-west-2" 3 | } 4 | 5 | resource "aws_s3_bucket" "state_bucket" { 6 | bucket = local.bucket_name 7 | acl = "private" 8 | 9 | versioning { 10 | enabled = true 11 | } 12 | } 13 | 14 | resource "aws_dynamodb_table" "terraform_statelock" { 15 | name = local.dynamodb_table_name 16 | hash_key = "LockID" 17 | billing_mode = "PAY_PER_REQUEST" 18 | 19 | attribute { 20 | name = "LockID" 21 | type = "S" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/time_module/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load( 2 | "@rules_terraform//:defs.bzl", 3 | "terraform_format_test", 4 | "terraform_module", 5 | ) 6 | 7 | terraform_module( 8 | name = "module", 9 | srcs = glob(["**/*.tf"]), 10 | providers = [ 11 | "@terraform_provider_time_0_7_0//:provider", 12 | ], 13 | visibility = ["//visibility:public"], 14 | ) 15 | 16 | terraform_format_test( 17 | name = "format", 18 | module = ":module", 19 | terraform = "@terraform_1_1_2//:terraform", 20 | ) 21 | -------------------------------------------------------------------------------- /test/time_module/main.tf: -------------------------------------------------------------------------------- 1 | # time provider 2 | resource "time_offset" "example" { 3 | offset_days = 7 4 | } 5 | 6 | output "one_week_from_now" { 7 | value = time_offset.example.rfc3339 8 | } 9 | -------------------------------------------------------------------------------- /test/vpc/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load( 2 | "@rules_terraform//:defs.bzl", 3 | "terraform_backend", 4 | "terraform_format_test", 5 | "terraform_module", 6 | "terraform_root_module", 7 | "terraform_validate_test", 8 | ) 9 | 10 | TERRAFORM = "@terraform_1_1_2//:terraform" 11 | 12 | terraform_backend( 13 | name = "backend", 14 | src = "backend.star", 15 | visibility = ["//visibility:public"], 16 | deps = ["//common"], 17 | ) 18 | 19 | terraform_module( 20 | name = "module", 21 | srcs = [ 22 | "main.tf", 23 | ":backend", 24 | ], 25 | providers = [ 26 | "@terraform_provider_aws_3_67_0//:provider", 27 | ], 28 | ) 29 | 30 | terraform_root_module( 31 | name = "root_module", 32 | module = ":module", 33 | terraform = TERRAFORM, 34 | ) 35 | 36 | terraform_validate_test( 37 | name = "validate", 38 | root_module = ":root_module", 39 | ) 40 | 41 | terraform_format_test( 42 | name = "format", 43 | module = ":module", 44 | terraform = TERRAFORM, 45 | ) 46 | -------------------------------------------------------------------------------- /test/vpc/backend.star: -------------------------------------------------------------------------------- 1 | load("../common/config.star", "create_backend_config") 2 | 3 | def main(): 4 | return { 5 | "backend_type": "s3", 6 | "config": create_backend_config("vpc"), 7 | } 8 | -------------------------------------------------------------------------------- /test/vpc/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-west-2" 3 | } 4 | 5 | resource "aws_vpc" "main" { 6 | cidr_block = "10.25.0.0/16" 7 | instance_tenancy = "default" 8 | 9 | tags = { 10 | Name = "rules-terraform-test-vpc" 11 | } 12 | } 13 | 14 | locals { 15 | public_subnet_az = "us-west-2c" 16 | } 17 | 18 | resource "aws_subnet" "public" { 19 | vpc_id = aws_vpc.main.id 20 | cidr_block = "10.25.0.0/24" 21 | availability_zone = local.public_subnet_az 22 | 23 | tags = { 24 | Name = "rules-terraform-test-subnet" 25 | } 26 | } 27 | 28 | resource "aws_internet_gateway" "igw" { 29 | vpc_id = aws_vpc.main.id 30 | 31 | tags = { 32 | Name = "rules-terraform-test-igw" 33 | } 34 | } 35 | 36 | resource "aws_default_route_table" "main_default" { 37 | default_route_table_id = aws_vpc.main.default_route_table_id 38 | 39 | route { 40 | cidr_block = "0.0.0.0/0" 41 | gateway_id = aws_internet_gateway.igw.id 42 | } 43 | } 44 | 45 | output "vpc_id" { 46 | value = aws_vpc.main.id 47 | } 48 | 49 | output "public_subnet_id" { 50 | value = aws_subnet.public.id 51 | } 52 | 53 | output "public_subnet_az" { 54 | value = local.public_subnet_az 55 | } 56 | --------------------------------------------------------------------------------