Repository: chanzuckerberg/fogg
Pull request #447 (2020-04-02 00:40:07)

[fix] Fix generation of remote backends
### Summary
We should only configure remote_states for sibling components of type terraform.
Furthermore, remote some dead JSON code

### Test Plan
unittests

codecov-io: # [Codecov](https://codecov.io/gh/chanzuckerberg/fogg/pull/447?src=pr&el=h1) Report
> Merging [#447](https://codecov.io/gh/chanzuckerberg/fogg/pull/447?src=pr&el=desc) into [master](https://codecov.io/gh/chanzuckerberg/fogg/commit/2777651b8d6134af7f40700a0e5db829f1ca2761&el=desc) will **increase** coverage by `0.10%`.
> The diff coverage is `50.00%`.

[![Impacted file tree graph](https://codecov.io/gh/chanzuckerberg/fogg/pull/447/graphs/tree.svg?width=650&height=150&src=pr&token=57DhtZ7g2A)](https://codecov.io/gh/chanzuckerberg/fogg/pull/447?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #447      +/-   ##
==========================================
+ Coverage   61.33%   61.43%   +0.10%     
==========================================
  Files          19       19              
  Lines        1906     1893      -13     
==========================================
- Hits         1169     1163       -6     
+ Misses        586      579       -7     
  Partials      151      151              
```


| [Impacted Files](https://codecov.io/gh/chanzuckerberg/fogg/pull/447?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [config/v2/config.go](https://codecov.io/gh/chanzuckerberg/fogg/pull/447/diff?src=pr&el=tree#diff-Y29uZmlnL3YyL2NvbmZpZy5nbw==) | `7.46% <ø> (+0.05%)` | :arrow_up: |
| [util/testing.go](https://codecov.io/gh/chanzuckerberg/fogg/pull/447/diff?src=pr&el=tree#diff-dXRpbC90ZXN0aW5nLmdv) | `0.00% <0.00%> (ø)` | |
| [plan/plan.go](https://codecov.io/gh/chanzuckerberg/fogg/pull/447/diff?src=pr&el=tree#diff-cGxhbi9wbGFuLmdv) | `84.24% <100.00%> (+0.24%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/chanzuckerberg/fogg/pull/447?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/chanzuckerberg/fogg/pull/447?src=pr&el=footer). Last update [2777651...c9b7416](https://codecov.io/gh/chanzuckerberg/fogg/pull/447?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/config/v2/config.go b/config/v2/config.go
index e2226759..5904809d 100644
--- a/config/v2/config.go
+++ b/config/v2/config.go
@@ -2,7 +2,6 @@ package v2
 
 import (
 	"encoding/json"
-	"fmt"
 	"math/rand"
 	"path/filepath"
 	"reflect"
@@ -230,7 +229,6 @@ const (
 func (c *Config) Generate(r *rand.Rand, size int) reflect.Value {
 	// TODO write this to be part of tests https://github.com/shiwano/submarine/blob/5c02c0cfcf05126454568ef9624550eb0d84f86c/server/battle/src/battle/util/util_test.go#L19
 
-	fmt.Println("generate")
 	conf := &Config{}
 
 	const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
diff --git a/plan/plan.go b/plan/plan.go
index 90be2853..95aa1ec1 100644
--- a/plan/plan.go
+++ b/plan/plan.go
@@ -227,7 +227,6 @@ type Component struct {
 
 	Kind              *v2.ComponentKind  `yaml:"kind,omitempty"`
 	ModuleSource      *string            `yaml:"module_source"`
-	OtherComponents   []string           `yaml:"other_components"`
 	ComponentBackends map[string]Backend `yaml:"component_backends"`
 	Global            *Component         `yaml:"global"`
 }
@@ -357,7 +356,6 @@ func (p *Plan) buildGlobal(conf *v2.Config) Component {
 	}
 
 	componentPlan.Component = "global"
-	componentPlan.OtherComponents = []string{}
 	componentPlan.ExtraVars = resolveExtraVars(defaults.ExtraVars, global.ExtraVars)
 	componentPlan.PathToRepoRoot = "../../"
 
@@ -399,7 +397,6 @@ func (p *Plan) buildEnvs(conf *v2.Config) (map[string]Env, error) {
 
 			componentPlan.Env = envName
 			componentPlan.Component = componentName
-			componentPlan.OtherComponents = otherComponentNames(conf.Envs[envName].Components, componentName)
 			componentPlan.ModuleSource = componentConf.ModuleSource
 			componentPlan.PathToRepoRoot = "../../../../"
 
@@ -409,7 +406,13 @@ func (p *Plan) buildEnvs(conf *v2.Config) (map[string]Env, error) {
 		}
 
 		componentBackends := make(map[string]Backend)
+
 		for componentName, component := range envPlan.Components {
+			// FIXME (el): get rid of non-terraform component kinds
+			if component.Kind.GetOrDefault() != v2.ComponentKindTerraform {
+				continue
+			}
+
 			componentBackends[componentName] = component.Backend
 		}
 
@@ -591,20 +594,6 @@ func resolveComponentCommon(commons ...v2.Common) ComponentCommon {
 	}
 }
 
-func otherComponentNames(components map[string]v2.Component, thisComponent string) []string {
-	r := make([]string, 0)
-	for componentName, componentConf := range components {
-		// Only set up remote state for terraform components
-		if componentConf.Kind.GetOrDefault() != v2.ComponentKindTerraform {
-			continue
-		}
-		if componentName != thisComponent {
-			r = append(r, componentName)
-		}
-	}
-	return r
-}
-
 func resolveEKSConfig(def *v2.EKSConfig, override *v2.EKSConfig) *v2.EKSConfig {
 	resolved := &v2.EKSConfig{}
 	if def != nil {


Test patch:
diff --git a/apply/golden_file_test.go b/apply/golden_file_test.go
index 4e11e3ad..ad45011b 100644
--- a/apply/golden_file_test.go
+++ b/apply/golden_file_test.go
@@ -32,6 +32,7 @@ func TestIntegration(t *testing.T) {
 		{"v2_no_aws_provider_yaml"},
 		{"github_actions"},
 		{"circleci"},
+		{"component_kind"},
 	}
 
 	for _, tc := range testCases {
diff --git a/plan/plan_test.go b/plan/plan_test.go
index c61cf0fc..7afa8528 100644
--- a/plan/plan_test.go
+++ b/plan/plan_test.go
@@ -19,15 +19,6 @@ func init() {
 	logrus.SetFormatter(formatter)
 }
 
-// func TestResolveRequired(t *testing.T) {
-// 	resolved := resolveRequired("def", nil)
-// 	r.Equal("def", resolved)
-
-// 	over := "over"
-// 	resolved = resolveRequired("def", &over)
-// 	r.Equal("over", resolved)
-// }
-
 func TestResolveAccounts(t *testing.T) {
 	r := require.New(t)
 	foo, bar := json.Number("123"), json.Number("456")
@@ -151,6 +142,17 @@ func TestRemoteBackendPlan(t *testing.T) {
 	r.Equal(plan.Global.Backend.Kind, BackendKindRemote)
 }
 
+func TestComponentKindNotTerraform(t *testing.T) {
+	r := require.New(t)
+
+	plan := buildPlan(t, "component_kind")
+	backends := plan.Envs["env1"].Components["foo"].ComponentBackends
+
+	// only contains 1 backend, for itself, and not for "bar"
+	r.Len(backends, 1)
+	r.Contains(backends, "foo")
+}
+
 func buildPlan(t *testing.T, testfile string) *Plan {
 	r := require.New(t)
 	b, e := util.TestFile(testfile)
diff --git a/testdata/component_kind/.fogg-version b/testdata/component_kind/.fogg-version
new file mode 100644
index 00000000..2d5cfc01
--- /dev/null
+++ b/testdata/component_kind/.fogg-version
@@ -0,0 +1,1 @@
+undefined+undefined+dirty
\ No newline at end of file
diff --git a/testdata/component_kind/.gitattributes b/testdata/component_kind/.gitattributes
new file mode 100644
index 00000000..ebf21819
--- /dev/null
+++ b/testdata/component_kind/.gitattributes
@@ -0,0 +1,5 @@
+fogg.tf linguist-generated
+Makefile linguist-generated
+.travis.yml linguist-generated
+.circleci/config.yml linguist-generated
+.terraformignore linguist-generated
diff --git a/testdata/component_kind/.gitignore b/testdata/component_kind/.gitignore
new file mode 100644
index 00000000..69565ae8
--- /dev/null
+++ b/testdata/component_kind/.gitignore
@@ -0,0 +1,34 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+# Compiled files
+*.tfstate
+*.tfstate.*.backup
+*.tfstate.backup
+*tfvars
+
+# Module directory
+.terraform/
+
+# Pycharm folder
+.idea
+
+# Editor Swap Files
+*.swp
+*.swo
+*.swn
+*.swm
+*.swl
+*.swk
+
+.fogg
+/terraform.d
+
+.DS_Store
+.vscode
+.envrc
+
+buildevents.plan
+check-plan.output
+
+venv
diff --git a/testdata/component_kind/.terraform.d/plugin-cache/.gitignore b/testdata/component_kind/.terraform.d/plugin-cache/.gitignore
new file mode 100644
index 00000000..504d4d91
--- /dev/null
+++ b/testdata/component_kind/.terraform.d/plugin-cache/.gitignore
@@ -0,0 +1,5 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+*
+!.gitignore
diff --git a/testdata/component_kind/.terraformignore b/testdata/component_kind/.terraformignore
new file mode 100644
index 00000000..92a763ce
--- /dev/null
+++ b/testdata/component_kind/.terraformignore
@@ -0,0 +1,12 @@
+
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+
+.fogg/
+.git/
+.terraform.d/
+terraform/accounts/acct1/.terraform/plugins/
+terraform/envs/env1/foo/.terraform/plugins/
+terraform/global/.terraform/plugins/
+
diff --git a/testdata/component_kind/Makefile b/testdata/component_kind/Makefile
new file mode 100644
index 00000000..bbf4c931
--- /dev/null
+++ b/testdata/component_kind/Makefile
@@ -0,0 +1,146 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+include scripts/common.mk
+
+ENVS=env1
+MODULES=
+ACCOUNTS=acct1
+
+all: check
+
+setup: ## set up working directory by installing dependencies
+	curl -s https://raw.githubusercontent.com/chanzuckerberg/fogg/master/download.sh | bash -s -- -b .fogg/bin vundefined+undefined+dirty
+	.fogg/bin/fogg setup
+.PHONY: setup
+
+lint: lint-terraform ## lint the code in this repo
+.PHONY: lint
+
+lint-terraform: lint-accounts lint-envs lint-modules ## lint the terraform code in this repo
+.PHONY: lint-terraform
+
+lint-accounts:  ## lint the terrarform code in terraform/accounts
+	@for account in $(ACCOUNTS); do \
+		$(MAKE) -C terraform/accounts/$$account lint || exit $$?; \
+	done
+.PHONY: lint-accounts
+
+lint-envs: ## lint the terraform code in terraform/envs
+	@for env in $(ENVS); do \
+		$(MAKE) -C terraform/envs/$$env lint || exit $$?; \
+	done; \
+	$(MAKE) -C terraform/global lint || exit $$?
+.PHONY: lint-envs
+
+lint-modules: ## lint the terraform code in terraform/modules
+	@for module in $(MODULES); do \
+		$(MAKE) -C terraform/modules/$$module lint || exit $$?; \
+	done
+.PHONY: lint-modules
+
+fmt: fmt-accounts fmt-envs fmt-global fmt-modules ## format code in this repo
+.PHONY: fmt
+
+fmt-accounts: ## format code in terraform/accounts
+	@for account in $(ACCOUNTS); do \
+		$(MAKE) -C terraform/accounts/$$account fmt || exit $$?; \
+	done
+.PHONY: fmt-accounts
+
+fmt-envs: ## format the code in teraform/envs
+	@for env in $(ENVS); do \
+		$(MAKE) -C terraform/envs/$$env fmt || exit $$?; \
+	done
+.PHONY: fmt-envs
+
+fmt-global: ## format the code in terraform/global
+	$(MAKE) -C terraform/global fmt || exit $$?
+.PHONY: fmt-global
+
+fmt-modules:  ## format the code in terraform/modules
+	@for module in $(MODULES); do \
+		$(MAKE) -C terraform/modules/$$module fmt || exit $$?; \
+	done
+.PHONY: fmt-modules
+
+docs: docs-envs docs-global docs-modules ## update docs
+.PHONY: docs
+
+docs-accounts: ## update docs in terraform/accounts
+	@for account in $(ACCOUNTS); do \
+		$(MAKE) -C terraform/accounts/$$account docs || exit $$?; \
+	done
+.PHONY: docs-accounts
+
+docs-envs: ## update the docs in terraform/envs
+	@for env in $(ENVS); do \
+		$(MAKE) -C terraform/envs/$$env docs || exit $$?; \
+	done
+.PHONY: docs-envs
+
+docs-global: ## update the docs in terraform/global
+	$(MAKE) -C terraform/global docs || exit $$?;
+.PHONY: docs-global
+
+docs-modules: ## update the docs in terraform/modules
+	@for module in $(MODULES); do \
+		$(MAKE) -C terraform/modules/$$module docs || exit $$?; \
+	done
+.PHONY: docs-modules
+
+test:
+.PHONY: test
+
+check: check-plan check-docs ## check all code in the repo
+.PHONY: check
+
+check-plan: check-plan-accounts check-plan-envs check-plan-global ## check terraform plans across all components
+.PHONY: check-plan
+
+check-plan-accounts: ## check terraform plans in terraform/accounts
+	@for account in $(ACCOUNTS); do \
+		$(MAKE) -C terraform/accounts/$$account check-plan || exit $$?; \
+	done
+.PHONY: check-plan-accounts
+
+check-plan-envs: ## check terraform plans in terraform/envs
+	@for env in $(ENVS); do \
+		$(MAKE) -C terraform/envs/$$env check-plan || exit $$?; \
+	done
+.PHONY: check-plan-envs
+
+check-plan-global: ## check terraform plans in terraform/global
+	$(MAKE) -C terraform/global check-plan || exit $$?
+.PHONY: check-plan-global
+
+check-docs: ## check terraform docs
+	@for mod in $(MODULES); do \
+		$(MAKE) -C terraform/modules/$$module check-docs || exit $$?; \
+	done
+.PHONY: check-docs
+
+clean: clean-accounts clean-envs clean-global ## clean the repo
+.PHONY: clean
+
+clean-accounts: ## clean in terraform/accounts
+	@for account in $(ACCOUNTS); do \
+		$(MAKE) -C terraform/accounts/$$account clean || exit $$?; \
+	done
+.PHONY: clean-accounts
+
+clean-envs: ## clean in terraform/envs
+	@for env in $(ENVS); do \
+		$(MAKE) -C terraform/envs/$$env clean || exit $$?; \
+	done
+.PHONY: clean-envs
+
+clean-global: ## clean in terraform/global
+	$(MAKE) -C terraform/global clean || exit $$?
+.PHONY: clean-global
+
+
+help: ## display help for this makefile
+	@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+.PHONY: help
+
diff --git a/testdata/component_kind/README.md b/testdata/component_kind/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/component_kind/fogg.yml b/testdata/component_kind/fogg.yml
new file mode 100644
index 00000000..7ca75f83
--- /dev/null
+++ b/testdata/component_kind/fogg.yml
@@ -0,0 +1,18 @@
+version: 2
+defaults:
+  backend:
+    kind: remote
+    host_name: tfe.example.com
+    organization: test-org
+  owner: foo@example.com
+  project: foo
+  providers: {}
+  terraform_version: 1.1.1
+accounts:
+  acct1: {}
+envs:
+  env1:
+    components:
+      foo: {}
+      bar:
+        kind: not_terraform
diff --git a/testdata/component_kind/scripts/common.mk b/testdata/component_kind/scripts/common.mk
new file mode 100644
index 00000000..fd2985b6
--- /dev/null
+++ b/testdata/component_kind/scripts/common.mk
@@ -0,0 +1,31 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+SHELL=/bin/bash -o pipefail
+TF_VARS := $(patsubst %,-e%,$(filter TF_VAR_%,$(.VARIABLES)))
+REPO_ROOT := $(shell git rev-parse --show-toplevel)
+REPO_RELATIVE_PATH := $(shell git rev-parse --show-prefix)
+AUTO_APPROVE := false
+
+TFENV_DIR ?= $(REPO_ROOT)/.fogg/tfenv
+export PATH :=$(TFENV_DIR)/libexec:$(TFENV_DIR)/versions/$(TERRAFORM_VERSION)/:$(REPO_ROOT)/.fogg/bin:$(PATH)
+export TF_PLUGIN_CACHE_DIR=$(REPO_ROOT)/.terraform.d/plugin-cache
+export TF_IN_AUTOMATION=1
+terraform_command ?= $(TFENV_DIR)/versions/$(TERRAFORM_VERSION)/terraform
+
+ifeq ($(TF_BACKEND_KIND),remote)
+	REFRESH := true
+else
+	REFRESH := false
+endif
+
+
+tfenv: ## install the tfenv tool
+	@if [ ! -d ${TFENV_DIR} ]; then \
+		git clone -q https://github.com/chanzuckerberg/tfenv.git $(TFENV_DIR); \
+	fi
+.PHONY: tfenv
+
+terraform: tfenv ## ensure that the proper version of terraform is installed
+	@${TFENV_DIR}/bin/tfenv install $(TERRAFORM_VERSION)
+.PHONY: terraform
diff --git a/testdata/component_kind/scripts/component.mk b/testdata/component_kind/scripts/component.mk
new file mode 100644
index 00000000..04d5e533
--- /dev/null
+++ b/testdata/component_kind/scripts/component.mk
@@ -0,0 +1,127 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+SELF_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
+CHECK_PLANFILE_PATH ?= check-plan.output
+
+include $(SELF_DIR)/common.mk
+
+all:
+.PHONY: all
+
+setup: ## set up local dependencies for this repo
+	$(MAKE) -C $(REPO_ROOT) setup
+.PHONY: setup
+
+check: lint check-plan ## run all checks for this component
+.PHONY: check
+
+fmt: terraform ## format code in this component
+	$(terraform_command) fmt $(TF_ARGS)
+.PHONY: fmt
+
+lint: lint-terraform-fmt lint-tflint ## run all linters for this component
+.PHONY: lint
+
+lint-tflint: ## run the tflint linter for this component
+	@printf "tflint: "
+ifeq ($(TFLINT_ENABLED),1)
+	@tflint || exit $$?;
+else
+	@echo "disabled"
+endif
+.PHONY: lint-tflint
+
+lint-terraform-fmt: terraform ## run `terraform fmt` in check mode
+	@printf "fmt check: "
+	@for f in $(TF); do \
+		printf . \
+		$(terraform_command) fmt $(TF_ARGS) --check=true --diff=true $$f || exit $$? ; \
+	done
+	@echo
+.PHONY: lint-terraform-fmt
+
+check-auth: check-auth-aws check-auth-heroku ## check that authentication is properly set up for this component
+.PHONY: check-auth
+
+check-auth-aws:
+	@for p in $(AWS_BACKEND_PROFILE) $(AWS_PROVIDER_PROFILE); do \
+		aws --profile $$p sts get-caller-identity > /dev/null || (echo "AWS AUTH error. This component is configured to use a profile named '$$p'. Please add one to your ~/.aws/config" && exit -1); \
+	done
+.PHONY: check-auth-aws
+
+check-auth-heroku:
+ifeq ($(HEROKU_PROVIDER),1)
+	@echo "Checking heroku auth..."
+	@if command heroku >/dev/null; then \
+		heroku auth:whoami || (echo "Not authenticated to heroku. For SSO accounts, run 'heroku login', for non-sso accounts set HEROKU_EMAIL and HEROKU_API_KEY" && exit -1); \
+	else \
+		echo "Heroku CLI not installed, can't check auth."; \
+	fi
+endif
+.PHONY: check-auth-heroku
+
+refresh:
+	@if [ $(TF_BACKEND_KIND) != "remote" ]; then \
+		$(terraform_command) refresh $(TF_ARGS); \
+		date +%s > .terraform/refreshed_at; \
+	else \
+		echo "remote backend does not support the refresh command, skipping"; \
+	fi
+.PHONY: refresh
+
+refresh-cached:
+	@last_refresh=`cat .terraform/refreshed_at || echo '0'`; \
+	current_time=`date +%s`; \
+	if (( current_time - last_refresh > 600 )); then \
+		echo "It has been awhile since the last refresh. It is time."; \
+		$(MAKE) refresh; \
+	else \
+		echo "Not time to refresh yet."; \
+	fi;
+.PHONY: refresh-cached
+
+plan: check-auth init fmt refresh-cached ## run a terraform plan
+	$(terraform_command) plan $(TF_ARGS) -refresh=$(REFRESH) -input=false
+.PHONY: plan
+
+apply: check-auth init refresh ## run a terraform apply
+	@$(terraform_command) apply $(TF_ARGS) -refresh=$(REFRESH) -auto-approve=$(AUTO_APPROVE)
+.PHONY: apply
+
+docs:
+	echo
+.PHONY: docs
+
+clean: ## clean modules and plugins for this component
+	-rm -rfv .terraform/modules
+	-rm -rfv .terraform/plugins
+.PHONY: clean
+
+test:
+.PHONY: test
+
+init: terraform check-auth ## run terraform init for this component
+	@$(terraform_command) init -input=false
+.PHONY: init
+
+check-plan: check-auth init refresh-cached ## run a terraform plan and check that it does not fail
+	@$(terraform_command) plan $(TF_ARGS) -detailed-exitcode -lock=false -out=$(CHECK_PLANFILE_PATH) ; \
+	ERR=$$?; \
+	if [ $$ERR -eq 0 ] ; then \
+		echo "Success"; \
+	elif [ $$ERR -eq 1 ] ; then \
+		echo "Error in plan execution."; \
+		exit 1; \
+	elif [ $$ERR -eq 2 ] ; then \
+		echo "Diff";  \
+	fi ; \
+	if [ -n "$(BUILDEVENT_FILE)" ]; then \
+		fogg exp entropy -f $(CHECK_PLANFILE_PATH) -o $(BUILDEVENT_FILE) ; \
+	fi
+	rm $(CHECK_PLANFILE_PATH)
+.PHONY: check-plan
+
+run: check-auth ## run an arbitrary terraform command, CMD. ex `make run CMD='show'`
+	@$(terraform_command) $(CMD)
+.PHONY: run
diff --git a/testdata/component_kind/scripts/failed_output_only b/testdata/component_kind/scripts/failed_output_only
new file mode 100644
index 00000000..a21d3dab
--- /dev/null
+++ b/testdata/component_kind/scripts/failed_output_only
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+t=`mktemp` && \
+exec $@  2>&1 > $t || \
+(
+    a=$?;
+    echo $a;
+    cat $t;
+    rm $t;
+    exit $a
+)
+
+
diff --git a/testdata/component_kind/scripts/module.mk b/testdata/component_kind/scripts/module.mk
new file mode 100644
index 00000000..d8353c0d
--- /dev/null
+++ b/testdata/component_kind/scripts/module.mk
@@ -0,0 +1,49 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+SELF_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
+
+include $(SELF_DIR)/common.mk
+
+all: fmt lint doc
+.PHONY: all
+
+fmt: terraform ## run terraform fmt on this module
+	@printf "fmt: ";
+	@for f in $(TF); do printf .; $(terraform_command) fmt $(TF_ARGS) $$f; done
+	@echo
+.PHONY: fmt
+
+check: lint check-docs ## run all checks on this module
+.PHONY: check
+
+lint: lint-tf ## run all linters on this module
+.PHONY: lint
+
+lint-tf: terraform ## run terraform linters on this module
+	@printf "fmt: ";
+	@for f in $(TF); do \
+	  printf .; $(terraform_command) fmt $(TF_ARGS) --check=true --diff=true || exit $$? ; \
+	done
+	@echo
+.PHONY: lint-tf
+
+readme: ## update this module's README.md 
+	bash $(REPO_ROOT)/scripts/update-readme.sh update
+.PHONY: readme
+
+docs: readme ## update all docs for this module
+.PHONY: docs
+
+check-docs: ## check that this module's docs are up-to-date
+	@bash $(REPO_ROOT)/scripts/update-readme.sh check; \
+	if [ ! $$? -eq 0 ];  then \
+		echo "Docs are out of date, run \`make docs\`"; \
+	fi
+.PHONY: check-docs
+
+clean:
+.PHONY: clean
+
+test:
+.PHONY: test
diff --git a/testdata/component_kind/scripts/update-readme.sh b/testdata/component_kind/scripts/update-readme.sh
new file mode 100644
index 00000000..abe8fb47
--- /dev/null
+++ b/testdata/component_kind/scripts/update-readme.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+# I would have written this directly in the Makefile, but that was difficult.
+
+CMD="$1"
+
+TMP=`mktemp`
+TMP2=`mktemp`
+terraform-docs md . > "$TMP"
+sed '/^<!-- START -->$/,/<!-- END -->/{//!d;}' README.md | sed "/^<!-- START -->$/r $TMP" > $TMP2
+
+case "$CMD" in
+    update)
+        mv $TMP2 README.md
+    ;;
+    check)
+        diff $TMP2 README.md >/dev/null
+    ;;
+esac
+
+exit $?
diff --git a/testdata/component_kind/terraform/accounts/acct1/Makefile b/testdata/component_kind/terraform/accounts/acct1/Makefile
new file mode 100644
index 00000000..dec02ad3
--- /dev/null
+++ b/testdata/component_kind/terraform/accounts/acct1/Makefile
@@ -0,0 +1,18 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+export TERRAFORM_VERSION := 1.1.1
+export TFLINT_ENABLED := 0
+export TF_PLUGIN_CACHE_DIR := ../../..//.terraform.d/plugin-cache
+export TF_BACKEND_KIND := remote
+
+
+
+
+include ../../..//scripts/component.mk
+
+
+help: ## display help for this makefile
+	@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+.PHONY: help
+
diff --git a/testdata/component_kind/terraform/accounts/acct1/fogg.tf b/testdata/component_kind/terraform/accounts/acct1/fogg.tf
new file mode 100644
index 00000000..38a31b31
--- /dev/null
+++ b/testdata/component_kind/terraform/accounts/acct1/fogg.tf
@@ -0,0 +1,102 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+terraform {
+  required_version = "=1.1.1"
+
+  backend remote {
+
+    hostname     = "tfe.example.com"
+    organization = "test-org"
+    workspaces {
+      name = "accounts-acct1"
+    }
+
+  }
+
+}
+
+variable project {
+  type    = string
+  default = "foo"
+}
+
+
+
+
+
+variable owner {
+  type    = string
+  default = "foo@example.com"
+}
+
+# map of aws_accounts
+variable aws_accounts {
+  type = map
+  default = {
+
+
+
+  }
+}
+
+
+
+data terraform_remote_state global {
+  backend = "remote"
+
+  config = {
+
+
+    hostname     = "tfe.example.com"
+    organization = "test-org"
+    workspaces = {
+      name = "global"
+    }
+
+
+  }
+}
+
+
+
+
+
+
+provider random {
+  version = "~> 2.2"
+}
+
+provider template {
+  version = "~> 2.1"
+}
+
+provider archive {
+  version = "~> 1.3"
+}
+
+provider null {
+  version = "~> 2.1"
+}
+
+provider local {
+  version = "~> 1.4"
+}
+
+provider tls {
+  version = "~> 2.1"
+}
diff --git a/testdata/component_kind/terraform/accounts/acct1/terraform.d b/testdata/component_kind/terraform/accounts/acct1/terraform.d
new file mode 120000
index 00000000..29617d28
--- /dev/null
+++ b/testdata/component_kind/terraform/accounts/acct1/terraform.d
@@ -0,0 +1,1 @@
+../../../terraform.d
\ No newline at end of file
diff --git a/testdata/component_kind/terraform/envs/env1/Makefile b/testdata/component_kind/terraform/envs/env1/Makefile
new file mode 100644
index 00000000..398224a7
--- /dev/null
+++ b/testdata/component_kind/terraform/envs/env1/Makefile
@@ -0,0 +1,51 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+COMPONENTS=bar foo
+
+all:
+.PHONY: all
+
+lint: ## lint all components in the env
+	@for c in $(COMPONENTS); do \
+		echo $$c; \
+		$(MAKE) -C $$c lint || exit $$? ; \
+	done
+.PHONY: lint
+
+fmt: ## format terraform code in all components in this env
+	@for c in $(COMPONENTS); do \
+		echo $$c; \
+		$(MAKE) -C $$c fmt || exit $$? ; \
+	done
+.PHONY: fmt
+
+check-plan: ## check all plans in this environment
+	@for c in $(COMPONENTS); do \
+		echo $$c; \
+		$(MAKE) -C $$c check-plan || exit $$? ; \
+	done
+.PHONY: check-plan
+
+docs: ## generate docs for all components in this env
+	@for c in $(COMPONENTS); do \
+		echo $$c; \
+		$(MAKE) -C $$c docs || exit $$? ; \
+	done
+.PHONY: docs
+
+clean: ## clean all components in this env
+	@for c in $(COMPONENTS); do \
+		echo $$c; \
+		$(MAKE) -C $$c clean || exit $$? ; \
+	done
+.PHONY: clean
+
+test:
+.PHONY: test
+
+
+help: ## display help for this makefile
+	@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+.PHONY: help
+
diff --git a/testdata/component_kind/terraform/envs/env1/README.md b/testdata/component_kind/terraform/envs/env1/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/component_kind/terraform/envs/env1/bar/apply.go b/testdata/component_kind/terraform/envs/env1/bar/apply.go
new file mode 100644
index 00000000..4059fa50
--- /dev/null
+++ b/testdata/component_kind/terraform/envs/env1/bar/apply.go
@@ -0,0 +1,465 @@
+package apply
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/url"
+	"os"
+	"path/filepath"
+	"regexp"
+	"sort"
+	"strings"
+
+	v2 "github.com/chanzuckerberg/fogg/config/v2"
+	"github.com/chanzuckerberg/fogg/errs"
+	"github.com/chanzuckerberg/fogg/plan"
+	"github.com/chanzuckerberg/fogg/templates"
+	"github.com/chanzuckerberg/fogg/util"
+	"github.com/gobuffalo/packr"
+	getter "github.com/hashicorp/go-getter"
+	"github.com/hashicorp/hcl2/hclwrite"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+)
+
+const rootPath = "terraform"
+
+// Apply will run a plan and apply all the changes to the current repo.
+func Apply(fs afero.Fs, conf *v2.Config, tmp *templates.T, upgrade bool) error {
+	if !upgrade {
+		toolVersion, err := util.VersionString()
+		if err != nil {
+			return err
+		}
+		versionChange, repoVersion, _ := checkToolVersions(fs, toolVersion)
+		if versionChange {
+			return errs.NewUserf("fogg version (%s) is different than version currently used to manage repo (%s). To upgrade add --upgrade.", toolVersion, repoVersion)
+		}
+	}
+	p, err := plan.Eval(conf)
+	if err != nil {
+		return errs.WrapUser(err, "unable to evaluate plan")
+	}
+
+	e := applyRepo(fs, p, &tmp.Repo, &tmp.Common)
+	if e != nil {
+		return errs.WrapUser(e, "unable to apply repo")
+	}
+
+	if p.TravisCI.Enabled {
+		e = applyTree(fs, &tmp.TravisCI, &tmp.Common, "", p.TravisCI)
+		if e != nil {
+			return errs.WrapUser(e, "unable to apply travis ci")
+		}
+	}
+
+	if p.CircleCI.Enabled {
+		e = applyTree(fs, &tmp.CircleCI, &tmp.Common, "", p.CircleCI)
+		if e != nil {
+			return errs.WrapUser(e, "unable to apply CircleCI")
+		}
+	}
+
+	if p.GitHubActionsCI.Enabled {
+		e = applyTree(fs, &tmp.GitHubActionsCI, &tmp.Common, ".github", p.GitHubActionsCI)
+		if e != nil {
+			return errs.WrapUser(e, "unable to apply GitHub Actions CI")
+		}
+	}
+
+	e = applyAccounts(fs, p, &tmp.Account, &tmp.Common)
+	if e != nil {
+		return errs.WrapUser(e, "unable to apply accounts")
+	}
+
+	e = applyEnvs(fs, p, &tmp.Env, tmp.Components, &tmp.Common)
+	if e != nil {
+		return errs.WrapUser(e, "unable to apply envs")
+	}
+
+	e = applyGlobal(fs, p.Global, &tmp.Global, &tmp.Common)
+	if e != nil {
+		return errs.WrapUser(e, "unable to apply global")
+	}
+
+	e = applyModules(fs, p.Modules, &tmp.Module, &tmp.Common)
+	return errs.WrapUser(e, "unable to apply modules")
+}
+
+func checkToolVersions(fs afero.Fs, current string) (bool, string, error) {
+	f, e := fs.Open(".fogg-version")
+	if e != nil {
+		return false, "", errs.WrapUser(e, "unable to open .fogg-version file")
+	}
+	reader := io.ReadCloser(f)
+	defer reader.Close()
+
+	b, e := ioutil.ReadAll(reader)
+	if e != nil {
+		return false, "", errs.WrapUser(e, "unable to read .fogg-version file")
+	}
+	repoVersion := strings.TrimSpace(string(b))
+	changed, e := versionIsChanged(repoVersion, current)
+	return changed, repoVersion, e
+}
+
+func versionIsChanged(repo string, tool string) (bool, error) {
+	repoVersion, repoSha, repoDirty := util.ParseVersion(repo)
+	toolVersion, toolSha, toolDirty := util.ParseVersion(tool)
+
+	if repoDirty || toolDirty {
+		return true, nil
+	}
+
+	if (repoSha != "" || toolSha != "") && repoSha != toolSha {
+		return true, nil
+	}
+
+	return toolVersion.NE(repoVersion), nil
+}
+
+func applyRepo(fs afero.Fs, p *plan.Plan, repoTemplates, commonTemplates *packr.Box) error {
+	return applyTree(fs, repoTemplates, commonTemplates, "", p)
+}
+
+func applyGlobal(fs afero.Fs, p plan.Component, repoBox, commonBox *packr.Box) error {
+	logrus.Debug("applying global")
+	path := fmt.Sprintf("%s/global", rootPath)
+	e := fs.MkdirAll(path, 0755)
+	if e != nil {
+		return errs.WrapUserf(e, "unable to make directory %s", path)
+	}
+	return applyTree(fs, repoBox, commonBox, path, p)
+}
+
+func applyAccounts(fs afero.Fs, p *plan.Plan, accountBox, commonBox *packr.Box) (e error) {
+	for account, accountPlan := range p.Accounts {
+		path := fmt.Sprintf("%s/accounts/%s", rootPath, account)
+		e = fs.MkdirAll(path, 0755)
+		if e != nil {
+			return errs.WrapUser(e, "unable to make directories for accounts")
+		}
+		e = applyTree(fs, accountBox, commonBox, path, accountPlan)
+		if e != nil {
+			return errs.WrapUser(e, "unable to apply templates to account")
+		}
+	}
+	return nil
+}
+
+func applyModules(fs afero.Fs, p map[string]plan.Module, moduleBox, commonBox *packr.Box) (e error) {
+	for module, modulePlan := range p {
+		path := fmt.Sprintf("%s/modules/%s", rootPath, module)
+		e = fs.MkdirAll(path, 0755)
+		if e != nil {
+			return errs.WrapUserf(e, "unable to make path %s", path)
+		}
+		e = applyTree(fs, moduleBox, commonBox, path, modulePlan)
+		if e != nil {
+			return errs.WrapUser(e, "unable to apply tree")
+		}
+	}
+	return nil
+}
+
+func applyEnvs(fs afero.Fs, p *plan.Plan, envBox *packr.Box, componentBoxes map[v2.ComponentKind]packr.Box, commonBox *packr.Box) (e error) {
+	logrus.Debug("applying envs")
+	for env, envPlan := range p.Envs {
+		logrus.Debugf("applying %s", env)
+		path := fmt.Sprintf("%s/envs/%s", rootPath, env)
+		e = fs.MkdirAll(path, 0755)
+		if e != nil {
+			return errs.WrapUserf(e, "unable to make directory %s", path)
+		}
+		e := applyTree(fs, envBox, commonBox, path, envPlan)
+		if e != nil {
+			return errs.WrapUser(e, "unable to apply templates to env")
+		}
+		for component, componentPlan := range envPlan.Components {
+			path = fmt.Sprintf("%s/envs/%s/%s", rootPath, env, component)
+			e = fs.MkdirAll(path, 0755)
+			if e != nil {
+				return errs.WrapUser(e, "unable to make directories for component")
+			}
+			componentBox := componentBoxes[componentPlan.Kind.GetOrDefault()]
+			e := applyTree(fs, &componentBox, commonBox, path, componentPlan)
+			if e != nil {
+				return errs.WrapUser(e, "unable to apply templates for component")
+			}
+
+			if componentPlan.ModuleSource != nil {
+				e := applyModuleInvocation(fs, path, *componentPlan.ModuleSource, templates.Templates.ModuleInvocation, commonBox)
+				if e != nil {
+					return errs.WrapUser(e, "unable to apply module invocation")
+				}
+			}
+
+		}
+	}
+	return nil
+}
+
+func applyTree(dest afero.Fs, source *packr.Box, common *packr.Box, targetBasePath string, subst interface{}) (e error) {
+	return source.Walk(func(path string, sourceFile packr.File) error {
+
+		extension := filepath.Ext(path)
+		target := getTargetPath(targetBasePath, path)
+		targetExtension := filepath.Ext(target)
+
+		if extension == ".tmpl" {
+			e = applyTemplate(sourceFile, common, dest, target, subst)
+			if e != nil {
+				return errs.WrapUser(e, "unable to apply template")
+			}
+		} else if extension == ".touch" {
+			e = touchFile(dest, target)
+			if e != nil {
+				return errs.WrapUserf(e, "unable to touch file %s", target)
+			}
+		} else if extension == ".create" {
+			e = createFile(dest, target, sourceFile)
+			if e != nil {
+				return errs.WrapUserf(e, "unable to create file %s", target)
+			}
+		} else if extension == ".rm" {
+			e = os.Remove(target)
+			if e != nil && !os.IsNotExist(e) {
+				return errs.WrapUserf(e, "unable to remove %s", target)
+			}
+			logrus.Infof("%s removed", target)
+		} else if extension == ".ln" {
+
+			linkTargetBytes, err := ioutil.ReadAll(sourceFile)
+			if err != nil {
+				return errs.WrapUserf(err, "could not read source file %#v", sourceFile)
+			}
+
+			linkTarget := string(linkTargetBytes)
+
+			err = linkFile(dest, target, linkTarget)
+
+			if err != nil {
+				return errs.WrapInternal(err, "can't symlink file")
+			}
+		} else {
+			e = afero.WriteReader(dest, target, sourceFile)
+			if e != nil {
+				return errs.WrapUser(e, "unable to copy file")
+			}
+			logrus.Infof("%s copied", target)
+		}
+
+		if targetExtension == ".tf" {
+			e = fmtHcl(dest, target)
+			if e != nil {
+				return errs.WrapUser(e, "unable to format HCL")
+			}
+		}
+		return nil
+	})
+}
+
+func fmtHcl(fs afero.Fs, path string) error {
+	in, e := afero.ReadFile(fs, path)
+	if e != nil {
+		return errs.WrapUserf(e, "unable to read file %s", path)
+	}
+	out := hclwrite.Format(in)
+	return afero.WriteReader(fs, path, bytes.NewReader(out))
+}
+
+func touchFile(dest afero.Fs, path string) error {
+	dir, _ := filepath.Split(path)
+	ospath := filepath.FromSlash(dir)
+	err := dest.MkdirAll(ospath, 0755)
+	if err != nil {
+		return errs.WrapUserf(err, "couldn't create %s directory", dir)
+	}
+
+	_, err = dest.Stat(path)
+	if err != nil { // TODO we might not want to do this for all errors
+		logrus.Infof("%s touched", path)
+		_, err = dest.Create(path)
+		if err != nil {
+			return errs.WrapUser(err, "unable to touch file")
+		}
+	} else {
+		logrus.Infof("%s skipped touch", path)
+	}
+	return nil
+}
+
+func createFile(dest afero.Fs, path string, sourceFile io.Reader) error {
+	_, err := dest.Stat(path)
+	if err != nil { // TODO we might not want to do this for all errors
+		logrus.Infof("%s created", path)
+		err = afero.WriteReader(dest, path, sourceFile)
+		if err != nil {
+			return errs.WrapUser(err, "unable to create file")
+		}
+	} else {
+		logrus.Infof("%s skipped", path)
+	}
+	return nil
+}
+
+func removeExtension(path string) string {
+	return strings.TrimSuffix(path, filepath.Ext(path))
+}
+
+func applyTemplate(sourceFile io.Reader, commonTemplates *packr.Box, dest afero.Fs, path string, overrides interface{}) error {
+	dir, _ := filepath.Split(path)
+	ospath := filepath.FromSlash(dir)
+	err := dest.MkdirAll(ospath, 0775)
+	if err != nil {
+		return errs.WrapUserf(err, "couldn't create %s directory", dir)
+	}
+
+	logrus.Infof("%s templated", path)
+	writer, err := dest.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return errs.WrapUser(err, "unable to open file")
+	}
+	t, e := util.OpenTemplate(path, sourceFile, commonTemplates)
+	if e != nil {
+		return e
+	}
+	return t.Execute(writer, overrides)
+}
+
+// This should really be part of the plan stage, not apply. But going to
+// leave it here for now and re-think it when we make this mechanism
+// general purpose.
+type moduleData struct {
+	ModuleName   string
+	ModuleSource string
+	Variables    []string
+	Outputs      []string
+}
+
+func applyModuleInvocation(fs afero.Fs, path, moduleAddress string, box packr.Box, commonBox *packr.Box) error {
+	e := fs.MkdirAll(path, 0755)
+	if e != nil {
+		return errs.WrapUserf(e, "couldn't create %s directory", path)
+	}
+
+	moduleConfig, e := util.DownloadAndParseModule(fs, moduleAddress)
+	if e != nil {
+		return errs.WrapUser(e, "could not download or parse module")
+	}
+
+	// This should really be part of the plan stage, not apply. But going to
+	// leave it here for now and re-think it when we make this mechanism
+	// general purpose.
+	variables := make([]string, 0)
+	for _, v := range moduleConfig.Variables {
+		variables = append(variables, v.Name)
+	}
+	sort.Strings(variables)
+	outputs := make([]string, 0)
+	for _, o := range moduleConfig.Outputs {
+		outputs = append(outputs, o.Name)
+	}
+	sort.Strings(outputs)
+	moduleName := filepath.Base(moduleAddress)
+	re := regexp.MustCompile(`\?ref=.*`)
+	moduleName = re.ReplaceAllString(moduleName, "")
+
+	moduleAddressForSource, _ := calculateModuleAddressForSource(path, moduleAddress)
+	// MAIN
+	f, e := box.Open("main.tf.tmpl")
+	if e != nil {
+		return errs.WrapUser(e, "could not open template file")
+	}
+	e = applyTemplate(f, commonBox, fs, filepath.Join(path, "main.tf"), &moduleData{moduleName, moduleAddressForSource, variables, outputs})
+	if e != nil {
+		return errs.WrapUser(e, "unable to apply template for main.tf")
+	}
+	e = fmtHcl(fs, filepath.Join(path, "main.tf"))
+	if e != nil {
+		return errs.WrapUser(e, "unable to format main.tf")
+	}
+
+	// OUTPUTS
+	f, e = box.Open("outputs.tf.tmpl")
+	if e != nil {
+		return errs.WrapUser(e, "could not open template file")
+	}
+
+	e = applyTemplate(f, commonBox, fs, filepath.Join(path, "outputs.tf"), &moduleData{moduleName, moduleAddressForSource, variables, outputs})
+	if e != nil {
+		return errs.WrapUser(e, "unable to apply template for outputs.tf")
+	}
+
+	e = fmtHcl(fs, filepath.Join(path, "outputs.tf"))
+	if e != nil {
+		return errs.WrapUser(e, "unable to format outputs.tf")
+	}
+
+	return nil
+}
+
+func calculateModuleAddressForSource(path, moduleAddress string) (string, error) {
+	// For cases where the module is a local path, we need to calculate the
+	// relative path from the component to the module.
+	// The module_source path in the fogg.yml is relative to the repo root.
+	var moduleAddressForSource string
+	// getter will kinda normalize the module address, but it will actually be
+	// wrong for local file paths, so we need to calculate that ourselves below
+	s, e := getter.Detect(moduleAddress, path, getter.Detectors)
+	if e != nil {
+		return "", e
+	}
+	u, e := url.Parse(s)
+	if e != nil || u.Scheme == "file" {
+		// This indicates that we have a local path to the module.
+		// It is possible that this test is unreliable.
+		moduleAddressForSource, e = filepath.Rel(path, moduleAddress)
+		if e != nil {
+			return "", e
+		}
+	} else {
+		moduleAddressForSource = moduleAddress
+	}
+	return moduleAddressForSource, nil
+}
+func getTargetPath(basePath, path string) string {
+	target := filepath.Join(basePath, path)
+	extension := filepath.Ext(path)
+
+	if extension == ".tmpl" || extension == ".touch" || extension == ".create" || extension == ".rm" || extension == ".ln" {
+		target = removeExtension(target)
+	}
+
+	return target
+}
+
+func linkFile(fs afero.Fs, name, target string) error {
+	logrus.Debugf("linking %s to %s", name, target)
+	linker, ok := fs.(afero.Symlinker)
+
+	if !ok {
+		return errs.NewInternal("can't cast to afero.SymLinker")
+	}
+
+	relativePath, err := filepathRel(name, target)
+	logrus.Debugf("relative link %s err %#v", relativePath, err)
+	if err != nil {
+		return err
+	}
+
+	logrus.Debugf("removing link at %s", name)
+	err = fs.Remove(name)
+	logrus.Debugf("error removing file %s (probably ok): %s", name, err)
+
+	_, err = linker.SymlinkIfPossible(relativePath, name)
+	return err
+}
+
+func filepathRel(path, name string) (string, error) {
+	dirs := strings.Count(path, "/")
+	fullPath := fmt.Sprintf("%s/%s", strings.Repeat("../", dirs), name)
+	return filepath.Clean(fullPath), nil
+}
diff --git a/testdata/component_kind/terraform/envs/env1/bar/apply_test.go b/testdata/component_kind/terraform/envs/env1/bar/apply_test.go
new file mode 100644
index 00000000..c07fea9a
--- /dev/null
+++ b/testdata/component_kind/terraform/envs/env1/bar/apply_test.go
@@ -0,0 +1,448 @@
+package apply
+
+import (
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	v2 "github.com/chanzuckerberg/fogg/config/v2"
+	"github.com/chanzuckerberg/fogg/templates"
+	"github.com/chanzuckerberg/fogg/util"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v3"
+)
+
+func init() {
+	logrus.SetLevel(logrus.DebugLevel)
+	formatter := &logrus.TextFormatter{
+		DisableTimestamp: true,
+	}
+	logrus.SetFormatter(formatter)
+}
+
+func randomString(n int) string {
+	var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+
+	b := make([]rune, n)
+	for i := range b {
+		b[i] = letter[rand.Intn(len(letter))]
+	}
+	return string(b)
+}
+
+func getNonExistentDirectoryName() string {
+	nonexistentDir := "noexist-" + randomString(20)
+	for {
+		_, err := os.Stat(nonexistentDir)
+		if os.IsNotExist(err) {
+			return nonexistentDir
+		}
+		nonexistentDir = "noexist-" + randomString(20)
+	}
+}
+
+func TestRemoveExtension(t *testing.T) {
+	r := require.New(t)
+	x := removeExtension("foo")
+	r.Equal("foo", x)
+
+	x = removeExtension("foo.txt")
+	r.Equal("foo", x)
+
+	x = removeExtension("foo.txt.asdf")
+	r.Equal("foo.txt", x)
+
+	x = removeExtension("foo/bar.txt")
+	r.Equal("foo/bar", x)
+}
+
+func TestApplyTemplateBasic(t *testing.T) {
+	r := require.New(t)
+	sourceFile := strings.NewReader("foo")
+	dest, d, err := util.TestFs()
+	r.NoError(err)
+	defer os.Remove(d)
+
+	path := "bar"
+	overrides := struct{ Foo string }{"foo"}
+
+	e := applyTemplate(sourceFile, &templates.Templates.Common, dest, path, overrides)
+	r.Nil(e)
+	f, e := dest.Open("bar")
+	r.Nil(e)
+	i, e := ioutil.ReadAll(f)
+	r.Nil(e)
+	r.Equal("foo", string(i))
+}
+
+func TestApplyTemplateBasicNewDirectory(t *testing.T) {
+	r := require.New(t)
+	sourceFile := strings.NewReader("foo")
+
+	dest, d, err := util.TestFs()
+	r.NoError(err)
+	defer os.RemoveAll(d)
+
+	nonexistentDir := getNonExistentDirectoryName()
+	defer dest.RemoveAll(nonexistentDir) //nolint
+	path := filepath.Join(nonexistentDir, "bar")
+	overrides := struct{ Foo string }{"foo"}
+
+	e := applyTemplate(sourceFile, &templates.Templates.Common, dest, path, overrides)
+	r.Nil(e)
+	f, e := dest.Open(path)
+	r.Nil(e)
+	i, e := ioutil.ReadAll(f)
+	r.Nil(e)
+	r.Equal("foo", string(i))
+}
+
+func TestApplyTemplate(t *testing.T) {
+	r := require.New(t)
+	sourceFile := strings.NewReader("Hello {{.Name}}")
+	dest, d, err := util.TestFs()
+	r.NoError(err)
+	defer os.RemoveAll(d)
+
+	path := "hello"
+	overrides := struct{ Name string }{"World"}
+
+	e := applyTemplate(sourceFile, &templates.Templates.Common, dest, path, overrides)
+	r.Nil(e)
+	f, e := dest.Open("hello")
+	r.Nil(e)
+	i, e := ioutil.ReadAll(f)
+	r.Nil(e)
+	r.Equal("Hello World", string(i))
+}
+
+func TestTouchFile(t *testing.T) {
+	r := require.New(t)
+	fs, d, err := util.TestFs()
+	r.NoError(err)
+	defer os.RemoveAll(d)
+
+	e := touchFile(fs, "foo")
+	r.Nil(e)
+	i, e := readFile(fs, "foo")
+	r.Nil(e)
+	r.Equal("", i)
+
+	fs, d2, err := util.TestFs()
+	r.NoError(err)
+	defer os.RemoveAll(d2)
+
+	err = writeFile(fs, "asdf", "jkl")
+	r.NoError(err)
+
+	i, e = readFile(fs, "asdf")
+	r.Nil(e)
+	r.Equal("jkl", i)
+
+	e = touchFile(fs, "asdf")
+	r.Nil(e)
+	i, e = readFile(fs, "asdf")
+	r.Nil(e)
+	r.Equal("jkl", i)
+}
+
+func TestTouchFileNonExistentDirectory(t *testing.T) {
+	r := require.New(t)
+	dest, d, err := util.TestFs()
+	r.NoError(err)
+	defer os.RemoveAll(d)
+
+	nonexistentDir := getNonExistentDirectoryName()
+	defer dest.RemoveAll(nonexistentDir) //nolint
+	e := touchFile(dest, filepath.Join(nonexistentDir, "foo"))
+	r.Nil(e)
+	i, e := readFile(dest, filepath.Join(nonexistentDir, "foo"))
+	r.Nil(e)
+	r.Equal("", i)
+	r.Nil(e)
+}
+
+func TestCreateFile(t *testing.T) {
+	r := require.New(t)
+	fs, d, err := util.TestFs()
+	r.NoError(err)
+	defer os.RemoveAll(d)
+
+	// create new file
+
+	e := createFile(fs, "foo", strings.NewReader("bar"))
+	r.Nil(e)
+
+	i, e := readFile(fs, "foo")
+	r.Nil(e)
+	r.Equal("bar", i)
+
+	// not overwrite existing file
+
+	fs, d2, err := util.TestFs()
+	r.NoError(err)
+	defer os.RemoveAll(d2)
+
+	e = createFile(fs, "foo", strings.NewReader("bar"))
+	r.Nil(e)
+
+	i, e = readFile(fs, "foo")
+	r.Nil(e)
+	r.Equal("bar", i)
+
+	e = createFile(fs, "foo", strings.NewReader("BAM"))
+	r.Nil(e)
+
+	i, e = readFile(fs, "foo")
+	r.Nil(e)
+	r.Equal("bar", i)
+}
+
+func TestCreateFileNonExistentDirectory(t *testing.T) {
+	r := require.New(t)
+	// create new file in nonexistent directory
+	dest, _, e := util.TestFs()
+	r.NoError(e)
+
+	e = createFile(dest, "newdir/foo", strings.NewReader("bar"))
+	r.NoError(e)
+
+	i, e := readFile(dest, "newdir/foo")
+	r.NoError(e)
+	r.Equal("bar", i)
+}
+
+func TestApplySmokeTest(t *testing.T) {
+	r := require.New(t)
+	fs, _, err := util.TestFs()
+	r.NoError(err)
+
+	yml := `
+defaults:
+  owner: foo
+  project: bar
+  terraform_version: 0
+  backend:
+    bucket: baz
+    region: qux
+    profile: quux
+version: 2
+`
+	b, e := yaml.Marshal(yml)
+	r.NoError(e)
+	e = afero.WriteFile(fs, "fogg.yml", b, 0644)
+	r.NoError(e)
+	c, e := v2.ReadConfig(fs, []byte(yml), "fogg.yml")
+	r.NoError(e)
+
+	w, e := c.Validate()
+	r.NoError(e)
+	r.Len(w, 0)
+
+	e = Apply(fs, c, templates.Templates, false)
+	r.NoError(e)
+}
+
+func TestApplyModuleInvocation(t *testing.T) {
+	r := require.New(t)
+	testFs, d, err := util.TestFs()
+	r.NoError(err)
+	defer os.RemoveAll(d)
+	pwdFs, err := util.PwdFs()
+	r.NoError(err)
+
+	fs := afero.NewCopyOnWriteFs(pwdFs, testFs)
+
+	e := applyModuleInvocation(fs, "mymodule", "test-module", templates.Templates.ModuleInvocation, &templates.Templates.Common)
+	r.NoError(e)
+
+	s, e := fs.Stat("mymodule")
+	r.Nil(e)
+	r.True(s.IsDir())
+
+	_, e = fs.Stat("mymodule/main.tf")
+	r.Nil(e)
+	i, e := afero.ReadFile(fs, "mymodule/main.tf")
+	r.Nil(e)
+	expected := "# Auto-generated by fogg. Do not edit\n# Make improvements in fogg, so that everyone can benefit.\n\nmodule test-module {\n  source = \"../test-module\"\n  bar    = local.bar\n  foo    = local.foo\n\n}\n"
+	r.Equal(expected, string(i))
+
+	_, e = fs.Stat("mymodule/outputs.tf")
+	r.Nil(e)
+	i, e = afero.ReadFile(fs, "mymodule/outputs.tf")
+	r.Nil(e)
+	expected = "# Auto-generated by fogg. Do not edit\n# Make improvements in fogg, so that everyone can benefit.\n\noutput bar {\n  value = module.test-module.bar\n}\n\noutput foo {\n  value = module.test-module.foo\n}\n\n\n"
+	r.Equal(expected, string(i))
+}
+func TestGetTargetPath(t *testing.T) {
+	data := []struct {
+		base   string
+		source string
+		output string
+	}{
+		{"", "foo.tmpl", "foo"},
+		{"", "foo.tf.tmpl", "foo.tf"},
+		{"", "fogg.tf", "fogg.tf"},
+		{"", "fogg.tf.tmpl", "fogg.tf"},
+		{"foo", "foo.tmpl", "foo/foo"},
+		{"foo", "foo.tf.tmpl", "foo/foo.tf"},
+		{"foo", "fogg.tf", "foo/fogg.tf"},
+		{"foo", "fogg.tf.tmpl", "foo/fogg.tf"},
+	}
+	for _, test := range data {
+		t.Run(test.source, func(t *testing.T) {
+			r := require.New(t)
+			out := getTargetPath(test.base, test.source)
+			r.Equal(test.output, out)
+		})
+	}
+}
+
+func TestFmtHcl(t *testing.T) {
+	r := require.New(t)
+
+	before := `foo { bar     = "bam"}`
+	after := `foo { bar = "bam" }`
+	fs, d, err := util.TestFs()
+	r.NoError(err)
+	defer os.RemoveAll(d)
+
+	in := strings.NewReader(before)
+	e := afero.WriteReader(fs, "foo.tf", in)
+	r.Nil(e)
+	e = fmtHcl(fs, "foo.tf")
+	r.Nil(e)
+	out, e := afero.ReadFile(fs, "foo.tf")
+	r.Nil(e)
+	r.NotNil(out)
+	s := string(out)
+	r.Equal(after, s)
+}
+
+func TestCalculateLocalPath(t *testing.T) {
+	data := []struct {
+		path          string
+		moduleAddress string
+		expected      string
+	}{
+		{"foo/bar", "bam/baz", "../../bam/baz"},
+		{
+			"foo/bar",
+			"github.com/terraform-aws-modules/terraform-aws-vpc?ref=v1.30.0",
+			"github.com/terraform-aws-modules/terraform-aws-vpc?ref=v1.30.0",
+		},
+		{"foo/bar", "github.com/asdf/jkl", "github.com/asdf/jkl"},
+		// TODO modules from the registry don't work because it is
+		// ambigious with file paths need to figure out how terraform
+		// does this internally
+		// {"foo/bar", "from/the/registry", "from/the/registry"},
+	}
+
+	for _, test := range data {
+		t.Run("", func(t *testing.T) {
+			r := require.New(t)
+			p, e := calculateModuleAddressForSource(test.path, test.moduleAddress)
+			r.Nil(e)
+			r.Equal(test.expected, p)
+		})
+	}
+}
+
+var versionTests = []struct {
+	current string
+	tool    string
+	result  bool
+}{
+	{"0.0.0", "0.1.0", true},
+	{"0.1.0", "0.0.0", true},
+	{"0.1.0", "0.1.0", false},
+	{"0.1.0", "0.1.0-abcdef", true},
+	{"0.1.0", "0.1.0-abcdef-dirty", true},
+	{"0.1.0-abcdef-dirty", "0.1.0", true},
+	{"0.1.0-abc", "0.1.0-def", true},
+	{"0.1.0-abc", "0.1.0-abc", false},
+	{"\t\n 0.2.0 \n\t", "0.2.0", false},
+}
+
+func TestCheckToolVersions(t *testing.T) {
+	for _, tc := range versionTests {
+		t.Run("", func(t *testing.T) {
+			r := require.New(t)
+			fs, d, err := util.TestFs()
+			r.NoError(err)
+			defer os.RemoveAll(d)
+
+			err = writeFile(fs, ".fogg-version", tc.current)
+			r.NoError(err)
+
+			v, _, e := checkToolVersions(fs, tc.tool)
+			r.NoError(e)
+			r.Equal(tc.result, v)
+		})
+	}
+}
+
+func TestVersionIsChanged(t *testing.T) {
+	r := require.New(t)
+
+	for _, test := range versionTests {
+		t.Run("", func(t *testing.T) {
+			b, e := versionIsChanged(test.current, test.tool)
+			r.NoError(e)
+			r.Equal(test.result, b)
+		})
+	}
+}
+
+func readFile(fs afero.Fs, path string) (string, error) {
+	f, e := fs.Open(path)
+	if e != nil {
+		return "", e
+	}
+	r, e := ioutil.ReadAll(f)
+	if e != nil {
+		return "", e
+	}
+	return string(r), nil
+}
+
+func writeFile(fs afero.Fs, path string, contents string) error {
+	f, e := fs.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
+	if e != nil {
+		return e
+	}
+	_, e = f.WriteString(contents)
+	return e
+}
+
+func Test_filepathRel(t *testing.T) {
+	type args struct {
+		name string
+		path string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr bool
+	}{
+		{"terraform.d", args{"terraform/accounts/idseq/terraform.d", "terraform.d"}, "../../../terraform.d", false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := filepathRel(tt.args.name, tt.args.path)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("filepathRel() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("filepathRel() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/testdata/component_kind/terraform/envs/env1/bar/golden_file_test.go b/testdata/component_kind/terraform/envs/env1/bar/golden_file_test.go
new file mode 100644
index 00000000..ad45011b
--- /dev/null
+++ b/testdata/component_kind/terraform/envs/env1/bar/golden_file_test.go
@@ -0,0 +1,127 @@
+package apply_test
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/chanzuckerberg/fogg/apply"
+	"github.com/chanzuckerberg/fogg/config"
+	"github.com/chanzuckerberg/fogg/templates"
+	"github.com/chanzuckerberg/fogg/util"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+var updateGoldenFiles = flag.Bool("update", false, "when set, rewrite the golden files")
+
+func TestIntegration(t *testing.T) {
+
+	var testCases = []struct {
+		fileName string
+	}{
+		{"okta_provider_yaml"},
+		{"github_provider_yaml"},
+		{"bless_provider_yaml"},
+		{"snowflake_provider_yaml"},
+		{"v2_full_yaml"},
+		{"v2_minimal_valid_yaml"},
+		{"v2_no_aws_provider_yaml"},
+		{"github_actions"},
+		{"circleci"},
+		{"component_kind"},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.fileName, func(t *testing.T) {
+			r := require.New(t)
+
+			testdataFs := afero.NewBasePathFs(afero.NewOsFs(), filepath.Join(util.ProjectRoot(), "testdata", tc.fileName))
+
+			if *updateGoldenFiles {
+				// delete all files except fogg.yml
+				e := afero.Walk(testdataFs, ".", func(path string, info os.FileInfo, err error) error {
+					if !info.IsDir() && !(path == "fogg.yml") {
+						return testdataFs.Remove(path)
+					}
+					return nil
+				})
+				r.NoError(e)
+
+				conf, e := config.FindAndReadConfig(testdataFs, "fogg.yml")
+				r.NoError(e)
+				fmt.Printf("conf %#v\n", conf)
+				fmt.Println("READ CONFIG")
+
+				w, e := conf.Validate()
+				r.NoError(e)
+				r.Len(w, 0)
+
+				e = apply.Apply(testdataFs, conf, templates.Templates, true)
+				r.NoError(e)
+			} else {
+				fileName := "fogg.yml"
+				fs, _, e := util.TestFs()
+				r.NoError(e)
+
+				// copy fogg.yml into the tmp test dir (so that it doesn't show up as a diff)
+				configContents, e := afero.ReadFile(testdataFs, fileName)
+				if os.IsNotExist(e) { //If the error is related to the file being non-existent
+					fileName = "fogg.yml"
+					configContents, e = afero.ReadFile(testdataFs, fileName)
+				}
+				r.NoError(e)
+
+				configMode, e := testdataFs.Stat(fileName)
+				r.NoError(e)
+				r.NoError(afero.WriteFile(fs, fileName, configContents, configMode.Mode()))
+
+				conf, e := config.FindAndReadConfig(fs, fileName)
+				r.NoError(e)
+				fmt.Printf("conf %#v\n", conf)
+
+				w, e := conf.Validate()
+				r.NoError(e)
+				r.Len(w, 0)
+
+				e = apply.Apply(fs, conf, templates.Templates, true)
+				r.NoError(e)
+
+				r.NoError(afero.Walk(testdataFs, ".", func(path string, info os.FileInfo, err error) error {
+					logrus.Debug("================================================")
+					logrus.Debug(path)
+					if !info.Mode().IsRegular() {
+						logrus.Debug("dir or link")
+					} else {
+						i1, e1 := testdataFs.Stat(path)
+						r.NotNil(i1)
+						r.NoError(e1)
+
+						i2, e2 := fs.Stat(path)
+						r.NoError(e2)
+						r.NotNil(i2)
+
+						logrus.Debugf("i1 size: %d ii2 size %d", i1.Size(), i2.Size())
+						r.Equalf(i1.Size(), i2.Size(), "file size: %s", path)
+						// This (below) doesn't currently work for files created on a mac then tested on linux. :shrug:
+						// r.Equalf(i1.Mode(), i2.Mode(), "file mode: %s, %o vs %o", path, i1.Mode(), i2.Mode())
+
+						f1, e3 := afero.ReadFile(testdataFs, path)
+						r.NoError(e3)
+						f2, e4 := afero.ReadFile(fs, path)
+						r.NoError(e4)
+
+						logrus.Debugf("f1:\n%s\n\n---- ", f1)
+						logrus.Debugf("f2:\n%s\n\n---- ", f2)
+
+						r.Equal(f1, f2, path)
+					}
+					return nil
+				}))
+			}
+		})
+	}
+}
diff --git a/testdata/component_kind/terraform/envs/env1/bar/test-module/outputs.tf b/testdata/component_kind/terraform/envs/env1/bar/test-module/outputs.tf
new file mode 100644
index 00000000..b1630f39
--- /dev/null
+++ b/testdata/component_kind/terraform/envs/env1/bar/test-module/outputs.tf
@@ -0,0 +1,7 @@
+output "foo" {
+  value = "${var.foo}"
+}
+
+output "bar" {
+  value = "${var.bar}"
+}
diff --git a/testdata/component_kind/terraform/envs/env1/bar/test-module/variables.tf b/testdata/component_kind/terraform/envs/env1/bar/test-module/variables.tf
new file mode 100644
index 00000000..6d8ae6a0
--- /dev/null
+++ b/testdata/component_kind/terraform/envs/env1/bar/test-module/variables.tf
@@ -0,0 +1,2 @@
+variable "foo" {}
+variable "bar" {}
diff --git a/testdata/component_kind/terraform/envs/env1/foo/Makefile b/testdata/component_kind/terraform/envs/env1/foo/Makefile
new file mode 100644
index 00000000..bd4de971
--- /dev/null
+++ b/testdata/component_kind/terraform/envs/env1/foo/Makefile
@@ -0,0 +1,17 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+export TERRAFORM_VERSION := 1.1.1
+export TFLINT_ENABLED := 0
+export TF_PLUGIN_CACHE_DIR := ../../../..//.terraform.d/plugin-cache
+
+
+
+
+include ../../../..//scripts/component.mk
+
+
+help: ## display help for this makefile
+	@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+.PHONY: help
+
diff --git a/testdata/component_kind/terraform/envs/env1/foo/README.md b/testdata/component_kind/terraform/envs/env1/foo/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/component_kind/terraform/envs/env1/foo/fogg.tf b/testdata/component_kind/terraform/envs/env1/foo/fogg.tf
new file mode 100644
index 00000000..40945e00
--- /dev/null
+++ b/testdata/component_kind/terraform/envs/env1/foo/fogg.tf
@@ -0,0 +1,144 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+terraform {
+  required_version = "~>1.1.1"
+
+
+  backend remote {
+
+    hostname     = "tfe.example.com"
+    organization = "test-org"
+    workspaces {
+      name = "env1-foo"
+    }
+
+  }
+
+}
+
+variable env {
+  type    = string
+  default = "env1"
+}
+
+variable project {
+  type    = string
+  default = "foo"
+}
+
+
+
+variable component {
+  type    = string
+  default = "foo"
+}
+
+
+
+
+variable owner {
+  type    = string
+  default = "foo@example.com"
+}
+
+variable tags {
+  type = map(string)
+  default = {
+    project   = "foo"
+    env       = "env1"
+    service   = "foo"
+    owner     = "foo@example.com"
+    managedBy = "terraform"
+  }
+}
+
+
+
+data terraform_remote_state global {
+  backend = "remote"
+
+  config = {
+
+
+    hostname     = "tfe.example.com"
+    organization = "test-org"
+    workspaces = {
+      name = "global"
+    }
+
+
+  }
+}
+
+
+
+
+
+
+# remote state for accounts
+
+data terraform_remote_state acct1 {
+  backend = "remote"
+
+  config = {
+
+
+    hostname     = "tfe.example.com"
+    organization = "test-org"
+    workspaces = {
+      name = "accounts-acct1"
+    }
+
+
+  }
+}
+
+
+# map of aws_accounts
+variable aws_accounts {
+  type = map
+  default = {
+
+
+
+  }
+}
+
+provider random {
+  version = "~> 2.2"
+}
+
+provider template {
+  version = "~> 2.1"
+}
+
+provider archive {
+  version = "~> 1.3"
+}
+
+provider null {
+  version = "~> 2.1"
+}
+
+provider local {
+  version = "~> 1.4"
+}
+
+provider tls {
+  version = "~> 2.1"
+}
diff --git a/testdata/component_kind/terraform/envs/env1/foo/main.tf b/testdata/component_kind/terraform/envs/env1/foo/main.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/component_kind/terraform/envs/env1/foo/outputs.tf b/testdata/component_kind/terraform/envs/env1/foo/outputs.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/component_kind/terraform/envs/env1/foo/terraform.d b/testdata/component_kind/terraform/envs/env1/foo/terraform.d
new file mode 120000
index 00000000..b482d75b
--- /dev/null
+++ b/testdata/component_kind/terraform/envs/env1/foo/terraform.d
@@ -0,0 +1,1 @@
+../../../../terraform.d
\ No newline at end of file
diff --git a/testdata/component_kind/terraform/envs/env1/foo/variables.tf b/testdata/component_kind/terraform/envs/env1/foo/variables.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/component_kind/terraform/global/Makefile b/testdata/component_kind/terraform/global/Makefile
new file mode 100644
index 00000000..310136b2
--- /dev/null
+++ b/testdata/component_kind/terraform/global/Makefile
@@ -0,0 +1,18 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+export TERRAFORM_VERSION := 1.1.1
+export TFLINT_ENABLED := 0
+export TF_PLUGIN_CACHE_DIR := ../..//.terraform.d/plugin-cache
+export TF_BACKEND_KIND := remote
+
+
+
+
+include ../..//scripts/component.mk
+
+
+help: ## display help for this makefile
+	@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+.PHONY: help
+
diff --git a/testdata/component_kind/terraform/global/README.md b/testdata/component_kind/terraform/global/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/component_kind/terraform/global/fogg.tf b/testdata/component_kind/terraform/global/fogg.tf
new file mode 100644
index 00000000..3269e3c3
--- /dev/null
+++ b/testdata/component_kind/terraform/global/fogg.tf
@@ -0,0 +1,93 @@
+# Auto-generated by fogg. Do not edit
+# Make improvements in fogg, so that everyone can benefit.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+terraform {
+  required_version = "~>1.1.1"
+
+  backend remote {
+
+    hostname     = "tfe.example.com"
+    organization = "test-org"
+    workspaces {
+      name = "global"
+    }
+
+  }
+}
+
+variable env {
+  type    = string
+  default = ""
+}
+
+variable project {
+  type    = string
+  default = "foo"
+}
+
+
+
+variable component {
+  type    = string
+  default = "global"
+}
+
+
+
+variable owner {
+  type    = string
+  default = "foo@example.com"
+}
+
+variable tags {
+  type = map(string)
+  default = {
+    project   = "foo"
+    env       = ""
+    service   = "global"
+    owner     = "foo@example.com"
+    managedBy = "terraform"
+  }
+}
+
+
+
+
+provider random {
+  version = "~> 2.2"
+}
+
+provider template {
+  version = "~> 2.1"
+}
+
+provider archive {
+  version = "~> 1.3"
+}
+
+provider null {
+  version = "~> 2.1"
+}
+
+provider local {
+  version = "~> 1.4"
+}
+
+provider tls {
+  version = "~> 2.1"
+}
diff --git a/testdata/component_kind/terraform/global/main.tf b/testdata/component_kind/terraform/global/main.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/component_kind/terraform/global/outputs.tf b/testdata/component_kind/terraform/global/outputs.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/component_kind/terraform/global/terraform.d b/testdata/component_kind/terraform/global/terraform.d
new file mode 120000
index 00000000..a1f7d0d3
--- /dev/null
+++ b/testdata/component_kind/terraform/global/terraform.d
@@ -0,0 +1,1 @@
+../../terraform.d
\ No newline at end of file
diff --git a/testdata/component_kind/terraform/global/variables.tf b/testdata/component_kind/terraform/global/variables.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/v2_full_yaml/terraform/envs/staging/comp1/fogg.tf b/testdata/v2_full_yaml/terraform/envs/staging/comp1/fogg.tf
index b3291b65..29451662 100644
--- a/testdata/v2_full_yaml/terraform/envs/staging/comp1/fogg.tf
+++ b/testdata/v2_full_yaml/terraform/envs/staging/comp1/fogg.tf
@@ -136,24 +136,6 @@ data terraform_remote_state comp2 {
 
 
 
-data terraform_remote_state comp_helm_template {
-  backend = "s3"
-
-  config = {
-
-
-    bucket = "buck"
-
-    key     = "terraform/proj/envs/staging/components/comp_helm_template.tfstate"
-    region  = "us-west-2"
-    profile = "profile"
-
-
-  }
-}
-
-
-
 data terraform_remote_state vpc {
   backend = "s3"
 
diff --git a/testdata/v2_full_yaml/terraform/envs/staging/comp2/fogg.tf b/testdata/v2_full_yaml/terraform/envs/staging/comp2/fogg.tf
index a0b8191b..0ab0f3ec 100644
--- a/testdata/v2_full_yaml/terraform/envs/staging/comp2/fogg.tf
+++ b/testdata/v2_full_yaml/terraform/envs/staging/comp2/fogg.tf
@@ -136,24 +136,6 @@ data terraform_remote_state comp1 {
 
 
 
-data terraform_remote_state comp_helm_template {
-  backend = "s3"
-
-  config = {
-
-
-    bucket = "buck"
-
-    key     = "terraform/proj/envs/staging/components/comp_helm_template.tfstate"
-    region  = "us-west-2"
-    profile = "profile"
-
-
-  }
-}
-
-
-
 data terraform_remote_state vpc {
   backend = "s3"
 
diff --git a/testdata/v2_full_yaml/terraform/envs/staging/vpc/fogg.tf b/testdata/v2_full_yaml/terraform/envs/staging/vpc/fogg.tf
index 06a62e95..9fabae38 100644
--- a/testdata/v2_full_yaml/terraform/envs/staging/vpc/fogg.tf
+++ b/testdata/v2_full_yaml/terraform/envs/staging/vpc/fogg.tf
@@ -152,24 +152,6 @@ data terraform_remote_state comp2 {
 
 
 
-data terraform_remote_state comp_helm_template {
-  backend = "s3"
-
-  config = {
-
-
-    bucket = "buck"
-
-    key     = "terraform/proj/envs/staging/components/comp_helm_template.tfstate"
-    region  = "us-west-2"
-    profile = "profile"
-
-
-  }
-}
-
-
-
 
 
 # remote state for accounts
diff --git a/util/testing.go b/util/testing.go
index b4dc715d..b9a4577c 100644
--- a/util/testing.go
+++ b/util/testing.go
@@ -7,7 +7,6 @@ import (
 	"path/filepath"
 	"runtime"
 	"strconv"
-	"strings"
 
 	"github.com/spf13/afero"
 )
@@ -35,14 +34,9 @@ func ProjectRoot() string {
 }
 
 func TestFile(name string) ([]byte, error) {
-	// calculate the path to repository's root
-	if strings.Contains(name, "_yaml") {
-		path := filepath.Join(ProjectRoot(), "testdata", name, "fogg.yml")
-		return ioutil.ReadFile(path)
-	} else {
-		path := filepath.Join(ProjectRoot(), "testdata", name, "fogg.json")
-		return ioutil.ReadFile(path)
-	}
+	// always look for yaml, json deprecated
+	path := filepath.Join(ProjectRoot(), "testdata", name, "fogg.yml")
+	return ioutil.ReadFile(path)
 }
 
 func TestFs() (afero.Fs, string, error) {