Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow user to specify/override outputs from the setup stage #1741

Merged
merged 16 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cli/bptest/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
var flags struct {
testDir string
testStage string
setupVars map[string]string
}

func init() {
Expand All @@ -25,6 +26,7 @@ func init() {

Cmd.PersistentFlags().StringVar(&flags.testDir, "test-dir", "", "Path to directory containing integration tests (default is computed by scanning current working directory)")
runCmd.Flags().StringVar(&flags.testStage, "stage", "", "Test stage to execute (default is running all stages in order - init, apply, verify, teardown)")
runCmd.Flags().StringToStringVar(&flags.setupVars, "setup-var", map[string]string{}, "Specify outputs from the setup phase (useful with --stage=verify)")
}

var Cmd = &cobra.Command{
Expand Down Expand Up @@ -90,7 +92,7 @@ var runCmd = &cobra.Command{
if err != nil {
return err
}
testCmd, err := getTestCmd(intTestDir, testStage, args[0], relTestPkg)
testCmd, err := getTestCmd(intTestDir, testStage, args[0], relTestPkg, flags.setupVars)
if err != nil {
return err
}
Expand Down
9 changes: 8 additions & 1 deletion cli/bptest/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const (
// startBufSize is the initial of the buffer token
maxScanTokenSize = 10 * 1024 * 1024
startBufSize = 4096
// This must be kept in sync with what github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft parses.
setupEnvVarPrefix = "CFT_SETUP_"
)

var allTestArgs = []string{"-p", "1", "-count", "1", "-timeout", "0"}
Expand Down Expand Up @@ -99,13 +101,18 @@ func streamExec(cmd *exec.Cmd) error {
}

// getTestCmd returns a prepared cmd for running the specified tests(s)
func getTestCmd(intTestDir string, testStage string, testName string, relTestPkg string) (*exec.Cmd, error) {
func getTestCmd(intTestDir string, testStage string, testName string, relTestPkg string, setupVars map[string]string) (*exec.Cmd, error) {

// pass all current env vars to test command
env := os.Environ()
// set test stage env var if specified
if testStage != "" {
env = append(env, fmt.Sprintf("%s=%s", testStageEnvVarKey, testStage))
}
// Load the env with any setup-vars specified
for k, v := range setupVars {
env = append(env, fmt.Sprintf("%s%s=%s", setupEnvVarPrefix, k, v))
}

// determine binary and args used for test execution
testArgs := append([]string{relTestPkg}, allTestArgs...)
Expand Down
14 changes: 13 additions & 1 deletion cli/bptest/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ func TestGetTestCmd(t *testing.T) {
testStage string
testName string
relTestPkg string
setupVars map[string]string
wantArgs []string
wantEnv []string
errMsg string
}{
{
Expand All @@ -87,6 +89,15 @@ func TestGetTestCmd(t *testing.T) {
testName: "TestFoo",
testStage: "init",
wantArgs: []string{"./...", "-run", "TestFoo", "-p", "1", "-count", "1", "-timeout", "0"},
wantEnv: []string{"RUN_STAGE=init"},
},
{
name: "setup vars",
testName: "TestFoo",
testStage: "verify",
setupVars: map[string]string{"my-key": "my-value"},
wantArgs: []string{"./...", "-run", "TestFoo", "-p", "1", "-count", "1", "-timeout", "0"},
wantEnv: []string{"RUN_STAGE=verify", "CFT_SETUP_my-key=my-value"},
},
}
for _, tt := range tests {
Expand All @@ -98,7 +109,7 @@ func TestGetTestCmd(t *testing.T) {
if tt.relTestPkg == "" {
tt.relTestPkg = "./..."
}
gotCmd, err := getTestCmd(tt.intTestDir, tt.testStage, tt.testName, tt.relTestPkg)
gotCmd, err := getTestCmd(tt.intTestDir, tt.testStage, tt.testName, tt.relTestPkg, tt.setupVars)
if tt.errMsg != "" {
assert.NotNil(err)
assert.Contains(err.Error(), tt.errMsg)
Expand All @@ -109,6 +120,7 @@ func TestGetTestCmd(t *testing.T) {
assert.Contains(gotCmd.Env, fmt.Sprintf("RUN_STAGE=%s", tt.testStage))
}
}
assert.Subset(gotCmd.Env, tt.wantEnv)
})
}
}
44 changes: 44 additions & 0 deletions infra/blueprint-test/pkg/tft/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type TFBlueprintTest struct {
apply func(*assert.Assertions) // apply function
verify func(*assert.Assertions) // verify function
teardown func(*assert.Assertions) // teardown function
setupOutputOverrides map[string]interface{} // override outputs from the Setup phase
}

type tftOption func(*TFBlueprintTest)
Expand Down Expand Up @@ -149,6 +150,13 @@ func WithLogger(logger *logger.Logger) tftOption {
}
}

// WithSetupOutputs overrides output values from the setup stage
func WithSetupOutputs(vars map[string]interface{}) tftOption {
return func(f *TFBlueprintTest) {
f.setupOutputOverrides = vars
}
}

// NewTFBlueprintTest sets defaults, validates and returns a TFBlueprintTest.
func NewTFBlueprintTest(t testing.TB, opts ...tftOption) *TFBlueprintTest {
tft := &TFBlueprintTest{
Expand Down Expand Up @@ -216,6 +224,14 @@ func NewTFBlueprintTest(t testing.TB, opts ...tftOption) *TFBlueprintTest {
tft.logger.Logf(tft.t, "Skipping credential activation %s output from setup", setupKeyOutputName)
}
}
// Load env vars to supplement/override setup
tft.logger.Logf(tft.t, "Loading setup from environment")
if tft.setupOutputOverrides == nil {
tft.setupOutputOverrides = make(map[string]interface{})
}
for k, v := range extractFromEnv("CFT_SETUP_") {
tft.setupOutputOverrides[k] = v
}

tft.logger.Logf(tft.t, "Running tests TF configs in %s", tft.tfDir)
return tft
Expand Down Expand Up @@ -282,6 +298,13 @@ func (b *TFBlueprintTest) GetStringOutput(name string) string {
// GetTFSetupOutputListVal returns TF output from setup for a given key as list.
// It fails test if given key does not output a list type.
func (b *TFBlueprintTest) GetTFSetupOutputListVal(key string) []string {
if v, ok := b.setupOutputOverrides[key]; ok {
if listval, ok := v.([]string); ok {
return listval
} else {
b.t.Fatalf("Setup Override %s is not a list value", key)
}
}
if b.setupDir == "" {
b.t.Fatal("Setup path not set")
}
Expand All @@ -291,6 +314,9 @@ func (b *TFBlueprintTest) GetTFSetupOutputListVal(key string) []string {
// GetTFSetupStringOutput returns TF setup output for a given key as string.
// It fails test if given key does not output a primitive or if setupDir is not configured.
func (b *TFBlueprintTest) GetTFSetupStringOutput(key string) string {
if v, ok := b.setupOutputOverrides[key]; ok {
return v.(string)
}
if b.setupDir == "" {
b.t.Fatal("Setup path not set")
}
Expand All @@ -304,6 +330,24 @@ func loadTFEnvVar(m map[string]string, new map[string]string) {
}
}

// extractFromEnv parses environment variables with the given prefix, and returns a key-value map.
// e.g. CFT_SETUP_key=value returns map[string]string{"key": "value"}
func extractFromEnv(prefix string) map[string]interface{} {
r := make(map[string]interface{})
for _, s := range os.Environ() {
k, v, ok := strings.Cut(s, "=")
if !ok {
// skip malformed entries in os.Environ
continue
}
// For env vars with the prefix, extract the key and value
if setupvar, ok := strings.CutPrefix(k, prefix); ok {
r[setupvar] = v
}
}
return r
}

// ShouldSkip checks if a test should be skipped
func (b *TFBlueprintTest) ShouldSkip() bool {
return b.BlueprintTestConfig.Spec.Skip
Expand Down
161 changes: 156 additions & 5 deletions infra/blueprint-test/pkg/tft/terraform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,28 @@ output "simple_map" {
}
}

func getTFOutputMap(t *testing.T, tf string) map[string]interface{} {
// newTestDir creates a new directory suitable for use as TFDir
func newTestDir(t *testing.T, pattern string, input string) string {
t.Helper()
assert := assert.New(t)

// setup tf file
tfDir, err := os.MkdirTemp("", "")
tfDir, err := os.MkdirTemp("", pattern)
assert.NoError(err)
defer os.RemoveAll(tfDir)
tfFilePath := path.Join(tfDir, "test.tf")
err = os.WriteFile(tfFilePath, []byte(tf), 0644)
err = os.WriteFile(tfFilePath, []byte(input), 0644)
assert.NoError(err)
return tfDir
}

func getTFOutputMap(t *testing.T, tf string) map[string]interface{} {
t.Helper()

tfDir := newTestDir(t, "", tf)
defer os.RemoveAll(tfDir)

// apply tf and get outputs
tOpts := &terraform.Options{TerraformDir: path.Dir(tfFilePath), Logger: logger.Discard}
tOpts := &terraform.Options{TerraformDir: tfDir, Logger: logger.Discard}
terraform.Init(t, tOpts)
terraform.Apply(t, tOpts)
return terraform.OutputAll(t, tOpts)
Expand Down Expand Up @@ -121,3 +129,146 @@ func TestGetKVFromOutputString(t *testing.T) {
})
}
}

func TestSetupOverrideString(t *testing.T) {
tests := []struct {
name string
tfOutputs string
overrides map[string]interface{}
want map[string]string
}{
{name: "no overrides",
tfOutputs: `
output "simple_string" {
value = "foo"
}

output "simple_num" {
value = 1
}

output "simple_bool" {
value = true
}
`,
overrides: map[string]interface{}{},
want: map[string]string{
"simple_string": "foo",
"simple_num": "1",
"simple_bool": "true",
},
},
{name: "all overrides",
tfOutputs: `
output "simple_string" {
value = "foo"
}

output "simple_num" {
value = 1
}

output "simple_bool" {
value = true
}
`,
overrides: map[string]interface{}{
"simple_string": "bar",
"simple_num": "2",
"simple_bool": "false",
},
want: map[string]string{
"simple_string": "bar",
"simple_num": "2",
"simple_bool": "false",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
emptyDir := newTestDir(t, "empty*", "")
setupDir := newTestDir(t, "setup-*", tt.tfOutputs)
defer os.RemoveAll(emptyDir)
defer os.RemoveAll(setupDir)
b := NewTFBlueprintTest(&testingiface.RuntimeT{},
WithSetupOutputs(tt.overrides),
WithTFDir(emptyDir),
WithSetupPath(setupDir))
// create outputs from setup
_, err := terraform.ApplyE(t, &terraform.Options{TerraformDir: setupDir})
if err != nil {
t.Fatalf("Failed to apply setup: %v", err)
}
for k, want := range tt.want {
if b.GetTFSetupStringOutput(k) != want {
t.Errorf("unexpected string output for %s: want %s got %s", k, want, b.GetStringOutput(k))
}
}
})
}
}
func TestSetupOverrideList(t *testing.T) {
tests := []struct {
name string
tfOutputs string
overrides map[string]interface{}
want map[string][]string
}{
{name: "no overrides",
tfOutputs: `
output "simple_list" {
value = ["foo","bar"]
}
`,
overrides: map[string]interface{}{},
want: map[string][]string{
"simple_list": {"foo", "bar"},
},
},
{name: "all overrides",
tfOutputs: `
output "simple_list" {
value = ["foo","bar"]
}
`,
overrides: map[string]interface{}{
"simple_list": []string{"apple", "orange"},
},
want: map[string][]string{
"simple_list": {"apple", "orange"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
emptyDir := newTestDir(t, "empty*", "")
setupDir := newTestDir(t, "setup-*", tt.tfOutputs)
defer os.RemoveAll(emptyDir)
defer os.RemoveAll(setupDir)
b := NewTFBlueprintTest(&testingiface.RuntimeT{},
WithSetupOutputs(tt.overrides),
WithTFDir(emptyDir),
WithSetupPath(setupDir))
// create outputs from setup
_, err := terraform.ApplyE(t, &terraform.Options{TerraformDir: setupDir})
if err != nil {
t.Fatalf("Failed to apply setup: %v", err)
}
for k, want := range tt.want {
got := b.GetTFSetupOutputListVal(k)
assert.ElementsMatchf(t, got, want, "list mismatch: want %s got %s", want)
}
})
}

}

func TestSetupOverrideFromEnv(t *testing.T) {
t.Setenv("CFT_SETUP_my-key", "my-value")
emptyDir := newTestDir(t, "empty*", "")
defer os.RemoveAll(emptyDir)
b := NewTFBlueprintTest(&testingiface.RuntimeT{},
WithTFDir(emptyDir))
got := b.GetTFSetupStringOutput("my-key")
assert.Equal(t, got, "my-value")
}
Loading