Skip to content

Commit

Permalink
Merge pull request #32 from ivanilves/GH-23
Browse files Browse the repository at this point in the history
Add plain Terraform support via "plugable" IncludeFn()
  • Loading branch information
ivanilves committed Nov 15, 2022
2 parents 4724eda + 12d39f7 commit 8f95996
Show file tree
Hide file tree
Showing 20 changed files with 275 additions and 23 deletions.
1 change: 1 addition & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ changelog:
- '^test'
- '^style'
- '^docs'
- '^Merge pull request'
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
APP_NAME := travelgrunt
API_VERSION := 0.2
API_VERSION := 0.3
BUILD_PATH := ./cmd/${APP_NAME}

CURRENT_PATCH = $(shell (git fetch --tags && git tag --sort=creatordate | grep -F "v${API_VERSION}." || echo -1) | tail -n1 | sed -r "s/^v([0-9]+\.){2}//")
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,34 @@

# travelgrunt

Travel **[Terragrunt](https://terragrunt.gruntwork.io/)** directory tree as a first class passenger! :airplane:
Travel **[Terragrunt](https://terragrunt.gruntwork.io/)** or **[Terraform](https://www.terraform.io/)** directory tree as a first class passenger! :airplane:

## How to use?

* `cd` to the directory of your [locally cloned] Terragrunt/Terraform Git repo;
* run **tg** [alias](#shell-aliases) there :rocket: ([optional] arguments are "path filter" matches);
* use arrow keys to navigate the list and `/` key to search for specific items;

## Configuration
:bulb: `travelgrunt` doesn't need a configuration file, but can take advantage from having one :ok_hand:

Create `.travelgrunt.yml` file in the root path of your repository. Set it content to either:

```
mode: terragrunt
```
:arrow_up: this will follow the **default behavior** to travel across Terragrunt projects (you don't even need a config for this!).

```
mode: terraform
```
:arrow_up: this will navigate through Terraform projects/modules instead of Terragrunt ones. Use case: Terraform module [mono]repo.

```
mode: terraform_or_terragrunt
```
:arrow_up: this will navigate through **both** Terraform and Terragrunt projects inside the repo.

## Shell aliases

It is **absolutely required** to use `bash` (or `zsh`) aliases. Start from something like this:
Expand Down
9 changes: 8 additions & 1 deletion cmd/travelgrunt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"os"

"github.com/ivanilves/travelgrunt/pkg/config"
"github.com/ivanilves/travelgrunt/pkg/directory"
"github.com/ivanilves/travelgrunt/pkg/directory/tree"
"github.com/ivanilves/travelgrunt/pkg/file"
Expand Down Expand Up @@ -88,7 +89,13 @@ func main() {
writeFileAndExit(outFile, rootPath)
}

entries, paths, err := directory.Collect(rootPath)
cfg, err := config.NewConfig(rootPath)

if err != nil {
log.Fatalf("failed to load travelgrunt config: %s", err.Error())
}

entries, paths, err := directory.Collect(rootPath, cfg.IncludeFn())

if err != nil {
log.Fatalf("failed to collect Terragrunt project directories: %s", err.Error())
Expand Down
1 change: 1 addition & 0 deletions fixtures/config/include/nothing/foo.bar
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
baz
9 changes: 9 additions & 0 deletions fixtures/config/include/terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "aws_vpc" "default" {
cidr_block = var.vpc_cidr
tags = merge(
local.common-tags,
map(
"Description", "VPC for creating resources",
)
)
}
14 changes: 14 additions & 0 deletions fixtures/config/include/terragrunt/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
terraform {
source = "my-terraform-module"
}

include "root" {
path = find_in_parent_folders()
}

inputs = {
instance_type = "t2.medium"

min_size = 3
max_size = 3
}
1 change: 1 addition & 0 deletions fixtures/config/travelgrunt.yml.illegal
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mode: bogus
2 changes: 2 additions & 0 deletions fixtures/config/travelgrunt.yml.invalid
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
8�HMԧ��:q
{�
1 change: 1 addition & 0 deletions fixtures/config/travelgrunt.yml.terraform
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mode: terraform
1 change: 1 addition & 0 deletions fixtures/config/travelgrunt.yml.terraform_or_terragrunt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mode: terraform_or_terragrunt
1 change: 1 addition & 0 deletions fixtures/config/travelgrunt.yml.terragrunt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mode: terragrunt
70 changes: 70 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package config

import (
"fmt"
"os"

"gopkg.in/yaml.v3"

"github.com/ivanilves/travelgrunt/pkg/config/include"
)

var configFile = ".travelgrunt.yml"

// Config is a travelgrunt repo-level configuration
type Config struct {
Mode string `yaml:"mode"`

IsDefault bool
}

// NewConfig creates new travelgrunt repo-level configuration
func NewConfig(path string) (cfg Config, err error) {
var data []byte

data, err = os.ReadFile(path + "/" + configFile)
// We don't care about config file not being read,
// it's a common case and we just return default config.
if err != nil {
return DefaultConfig(), nil
}
// If we have a file, it should be a correct one though!
if err := yaml.Unmarshal(data, &cfg); err != nil {
return cfg, err
}

return cfg, validate(cfg)
}

func validate(cfg Config) error {
allowedModes := []string{"terragrunt", "terraform", "terraform_or_terragrunt"}

for _, mode := range allowedModes {
if cfg.Mode == mode {
return nil
}
}

return fmt.Errorf("illegal mode: %s", cfg.Mode)
}

// DefaultConfig returns default travelgrunt repo-level configuration
func DefaultConfig() Config {
return Config{Mode: "terragrunt", IsDefault: true}
}

// IncludeFn returns the "include" function used to select relevant directories
func (cfg Config) IncludeFn() (fn func(os.DirEntry) bool) {
switch cfg.Mode {
case "terragrunt":
fn = include.IsTerragrunt
case "terraform":
fn = include.IsTerraform
case "terraform_or_terragrunt":
fn = include.IsTerraformOrTerragrunt
default:
fn = nil
}

return fn
}
58 changes: 58 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package config

import (
"testing"

"github.com/stretchr/testify/assert"

"os"
"reflect"
"runtime"

"github.com/ivanilves/travelgrunt/pkg/config/include"
)

const (
fixturePath = "../../fixtures/config"
)

func getConfig(mode string, isDefault bool) Config {
return Config{Mode: mode, IsDefault: isDefault}
}

func TestNewConfig(t *testing.T) {
assert := assert.New(t)

testCases := map[string]struct {
cfg Config
success bool
includeFn func(os.DirEntry) bool
}{
"travelgrunt.yml.terragrunt": {cfg: getConfig("terragrunt", false), success: true, includeFn: include.IsTerragrunt},
"travelgrunt.yml.terraform": {cfg: getConfig("terraform", false), success: true, includeFn: include.IsTerraform},
"travelgrunt.yml.terraform_or_terragrunt": {cfg: getConfig("terraform_or_terragrunt", false), success: true, includeFn: include.IsTerraformOrTerragrunt},
"travelgrunt.yml.invalid": {cfg: getConfig("", false), success: false, includeFn: nil},
"travelgrunt.yml.illegal": {cfg: getConfig("bogus", false), success: false, includeFn: nil},
"travelgrunt.yml.nonexistent": {cfg: getConfig("terragrunt", true), success: true, includeFn: include.IsTerragrunt},
}

for cfgFile, expected := range testCases {
configFile = cfgFile

cfg, err := NewConfig(fixturePath)

assert.Equal(expected.cfg, cfg)

assert.Equalf(
runtime.FuncForPC(reflect.ValueOf(expected.includeFn).Pointer()).Name(),
runtime.FuncForPC(reflect.ValueOf(cfg.IncludeFn()).Pointer()).Name(),
"got unexpected include function while loading config file: %s", configFile,
)

if expected.success {
assert.Nil(err)
} else {
assert.NotNil(err)
}
}
}
25 changes: 25 additions & 0 deletions pkg/config/include/include.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package include

import (
"os"
"strings"
)

func fileOrSymlink(d os.DirEntry) bool {
return d.Type().IsRegular() || d.Type() == os.ModeSymlink
}

// IsTerragrunt tells us if we operate on Terragrunt config file
func IsTerragrunt(d os.DirEntry) bool {
return fileOrSymlink(d) && d.Name() == "terragrunt.hcl"
}

// IsTerraform tells us if we operate on Terraform file(s)
func IsTerraform(d os.DirEntry) bool {
return fileOrSymlink(d) && strings.HasSuffix(d.Name(), ".tf")
}

// IsTerraformOrTerragrunt tells us if we operate on Terraform or Terragrunt file(s)
func IsTerraformOrTerragrunt(d os.DirEntry) bool {
return IsTerraform(d) || IsTerragrunt(d)
}
47 changes: 47 additions & 0 deletions pkg/config/include/include_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package include

import (
"os"
"path/filepath"

"testing"

"github.com/stretchr/testify/assert"
)

const (
fixturePath = "../../../fixtures/config/include"
)

func TestIncludeFn(t *testing.T) {
assert := assert.New(t)

type testCase struct {
IsTerragrunt bool
IsTerraform bool
IsTerraformOrTerragrunt bool
}

testCases := map[string]testCase{
"../../../fixtures/config/include/terragrunt/terragrunt.hcl": testCase{true, false, true},
"../../../fixtures/config/include/terraform/main.tf": testCase{false, true, true},
"../../../fixtures/config/include/nothing/foo.bar": testCase{false, false, false},
}

err := filepath.WalkDir(fixturePath,
func(path string, d os.DirEntry, err error) error {
assert.Nil(err)

for p, expected := range testCases {
if p == path {
assert.Equal(expected.IsTerragrunt, IsTerragrunt(d))
assert.Equal(expected.IsTerraform, IsTerraform(d))
assert.Equal(expected.IsTerraformOrTerragrunt, IsTerraformOrTerragrunt(d))
}
}

return nil
})

assert.Nil(err)
}
17 changes: 4 additions & 13 deletions pkg/directory/directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@ package directory

import (
"os"
"strings"

"path/filepath"
"strings"
)

func isHidden(d os.DirEntry) bool {
return d.IsDir() && string(d.Name()[0]) == "."
}

func isTerragruntConfig(d os.DirEntry) bool {
return (d.Type().IsRegular() || d.Type() == os.ModeSymlink) && d.Name() == "terragrunt.hcl"
}

// Collect gets a list of directory path entries containing file "terragrunt.hcl"
func Collect(rootPath string) (entries map[string]string, paths []string, err error) {
func Collect(rootPath string, includeFn func(os.DirEntry) bool) (entries map[string]string, paths []string, err error) {
entries = make(map[string]string, 0)
paths = make([]string, 0)

Expand All @@ -30,7 +25,7 @@ func Collect(rootPath string) (entries map[string]string, paths []string, err er
return filepath.SkipDir
}

if isTerragruntConfig(d) {
if includeFn(d) {
abs := filepath.Dir(path)
rel := strings.TrimPrefix(abs, rootPath+"/")

Expand All @@ -41,9 +36,5 @@ func Collect(rootPath string) (entries map[string]string, paths []string, err er
return nil
})

if err != nil {
return nil, nil, err
}

return entries, paths, nil
return entries, paths, err
}
6 changes: 5 additions & 1 deletion pkg/directory/directory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/ivanilves/travelgrunt/pkg/config"
)

const (
Expand All @@ -14,13 +16,15 @@ const (
func TestCollect(t *testing.T) {
assert := assert.New(t)

cfg := config.DefaultConfig()

testCases := map[string]bool{
fixturePath: true,
invalidPath: false,
}

for path, expectedSuccess := range testCases {
entries, paths, err := Collect(path)
entries, paths, err := Collect(path, cfg.IncludeFn())

if expectedSuccess {
assert.Greater(len(entries), 0)
Expand Down

0 comments on commit 8f95996

Please sign in to comment.