Skip to content

Commit

Permalink
Feat: support OpenTofu (#185)
Browse files Browse the repository at this point in the history
* Feat: support OpenTofu

* fix pipeline

* more changes

* use opentofu

* added arg default-to-terraform
  • Loading branch information
TomerHeber committed Feb 4, 2024
1 parent ba81502 commit a62f048
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 111 deletions.
43 changes: 33 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.21
- name: Go fmt
Expand All @@ -40,9 +40,9 @@ jobs:

steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: 1.21
id: go
Expand Down Expand Up @@ -83,29 +83,52 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: 1.21
id: go
- name: Get dependencies
run: |
go mod tidy
- name: Install Terraform
uses: hashicorp/setup-terraform@v2
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.2.5
- name: Print Terraform version
run: |
terraform --version
- name: Install Terragrunt
uses: autero1/[email protected].1
uses: autero1/[email protected].2
with:
terragrunt_version: 0.38.5
terragrunt_version: 0.54.22
- name: Print Terragrunt version
run: |
terragrunt --version
- name: Test
run: |
go test -v -run ^TestTerragruntWithCache$
opentofu-integration-tests:
name: OpenTofu Integration Tests
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.21
id: go
- name: Get dependencies
run: |
go mod tidy
- name: Install OpenTofu
uses: opentofu/setup-opentofu@v1
- name: Print OpenTofu version
run: |
tofu --version
- name: Test
run: |
go test -v -run ^TestOpenTofu$
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@
## What?

Terratag is a CLI tool allowing for tags or labels to be applied across an entire set of Terraform files. Terratag will apply tags or labels to any AWS, GCP and Azure resources.
Terratag is a CLI tool allowing for tags or labels to be applied across an entire set of OpenTofu/Terraform files. Terratag will apply tags or labels to any AWS, GCP and Azure resources.

### Terratag in action

![](https://assets.website-files.com/5dc3f52851595b160ba99670/5f62090d2d532ca35e143133_terratag.gif)

## Why?

Maintaining tags across your application is hard, especially when done manually. Terratag enables you to easily add tags to your existing IaC and benefit from some cross-resource tag applications you wish you had thought of when you had just started writing your Terraform, saving you tons of time and making future updates easy. [Read more](https://d1.awsstatic.com/whitepapers/aws-tagging-best-practices.pdf) on why tagging is important.
Maintaining tags across your application is hard, especially when done manually. Terratag enables you to easily add tags to your existing IaC and benefit from some cross-resource tag applications you wish you had thought of when you had just started writing your OpenTofu/Terraform, saving you tons of time and making future updates easy. [Read more](https://d1.awsstatic.com/whitepapers/aws-tagging-best-practices.pdf) on why tagging is important.

## How?

### Prerequisites

- Terraform 0.12 through 1.x
- OpenTofu 1.x or Terraform 0.12 through 1.x.

### Usage

Expand All @@ -35,7 +35,11 @@ Maintaining tags across your application is hard, especially when done manually.

Or download the latest [release binary](https://github.com/env0/terratag/releases) .

1. Initialize Terraform modules to get provider schema and pull child modules:
1. Initialize Opentofu/Terraform modules to get provider schema and pull child modules:
```bash
tofu init
```

```bash
terraform init
```
Expand All @@ -51,14 +55,6 @@ Maintaining tags across your application is hard, especially when done manually.
terratag -dir=foo/bar -tags="environment_id=prod,some-tag=value"
```

Terratag supports the following arguments:

- `-dir` - optional, the directory to recursively search for any `.tf` file and try to terratag it.
- `-tags` - tags, as valid JSON (NOT HCL) or a comma seperated list of key=value.
- `-skipTerratagFiles` - optional. Default to `true`. Skips any previously tagged - (files with `terratag.tf` suffix)
- `-filter` - optional. Only apply tags to the selected resource types (regex)
- `-skip` - optional. Skip applying tags to the selected resource types (regex)

### Example Output

#### Before Terratag
Expand Down Expand Up @@ -168,12 +164,13 @@ locals {

### Optional CLI flags

- `-dir=<path>` - defaults to `.`. Sets the terraform folder to tag `.tf` files in
- `-dir=<path>` - defaults to `.`. Sets the opentofu/terraform folder to tag `.tf` files in
- `-skipTerratagFiles=false` - Dont skip processing `*.terratag.tf` files (when running terratag a second time for the same directory)
- `-verbose=true` - Turn on verbose logging
- `-rename=false` - Instead of replacing files named `<basename>.tf` with `<basename>.terratag.tf`, keep the original filename
- `-filter=<regular expression>` - defaults to `.*`. Only apply tags to the resource types matched by the regular expression
- `-type=<terraform or terragrunt>` - defaults to `terraform`. If `terragrunt` is used, tags the files under `.terragrunt-cache` folder. Note: if Terragrunt does not create a `.terragrunt-cache` folder, use the default or omit.
- `-type=<terraform or terragrunt>` - defaults to `terraform` (and `opentofu`). If `terragrunt` is used, tags the files under `.terragrunt-cache` folder. Note: if Terragrunt does not create a `.terragrunt-cache` folder, use the default or omit.
- `-verbose` - Turn on verbose logging
- `-default-to-terraform` By default uses OpenTofu (if installed), if set will use Terraform even when Opentofu is installed

Setting options via enviroment variables is also supported. CLI flags have a precedence over envrionment variables.

Expand All @@ -186,6 +183,7 @@ TERRATAG_SKIP
TERRATAG_VERBOSE
TERRATAG_RENAME
TERRATAG_TYPE
TERRATAG_DEFAULT_TO_TERRAFORM
```

##### See more samples [here](https://github.com/env0/terratag/tree/master/test/fixture)
Expand Down
2 changes: 2 additions & 0 deletions cli/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Args struct {
Verbose bool
Rename bool
Version bool
DefaultToTerraform bool
}

func validate(args Args) error {
Expand Down Expand Up @@ -48,6 +49,7 @@ func InitArgs() (Args, error) {
fs.BoolVar(&args.Rename, "rename", true, "Keep the original filename or replace it with <basename>.terratag.tf")
fs.StringVar(&args.Type, "type", string(common.Terraform), "The IAC type. Valid values: terraform or terragrunt")
fs.BoolVar(&args.Version, "version", false, "Prints the version")
fs.BoolVar(&args.DefaultToTerraform, "default-to-terraform", false, "By default uses OpenTofu (if installed), if set will use Terraform even when Opentofu is installed")

// Set cli args based on environment variables.
//The command line flags have precedence over environment variables.
Expand Down
2 changes: 1 addition & 1 deletion internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ type TaggingArgs struct {
Matches []string
IsSkipTerratagFiles bool
Rename bool
DefaultToTerraform bool
IACType IACType
TFVersion Version
}

type TerratagLocal struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/zclconf/go-cty/cty"
)

func GetExistingTagsExpression(tokens hclwrite.Tokens, tfVersion common.Version) string {
func GetExistingTagsExpression(tokens hclwrite.Tokens) string {
return stringifyExpression(tokens)
}

Expand Down
17 changes: 6 additions & 11 deletions internal/tagging/tagging.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,10 @@ func TagBlock(args TagBlockArgs) (string, error) {

if hasExistingTags {
existingTagsKey := tag_keys.GetResourceExistingTagsKey(args.Filename, args.Block)
existingTagsExpression := convert.GetExistingTagsExpression(args.Terratag.Found[existingTagsKey], args.TfVersion)
existingTagsExpression := convert.GetExistingTagsExpression(args.Terratag.Found[existingTagsKey])
newTagsValue = "merge( " + existingTagsExpression + ", " + terratagAddedKey + ")"
}

if args.TfVersion.Major == 0 && args.TfVersion.Minor == 11 {
newTagsValue = "\"${" + newTagsValue + "}\""
}

newTagsValueTokens := ParseHclValueStringToTokens(newTagsValue)
args.Block.Body().SetAttributeRaw(args.TagId, newTagsValueTokens)

Expand Down Expand Up @@ -78,12 +74,11 @@ var resourceTypeToFnMap = map[string]TagResourceFn{
}

type TagBlockArgs struct {
Filename string
Block *hclwrite.Block
Tags string
Terratag common.TerratagLocal
TagId string
TfVersion common.Version
Filename string
Block *hclwrite.Block
Tags string
Terratag common.TerratagLocal
TagId string
}

type TagResourceFn func(args TagBlockArgs) (*Result, error)
Expand Down
54 changes: 0 additions & 54 deletions internal/terraform/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ package terraform

import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"

"github.com/bmatcuk/doublestar"
Expand All @@ -20,56 +16,6 @@ import (
"github.com/thoas/go-funk"
)

func GetTerraformVersion() (*common.Version, error) {
output, err := exec.Command("terraform", "version").Output()
if err != nil {
return nil, err
}

outputAsString := strings.TrimSpace(string(output))
regularExpression := regexp.MustCompile(`Terraform v(\d+).(\d+)\.\d+`)
matches := regularExpression.FindStringSubmatch(outputAsString)[1:]

if matches == nil {
return nil, errors.New("unable to parse 'terraform version'")
}

majorVersion, err := getVersionPart(matches, Major)
if err != nil {
return nil, err
}
minorVersion, err := getVersionPart(matches, Minor)
if err != nil {
return nil, err
}

if (majorVersion == 0 && minorVersion < 11 || minorVersion > 15) || majorVersion > 1 {
return nil, fmt.Errorf("terratag only supports Terraform from version 0.11.x and up to 1.x.x - your version says %s", outputAsString)
}

return &common.Version{Major: majorVersion, Minor: minorVersion}, nil
}

type VersionPart int

const (
Major VersionPart = iota
Minor
)

func (w VersionPart) EnumIndex() int {
return int(w)
}

func getVersionPart(parts []string, versionPart VersionPart) (int, error) {
version, err := strconv.Atoi(parts[versionPart])
if err != nil {
return -1, fmt.Errorf("unable to parse %s as integer", parts[versionPart])
}

return version, nil
}

func GetResourceType(resource hclwrite.Block) string {
return resource.Labels()[0]
}
Expand Down
16 changes: 11 additions & 5 deletions internal/tfschema/tfschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ type ProviderSchemas struct {
ProviderSchemas map[string]*ProviderSchema `json:"provider_schemas"`
}

func IsTaggable(dir string, iacType common.IACType, resource hclwrite.Block) (bool, error) {
func IsTaggable(dir string, iacType common.IACType, defaultToTerraform bool, resource hclwrite.Block) (bool, error) {
var isTaggable bool
resourceType := terraform.GetResourceType(resource)

if providers.IsSupportedResource(resourceType) {
resourceSchema, err := getResourceSchema(resourceType, resource, dir, iacType)
resourceSchema, err := getResourceSchema(resourceType, resource, dir, iacType, defaultToTerraform)
if err != nil {
if err == ErrResourceTypeNotFound {
log.Print("[WARN] Skipped ", resourceType, " as it is not YET supported")
Expand Down Expand Up @@ -132,7 +132,7 @@ func detectProviderName(resource hclwrite.Block) (string, error) {
return extractProviderNameFromResourceType(terraform.GetResourceType(resource))
}

func getResourceSchema(resourceType string, resource hclwrite.Block, dir string, iacType common.IACType) (*ResourceSchema, error) {
func getResourceSchema(resourceType string, resource hclwrite.Block, dir string, iacType common.IACType, defaultToTerraform bool) (*ResourceSchema, error) {
if iacType == common.Terragrunt {
// which mode of terragrunt it is (with or without cache folder).
if _, err := os.Stat(dir + "/.terragrunt-cache"); err == nil {
Expand All @@ -147,12 +147,18 @@ func getResourceSchema(resourceType string, resource hclwrite.Block, dir string,
if !ok {
providerSchemas = &ProviderSchemas{}

cmd := exec.Command("terraform", "providers", "schema", "-json")
// Use tofu by default (if it exists).
name := "terraform"
if _, err := exec.LookPath("tofu"); !defaultToTerraform && err == nil {
name = "tofu"
}

cmd := exec.Command(name, "providers", "schema", "-json")
cmd.Dir = dir

out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to execute 'terraform providers schema -json' command: %w", err)
return nil, fmt.Errorf("failed to execute '%s providers schema -json' command: %w", name, err)
}

// Output can vary between operating systems. Get the correct output line.
Expand Down
Loading

0 comments on commit a62f048

Please sign in to comment.