Repository: chanzuckerberg/fogg
Pull request #305 (2019-06-22 04:01:46)

[feature] TravisCI now allows per component configuration + allows to set a specific make command to run
### Summary
- Mostly lifted from the Atlantis implementation

### Test Plan
- unittest
- run in existing repo



codecov[bot]: # [Codecov](https://codecov.io/gh/chanzuckerberg/fogg/pull/305?src=pr&el=h1) Report
> Merging [#305](https://codecov.io/gh/chanzuckerberg/fogg/pull/305?src=pr&el=desc) into [master](https://codecov.io/gh/chanzuckerberg/fogg/commit/deb1e0ec26aa90b8317236333d0975134385521a?src=pr&el=desc) will **increase** coverage by `0.54%`.
> The diff coverage is `96.11%`.

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

```diff
@@            Coverage Diff             @@
##           master     #305      +/-   ##
==========================================
+ Coverage   81.38%   81.92%   +0.54%     
==========================================
  Files          18       18              
  Lines        1590     1649      +59     
==========================================
+ Hits         1294     1351      +57     
- Misses        172      175       +3     
+ Partials      124      123       -1
```


| [Impacted Files](https://codecov.io/gh/chanzuckerberg/fogg/pull/305?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [config/v1/config.go](https://codecov.io/gh/chanzuckerberg/fogg/pull/305/diff?src=pr&el=tree#diff-Y29uZmlnL3YxL2NvbmZpZy5nbw==) | `84.61% <ø> (ø)` | :arrow_up: |
| [plan/travisci.go](https://codecov.io/gh/chanzuckerberg/fogg/pull/305/diff?src=pr&el=tree#diff-cGxhbi90cmF2aXNjaS5nbw==) | `100% <100%> (+5.55%)` | :arrow_up: |
| [config/v2/validation.go](https://codecov.io/gh/chanzuckerberg/fogg/pull/305/diff?src=pr&el=tree#diff-Y29uZmlnL3YyL3ZhbGlkYXRpb24uZ28=) | `83.51% <100%> (+0.27%)` | :arrow_up: |
| [config/v2/config.go](https://codecov.io/gh/chanzuckerberg/fogg/pull/305/diff?src=pr&el=tree#diff-Y29uZmlnL3YyL2NvbmZpZy5nbw==) | `100% <100%> (ø)` | :arrow_up: |
| [plan/plan.go](https://codecov.io/gh/chanzuckerberg/fogg/pull/305/diff?src=pr&el=tree#diff-cGxhbi9wbGFuLmdv) | `92.13% <100%> (+0.32%)` | :arrow_up: |
| [config/v2/resolvers.go](https://codecov.io/gh/chanzuckerberg/fogg/pull/305/diff?src=pr&el=tree#diff-Y29uZmlnL3YyL3Jlc29sdmVycy5nbw==) | `94.32% <80.95%> (-1.35%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/chanzuckerberg/fogg/pull/305?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/305?src=pr&el=footer). Last update [deb1e0e...b3b50dc](https://codecov.io/gh/chanzuckerberg/fogg/pull/305?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/config/v1/config.go b/config/v1/config.go
index ad1cbc93..3a6d1d13 100644
--- a/config/v1/config.go
+++ b/config/v1/config.go
@@ -126,9 +126,10 @@ type Module struct {
 }
 
 type TravisCI struct {
-	Enabled        bool   `json:"enabled,omitempty"`
-	AWSIAMRoleName string `json:"aws_iam_role_name"`
-	TestBuckets    int    `json:"test_buckets"`
+	Enabled        *bool   `json:"enabled,omitempty"`
+	AWSIAMRoleName *string `json:"aws_iam_role_name,omitempty"`
+	TestBuckets    *int    `json:"test_buckets,omitempty"`
+	Command        *string `json:"command,omitempty"`
 }
 
 type Config struct {
diff --git a/config/v2/config.go b/config/v2/config.go
index 300f6b8c..2b3a5f02 100644
--- a/config/v2/config.go
+++ b/config/v2/config.go
@@ -148,6 +148,15 @@ func (c *Config) Generate(r *rand.Rand, size int) reflect.Value {
 		return &str
 	}
 
+	randBoolPtr := func(r *rand.Rand, s int) *bool {
+		b := r.Float32() > 0.5
+		return &b
+	}
+	randIntPtr := func(r *rand.Rand, s int) *int {
+		i := r.Intn(s)
+		return &i
+	}
+
 	randStringMap := func(r *rand.Rand, s int) map[string]string {
 		m := map[string]string{}
 
@@ -226,8 +235,8 @@ func (c *Config) Generate(r *rand.Rand, size int) reflect.Value {
 			c.Tools = &Tools{}
 			if r.Float32() < 0.5 {
 				c.Tools.TravisCI = &v1.TravisCI{
-					Enabled:     r.Float32() < 0.5,
-					TestBuckets: r.Intn(size),
+					Enabled:     randBoolPtr(r, s),
+					TestBuckets: randIntPtr(r, s),
 				}
 			}
 			if r.Float32() < 0.5 {
diff --git a/config/v2/resolvers.go b/config/v2/resolvers.go
index 343fde0e..807fd65b 100644
--- a/config/v2/resolvers.go
+++ b/config/v2/resolvers.go
@@ -194,6 +194,28 @@ func ResolveAtlantis(commons ...Common) *Atlantis {
 	}
 }
 
+func ResolveTravis(commons ...Common) *v1.TravisCI {
+	enabled := false
+	testCommand := "check"
+	for _, c := range commons {
+		if c.Tools != nil && c.Tools.TravisCI != nil && c.Tools.TravisCI.Enabled != nil {
+			enabled = *c.Tools.TravisCI.Enabled
+		}
+
+		if c.Tools != nil && c.Tools.TravisCI != nil && c.Tools.TravisCI.Command != nil {
+			testCommand = *c.Tools.TravisCI.Command
+		}
+	}
+
+	roleName := lastNonNil(TravisRoleNameGetter, commons...)
+
+	return &v1.TravisCI{
+		Enabled:        &enabled,
+		AWSIAMRoleName: roleName,
+		Command:        &testCommand,
+	}
+}
+
 func OwnerGetter(comm Common) *string {
 	return comm.Owner
 }
@@ -367,3 +389,17 @@ func AtlantisRoleNameGetter(comm Common) *string {
 	}
 	return comm.Tools.Atlantis.RoleName
 }
+
+func TravisRoleNameGetter(comm Common) *string {
+	if comm.Tools == nil || comm.Tools.TravisCI == nil {
+		return nil
+	}
+	return comm.Tools.TravisCI.AWSIAMRoleName
+}
+
+func TravisTestCommandGetter(comm Common) *string {
+	if comm.Tools == nil || comm.Tools.TravisCI == nil {
+		return nil
+	}
+	return comm.Tools.TravisCI.Command
+}
diff --git a/config/v2/validation.go b/config/v2/validation.go
index 3c4f949f..64035cb4 100644
--- a/config/v2/validation.go
+++ b/config/v2/validation.go
@@ -11,6 +11,11 @@ import (
 	validator "gopkg.in/go-playground/validator.v9"
 )
 
+var validTravisCommands = map[string]struct{}{
+	"check": struct{}{},
+	"lint":  struct{}{},
+}
+
 // Validate validates the config
 func (c *Config) Validate() ([]string, error) {
 	if c == nil {
@@ -49,12 +54,10 @@ func (c *Config) Validate() ([]string, error) {
 	errs = multierror.Append(errs, c.ValidateOktaProviders())
 	errs = multierror.Append(errs, c.validateModules())
 	errs = multierror.Append(errs, c.ValidateAtlantis())
+	errs = multierror.Append(errs, c.ValidateTravis())
 
 	// refactor to make it easier to manage these
-	w, e := c.ValidateToolsTravis()
-	warnings = append(warnings, w...)
-	errs = multierror.Append(errs, e)
-	w, e = c.ValidateToolsTfLint()
+	w, e := c.ValidateToolsTfLint()
 	warnings = append(warnings, w...)
 	errs = multierror.Append(errs, e)
 
@@ -200,17 +203,27 @@ func (c *Config) ValidateBlessProviders() error {
 	return errs
 }
 
-func (c *Config) ValidateToolsTravis() ([]string, error) {
-	var warns []string
+func (c *Config) ValidateTravis() error {
 	var errs *multierror.Error
 	c.WalkComponents(func(component string, comms ...Common) {
-		c := comms[len(comms)-1]
-		if c.Tools != nil && c.Tools.TravisCI != nil {
-			warns = append(warns, fmt.Sprintf("per-component travisci config is not implemented, ignoring config in %s", component))
+		t := ResolveTravis(comms...)
+		if t.Enabled == nil || *t.Enabled == false {
+			return // nothing to do
+		}
+
+		if t.AWSIAMRoleName == nil || *t.AWSIAMRoleName == "" {
+			errs = multierror.Append(errs, fmt.Errorf("if travis is enabled, aws_role_name must be set"))
+		}
+
+		if t.Command != nil {
+			_, ok := validTravisCommands[*t.Command]
+			if !ok {
+				errs = multierror.Append(errs, fmt.Errorf("unrecognized travisci command %s (%s)", *t.Command, component))
+			}
 		}
 	})
 
-	return warns, errs
+	return errs
 }
 
 func (c *Config) ValidateToolsTfLint() ([]string, error) {
diff --git a/plan/plan.go b/plan/plan.go
index 06f67d9b..38451668 100644
--- a/plan/plan.go
+++ b/plan/plan.go
@@ -39,6 +39,7 @@ type ComponentCommon struct {
 	Project   string
 	Providers Providers `yaml:"providers"`
 	TfLint    TfLint
+	TravisCI  TravisComponent
 }
 
 type AtlantisComponent struct {
@@ -47,6 +48,14 @@ type AtlantisComponent struct {
 	RolePath string `yaml:"role_path"`
 }
 
+type TravisComponent struct {
+	Enabled        bool
+	AWSProfileName string
+	AWSRoleName    string
+	AWSAccountID   string
+	Command        string
+}
+
 type Providers struct {
 	AWS       *AWSProvider       `yaml:"aws"`
 	Snowflake *SnowflakeProvider `yaml:"snowflake"`
@@ -153,8 +162,8 @@ func Eval(c *v2.Config) (*Plan, error) {
 	}
 
 	p.Modules = p.buildModules(c)
-	p.TravisCI = p.buildTravisCI(c, v)
 	p.Atlantis = p.buildAtlantis()
+	p.TravisCI = p.buildTravisCI(c, v)
 
 	return p, nil
 }
@@ -323,7 +332,15 @@ func resolveComponentCommon(commons ...v2.Common) ComponentCommon {
 	if atlantisPlan.Enabled {
 		atlantisPlan.RoleName = *atlantisConfig.RoleName
 		atlantisPlan.RolePath = *atlantisConfig.RolePath
+	}
 
+	travisConfig := v2.ResolveTravis(commons...)
+	travisPlan := TravisComponent{
+		Enabled: *travisConfig.Enabled,
+	}
+	if travisPlan.Enabled {
+		travisPlan.AWSRoleName = *travisConfig.AWSIAMRoleName
+		travisPlan.Command = *travisConfig.Command
 	}
 
 	return ComponentCommon{
@@ -346,6 +363,7 @@ func resolveComponentCommon(commons ...v2.Common) ComponentCommon {
 		Owner:     v2.ResolveRequiredString(v2.OwnerGetter, commons...),
 		Project:   v2.ResolveRequiredString(v2.ProjectGetter, commons...),
 		Common:    Common{TerraformVersion: v2.ResolveRequiredString(v2.TerraformVersionGetter, commons...)},
+		TravisCI:  travisPlan,
 	}
 }
 
diff --git a/plan/travisci.go b/plan/travisci.go
index 9aa3c9e5..3e9a0cfb 100644
--- a/plan/travisci.go
+++ b/plan/travisci.go
@@ -1,83 +1,146 @@
 package plan
 
 import (
-	"encoding/json"
-	"path"
+	"fmt"
+	"sort"
 
 	v2 "github.com/chanzuckerberg/fogg/config/v2"
-	"github.com/chanzuckerberg/fogg/util"
 )
 
-type AWSProfile struct {
-	Name string
-	ID   json.Number
-	Role string
+type TravisProject struct {
+	Name    string
+	Dir     string
+	Command string
 }
-
 type TravisCI struct {
-	AWSProfiles []AWSProfile
 	Enabled     bool
 	FoggVersion string
-	TestBuckets [][]string
+	TestBuckets [][]TravisProject
+	AWSProfiles map[string]AWSRole
 }
 
-func (p *Plan) buildTravisCI(c *v2.Config, version string) TravisCI {
-	if p.Accounts == nil {
-		panic("buildTravisCI must be run after buildAccounts")
+// TODO(el): mostly a duplicate of buildAtlantis(). refactor later
+func (p *Plan) buildTravisCI(c *v2.Config, foggVersion string) TravisCI {
+	enabled := false
+	projects := []TravisProject{}
+	awsProfiles := map[string]AWSRole{}
+
+	if p.Global.TravisCI.Enabled {
+		enabled = true
+		proj := TravisProject{
+			Name:    "global",
+			Dir:     "terraform/global",
+			Command: p.Global.TravisCI.Command,
+		}
+		projects = append(projects, proj)
+
+		if p.Global.Backend.AccountID != nil {
+			awsProfiles[p.Global.Backend.Profile] = AWSRole{
+				AccountID: *p.Global.Backend.AccountID,
+				RoleName:  p.Global.TravisCI.AWSRoleName,
+			}
+			if p.Global.Providers.AWS != nil {
+				a := *p.Global.Providers.AWS
+				awsProfiles[a.Profile] = AWSRole{
+					AccountID: *p.Global.Backend.AccountID,
+					RoleName:  p.Global.TravisCI.AWSRoleName,
+				}
+			}
+		}
 	}
 
-	if c.Defaults.Tools == nil || c.Defaults.Tools.TravisCI == nil {
-		return TravisCI{}
+	for name, acct := range p.Accounts {
+		if acct.TravisCI.Enabled {
+			enabled = true
+			proj := TravisProject{
+				Name:    fmt.Sprintf("accounts/%s", name),
+				Dir:     fmt.Sprintf("terraform/accounts/%s", name),
+				Command: acct.TravisCI.Command,
+			}
+
+			projects = append(projects, proj)
+
+			// Grab all profiles from accounts
+			if acct.Backend.AccountID != nil {
+				awsProfiles[acct.Backend.Profile] = AWSRole{
+					AccountID: *acct.Backend.AccountID,
+					RoleName:  acct.TravisCI.AWSRoleName,
+				}
+			}
+			if acct.Providers.AWS != nil {
+				awsProfiles[acct.Providers.AWS.Profile] = AWSRole{
+					AccountID: acct.Providers.AWS.AccountID.String(),
+					RoleName:  acct.TravisCI.AWSRoleName,
+				}
+			}
+		}
 	}
 
-	tr := TravisCI{
-		Enabled: c.Defaults.Tools.TravisCI.Enabled,
+	for envName, env := range p.Envs {
+		for cName, c := range env.Components {
+			if c.TravisCI.Enabled {
+				enabled = true
+				proj := TravisProject{
+					Name:    fmt.Sprintf("%s/%s", envName, cName),
+					Dir:     fmt.Sprintf("terraform/envs/%s/%s", envName, cName),
+					Command: "check",
+				}
+
+				projects = append(projects, proj)
+
+				if c.Backend.AccountID != nil {
+					awsProfiles[c.Backend.Profile] = AWSRole{
+						AccountID: *c.Backend.AccountID,
+						RoleName:  c.TravisCI.AWSRoleName,
+					}
+				}
+
+				if c.Providers.AWS != nil {
+					a := *c.Providers.AWS
+					awsProfiles[a.Profile] = AWSRole{
+						AccountID: a.AccountID.String(),
+						RoleName:  c.TravisCI.AWSRoleName,
+					}
+				}
+			}
+		}
 	}
-	var profiles []AWSProfile
-
-	tr.FoggVersion = version
-
-	for _, name := range util.SortedMapKeys(p.Accounts) {
-		profiles = append(profiles, AWSProfile{
-			Name: name,
-			// TODO since accountID is required here, that means we need
-			// to make it non-optional, either in defaults or post-plan.
-			ID:   p.Accounts[name].Providers.AWS.AccountID,
-			Role: c.Defaults.Tools.TravisCI.AWSIAMRoleName,
-		})
+
+	for moduleName := range p.Modules {
+		proj := TravisProject{
+			Name:    fmt.Sprintf("modules/%s", moduleName),
+			Dir:     fmt.Sprintf("terraform/modules/%s", moduleName),
+			Command: "check",
+		}
+		projects = append(projects, proj)
 	}
-	tr.AWSProfiles = profiles
 
 	var buckets int
-	if c.Defaults.Tools.TravisCI.TestBuckets > 0 {
-		buckets = c.Defaults.Tools.TravisCI.TestBuckets
+	if c.Defaults.Tools != nil &&
+		c.Defaults.Tools.TravisCI != nil &&
+		c.Defaults.Tools.TravisCI.TestBuckets != nil &&
+		*c.Defaults.Tools.TravisCI.TestBuckets > 0 {
+
+		buckets = *c.Defaults.Tools.TravisCI.TestBuckets
 	} else {
 		buckets = 1
 	}
 
-	var testPaths []string
-	testPaths = append(testPaths, path.Join("terraform", "global"))
+	sort.SliceStable(projects, func(i, j int) bool {
+		return projects[i].Name < projects[j].Name
+	})
 
-	for _, name := range util.SortedMapKeys(c.Accounts) {
-		testPaths = append(testPaths, path.Join("terraform", "accounts", name))
-	}
-
-	for _, envName := range util.SortedMapKeys(c.Envs) {
-		for _, name := range util.SortedMapKeys(c.Envs[envName].Components) {
-			testPaths = append(testPaths, path.Join("terraform", "envs", envName, name))
-		}
-	}
-
-	for _, moduleName := range util.SortedMapKeys(c.Modules) {
-		testPaths = append(testPaths, path.Join("terraform", "modules", moduleName))
-	}
-
-	testBuckets := make([][]string, buckets)
-	for i, path := range testPaths {
+	testBuckets := make([][]TravisProject, buckets)
+	for i, proj := range projects {
 		bucket := i % buckets
-		testBuckets[bucket] = append(testBuckets[bucket], path)
+		testBuckets[bucket] = append(testBuckets[bucket], proj)
 	}
 
-	tr.TestBuckets = testBuckets
+	tr := TravisCI{
+		Enabled:     enabled,
+		FoggVersion: foggVersion,
+		TestBuckets: testBuckets,
+		AWSProfiles: awsProfiles,
+	}
 	return tr
 }
diff --git a/templates/travis-ci/.travis.yml.tmpl b/templates/travis-ci/.travis.yml.tmpl
index 3b997c51..fa7c961e 100644
--- a/templates/travis-ci/.travis.yml.tmpl
+++ b/templates/travis-ci/.travis.yml.tmpl
@@ -16,10 +16,10 @@ before_script:
   - aws configure set aws_secret_access_key $IDACCT_AWS_SECRET_ACCESS_KEY --profile _idacct
   - aws --profile _idacct sts get-caller-identity
 
-{{ range $profile := .AWSProfiles}}
-  - aws configure set profile.{{ $profile.Name }}.role_arn arn:aws:iam::{{ $profile.ID }}:role/{{ $profile.Role}}
-  - aws configure set profile.{{ $profile.Name }}.source_profile _idacct
-  - aws --profile {{ $profile.Name }} sts get-caller-identity
+{{ range $profileName, $profile:= .AWSProfiles}}
+  - aws configure set profile.{{ $profileName }}.role_arn arn:aws:iam::{{ $profile.AccountID }}:role/{{ $profile.RoleName}}
+  - aws configure set profile.{{ $profileName }}.source_profile _idacct
+  - aws --profile {{ $profileName }} sts get-caller-identity
 {{ end }}
 
 jobs:
@@ -29,6 +29,6 @@ jobs:
       script:
         - set -e
       {{- range $component := $testBucket }}
-        - pushd {{ $component }} && travis_retry make check && popd
+        - pushd {{ $component.Dir }} && travis_retry make {{ $component.Command }} && popd
       {{- end -}}
 {{ end }}


Test patch:
diff --git a/apply/apply_test.go b/apply/apply_test.go
index 114e769b..19abc77e 100644
--- a/apply/apply_test.go
+++ b/apply/apply_test.go
@@ -9,7 +9,7 @@ import (
 	"testing"
 
 	"github.com/chanzuckerberg/fogg/config"
-	"github.com/chanzuckerberg/fogg/config/v1"
+	v1 "github.com/chanzuckerberg/fogg/config/v1"
 	"github.com/chanzuckerberg/fogg/templates"
 	"github.com/chanzuckerberg/fogg/util"
 	"github.com/sirupsen/logrus"
@@ -236,13 +236,15 @@ func TestApplySmokeTest(t *testing.T) {
     "infra_s3_bucket": "buck",
     "project": "proj",
     "terraform_version": "0.100.0",
-    "owner": "foo@example.com"
-  },
-  "travis_ci": {
-	"enabled": true,
-	"aws_iam_role_name": "travis",
-        "id_account_name": "id",
-        "test_buckets": 7
+		"owner": "foo@example.com",
+		"tools": {
+			  "travis_ci": {
+						"enabled": true,
+						"aws_iam_role_name": "travis",
+						"id_account_name": "id",
+						"test_buckets": 7
+			  }
+		}
   },
   "accounts": {
     "foo": {
diff --git a/config/config_test.go b/config/config_test.go
index a15de522..11dfb908 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -59,7 +59,7 @@ func Test_detectVersion(t *testing.T) {
 	}
 }
 
-func intptr(i int64) *int64 {
+func intptr(i int) *int {
 	return &i
 }
 
@@ -111,9 +111,9 @@ func TestUpgradeConfigVersion(t *testing.T) {
 						Enabled: boolptr(true),
 					},
 					TravisCI: &v1.TravisCI{
-						Enabled:        true,
-						AWSIAMRoleName: "travis-role",
-						TestBuckets:    13,
+						Enabled:        boolptr(true),
+						AWSIAMRoleName: util.StrPtr("travis-role"),
+						TestBuckets:    intptr(13),
 					},
 				},
 			},
diff --git a/config/v2/validation_test.go b/config/v2/validation_test.go
index b705e871..7cd558ed 100644
--- a/config/v2/validation_test.go
+++ b/config/v2/validation_test.go
@@ -176,3 +176,26 @@ func TestConfig_ValidateAWSProviders(t *testing.T) {
 		})
 	}
 }
+
+func TestConfig_ValidateTravis(t *testing.T) {
+	tests := []struct {
+		fileName string
+		wantErr  bool
+	}{
+		{"v2_invalid_travis_command", true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.fileName, func(t *testing.T) {
+			r := require.New(t)
+
+			b, e := util.TestFile(tt.fileName)
+			r.NoError(e)
+			c, e := ReadConfig(b)
+			r.NoError(e)
+
+			if err := c.ValidateTravis(); (err != nil) != tt.wantErr {
+				t.Errorf("Config.ValidateTravis() error = %v, wantErr %v (err != nil) %v", err, tt.wantErr, (err != nil))
+			}
+		})
+	}
+}
diff --git a/plan/travisci_test.go b/plan/travisci_test.go
index 634f95a5..2c02495e 100644
--- a/plan/travisci_test.go
+++ b/plan/travisci_test.go
@@ -12,9 +12,14 @@ import (
 
 var id1, id2 json.Number
 
+var f, tr bool
+
 func init() {
 	id1 = json.Number("123456789")
 	id2 = json.Number("987654321")
+
+	f = false
+	tr = true
 }
 
 func Test_buildTravisCI_Disabled(t *testing.T) {
@@ -25,7 +30,7 @@ func Test_buildTravisCI_Disabled(t *testing.T) {
 				Common: v2.Common{
 					Tools: &v2.Tools{
 						TravisCI: &v1.TravisCI{
-							Enabled: false,
+							Enabled: &f,
 						},
 					},
 				},
@@ -58,14 +63,16 @@ func Test_buildTravisCI_Profiles(t *testing.T) {
 					},
 				},
 				Backend: &v2.Backend{
-					Bucket:  util.StrPtr("bucket"),
-					Region:  util.StrPtr("us-west-2"),
-					Profile: util.StrPtr("profile"),
+					Bucket:    util.StrPtr("bucket"),
+					Region:    util.StrPtr("us-west-2"),
+					Profile:   util.StrPtr("profile"),
+					AccountID: util.StrPtr("some account id"),
 				},
-				Tools: &v2.Tools{TravisCI: &v1.TravisCI{
-					Enabled:        true,
-					AWSIAMRoleName: "rollin",
-				}},
+				Tools: &v2.Tools{
+					TravisCI: &v1.TravisCI{
+						Enabled:        &tr,
+						AWSIAMRoleName: util.StrPtr("rollin"),
+					}},
 			},
 		},
 		Accounts: map[string]v2.Account{
@@ -82,10 +89,11 @@ func Test_buildTravisCI_Profiles(t *testing.T) {
 	p := &Plan{}
 	p.Accounts = p.buildAccounts(c)
 	tr := p.buildTravisCI(c, "0.1.0")
-	a.Len(tr.AWSProfiles, 1)
-	a.Equal("foo", tr.AWSProfiles[0].Name)
-	a.Equal(id1, tr.AWSProfiles[0].ID)
-	a.Equal("rollin", tr.AWSProfiles[0].Role)
+	a.Len(tr.AWSProfiles, 2)
+	a.Contains(tr.AWSProfiles, "profile")
+	a.Contains(tr.AWSProfiles, "foo")
+	a.Equal(id1.String(), tr.AWSProfiles["foo"].AccountID)
+	a.Equal("rollin", tr.AWSProfiles["foo"].RoleName)
 }
 
 func Test_buildTravisCI_TestBuckets(t *testing.T) {
@@ -107,13 +115,14 @@ func Test_buildTravisCI_TestBuckets(t *testing.T) {
 					},
 				},
 				Backend: &v2.Backend{
-					Bucket:  util.StrPtr("bucket"),
-					Region:  util.StrPtr("us-west-2"),
-					Profile: util.StrPtr("profile"),
+					Bucket:    util.StrPtr("bucket"),
+					Region:    util.StrPtr("us-west-2"),
+					Profile:   util.StrPtr("profile"),
+					AccountID: util.StrPtr("some account id"),
 				},
 				Tools: &v2.Tools{TravisCI: &v1.TravisCI{
-					Enabled:        true,
-					AWSIAMRoleName: "rollin",
+					Enabled:        &tr,
+					AWSIAMRoleName: util.StrPtr("rollin"),
 				}},
 			},
 		},
@@ -137,6 +146,5 @@ func Test_buildTravisCI_TestBuckets(t *testing.T) {
 	a.NotNil(p.Accounts["foo"].Providers.AWS)
 	a.Equal(id1, p.Accounts["foo"].Providers.AWS.AccountID)
 	a.Len(tr.TestBuckets, 1)
-	// 3 because there is always a global
-	a.Len(tr.TestBuckets[0], 3)
+	a.Len(tr.TestBuckets[0], 2)
 }
diff --git a/testdata/v1_full/.travis.yml b/testdata/v1_full/.travis.yml
index e00018ea..68d2b5ae 100644
--- a/testdata/v1_full/.travis.yml
+++ b/testdata/v1_full/.travis.yml
@@ -17,13 +17,21 @@ before_script:
   - aws --profile _idacct sts get-caller-identity
 
 
-  - aws configure set profile.bar.role_arn arn:aws:iam::3:role/travis-role
-  - aws configure set profile.bar.source_profile _idacct
-  - aws --profile bar sts get-caller-identity
+  - aws configure set profile.czi-bar.role_arn arn:aws:iam::3:role/travis-role
+  - aws configure set profile.czi-bar.source_profile _idacct
+  - aws --profile czi-bar sts get-caller-identity
 
-  - aws configure set profile.foo.role_arn arn:aws:iam::2:role/travis-role
-  - aws configure set profile.foo.source_profile _idacct
-  - aws --profile foo sts get-caller-identity
+  - aws configure set profile.czi-env.role_arn arn:aws:iam::5:role/travis-role
+  - aws configure set profile.czi-env.source_profile _idacct
+  - aws --profile czi-env sts get-caller-identity
+
+  - aws configure set profile.czi-foo.role_arn arn:aws:iam::2:role/travis-role
+  - aws configure set profile.czi-foo.source_profile _idacct
+  - aws --profile czi-foo sts get-caller-identity
+
+  - aws configure set profile.czi-stage.role_arn arn:aws:iam::4:role/travis-role
+  - aws configure set profile.czi-stage.source_profile _idacct
+  - aws --profile czi-stage sts get-caller-identity
 
 
 jobs:
@@ -32,27 +40,27 @@ jobs:
     - stage: check
       script:
         - set -e
-        - pushd terraform/global && travis_retry make check && popd
+        - pushd terraform/accounts/bar && travis_retry make check && popd
     - stage: check
       script:
         - set -e
-        - pushd terraform/accounts/bar && travis_retry make check && popd
+        - pushd terraform/accounts/foo && travis_retry make check && popd
     - stage: check
       script:
         - set -e
-        - pushd terraform/accounts/foo && travis_retry make check && popd
+        - pushd terraform/global && travis_retry make check && popd
     - stage: check
       script:
         - set -e
-        - pushd terraform/envs/stage/cloud-env && travis_retry make check && popd
+        - pushd terraform/modules/module1 && travis_retry make check && popd
     - stage: check
       script:
         - set -e
-        - pushd terraform/envs/stage/helm && travis_retry make check && popd
+        - pushd terraform/envs/stage/cloud-env && travis_retry make check && popd
     - stage: check
       script:
         - set -e
-        - pushd terraform/modules/module1 && travis_retry make check && popd
+        - pushd terraform/envs/stage/helm && travis_retry make check && popd
     - stage: check
       script:
         - set -e
diff --git a/testdata/v2_invalid_travis_command/fogg.json b/testdata/v2_invalid_travis_command/fogg.json
new file mode 100644
index 00000000..b9be8b8d
--- /dev/null
+++ b/testdata/v2_invalid_travis_command/fogg.json
@@ -0,0 +1,35 @@
+{
+    "version": 2,
+    "defaults": {
+        "providers": {},
+        "backend": {
+            "bucket": "bucket",
+            "region": "region",
+            "profile": "foo"
+        },
+        "project": "foo",
+        "owner": "foo@example.com",
+        "terraform_version": "1.1.1",
+        "tools": {
+            "travis_ci": {
+                "enabled": true,
+                "command": "lint"
+            }
+        }
+    },
+    "accounts": {
+        "okta-czi-duo": {
+            "providers": {
+                "okta": {
+                    "org_name": "testo",
+                    "version": "v3.0.1"
+                }
+            },
+            "tools": {
+                "travis_ci": {
+                    "command": "cake"
+                }
+            }
+        }
+    }
+}