infra/blueprint-test/pkg/krmt/krm.go (293 lines of code) (raw):
package krmt
import (
"fmt"
"os"
"path"
"path/filepath"
gotest "testing"
"github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/discovery"
"github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/git"
"github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/kpt"
"github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils"
"github.com/gruntwork-io/terratest/modules/logger"
"github.com/mitchellh/go-testing-interface"
"github.com/otiai10/copy"
"github.com/stretchr/testify/assert"
)
const (
tmpBuildDir = ".build"
prowPullSha = "PULL_PULL_SHA"
)
var CommonSetters = []string{"PROJECT_ID", "BILLING_ACCOUNT_ID", "ORG_ID"}
// KRMBlueprintTest implements bpt.Blueprint and stores information associated with a KRM blueprint test.
type KRMBlueprintTest struct {
discovery.BlueprintTestConfig // additional blueprint test configs
name string // descriptive name for the test
exampleDir string // directory containing KRM blueprint example
additionalResources []string // paths to directories or files containing additional resources to be applied
buildDir string // directory to hydrated blueprint configs pre apply
kpt *kpt.CmdCfg // kpt cmd config
timeout string // timeout for KRM resource status
setters map[string]string // additional setters to populate
updatePkgs bool // whether to update packages in exampleDir
updateCommit string // specific commit to update to
logger *logger.Logger // custom logger
t testing.TB // TestingT or TestingB
init func(*assert.Assertions) // init function
apply func(*assert.Assertions) // apply function
verify func(*assert.Assertions) // verify function
teardown func(*assert.Assertions) // teardown function
}
type krmtOption func(*KRMBlueprintTest)
func WithName(name string) krmtOption {
return func(f *KRMBlueprintTest) {
f.name = name
}
}
func WithDir(dir string) krmtOption {
return func(f *KRMBlueprintTest) {
f.exampleDir = dir
}
}
func WithAdditionalResources(rscs ...string) krmtOption {
return func(f *KRMBlueprintTest) {
f.additionalResources = append(f.additionalResources, rscs...)
}
}
func WithBuildDir(buildDir string) krmtOption {
return func(f *KRMBlueprintTest) {
f.buildDir = buildDir
}
}
func WithUpdatePkgs(update bool) krmtOption {
return func(f *KRMBlueprintTest) {
f.updatePkgs = update
}
}
func WithUpdateCommit(commit string) krmtOption {
return func(f *KRMBlueprintTest) {
f.updateCommit = commit
}
}
func WithTimeout(timeout string) krmtOption {
return func(f *KRMBlueprintTest) {
f.timeout = timeout
}
}
func WithLogger(logger *logger.Logger) krmtOption {
return func(f *KRMBlueprintTest) {
f.logger = logger
}
}
func WithSetters(setters map[string]string) krmtOption {
return func(f *KRMBlueprintTest) {
f.setters = setters
}
}
// NewKRMBlueprintTest sets defaults, validates and returns a KRMBlueprintTest.
func NewKRMBlueprintTest(t testing.TB, opts ...krmtOption) *KRMBlueprintTest {
krmt := &KRMBlueprintTest{
name: fmt.Sprintf("%s KRM Blueprint", t.Name()),
t: t,
setters: make(map[string]string),
updatePkgs: true,
timeout: "10m",
}
// default KRM blueprint methods
krmt.init = krmt.DefaultInit
krmt.apply = krmt.DefaultApply
krmt.verify = krmt.DefaultVerify
krmt.teardown = krmt.DefaultTeardown
// apply options
for _, opt := range opts {
opt(krmt)
}
// if no custom logger, set default based on test verbosity
if krmt.logger == nil {
krmt.logger = utils.GetLoggerFromT()
}
// if explicit exampleDir is provided, validate it else try auto discovery
if krmt.exampleDir != "" {
_, err := os.Stat(krmt.exampleDir)
if os.IsNotExist(err) {
t.Fatalf("Dir path %s does not exist", krmt.exampleDir)
}
} else {
exampleDir, err := discovery.GetConfigDirFromTestDir(utils.GetWD(t))
if err != nil {
t.Fatalf("unable to detect KRM dir :%v", err)
}
krmt.exampleDir = exampleDir
}
// if explicit resourcesDir is provided, validate it.
if len(krmt.additionalResources) != 0 {
for _, path := range krmt.additionalResources {
_, err := os.Stat(path)
if os.IsNotExist(err) {
t.Fatalf("Path for additional resources %s does not exist", path)
}
}
}
// discover test config
var err error
krmt.BlueprintTestConfig, err = discovery.GetTestConfig(path.Join(krmt.exampleDir, discovery.DefaultTestConfigFilename))
if err != nil {
t.Fatal(err)
}
// if no explicit build directory is provided, setup build directory
if krmt.buildDir == "" {
krmt.buildDir = krmt.getDefaultBuildDir()
}
// configure kpt to run in buildDir
krmt.kpt = kpt.NewCmdConfig(t, kpt.WithDir(krmt.buildDir))
// get well known setters from env vars
krmt.getKnownSettersFromEnv()
krmt.logger.Logf(krmt.t, "Running tests KRM configs in %s", krmt.exampleDir)
return krmt
}
// getDefaultBuildDir returns a temporary build directory for hydrated configs.
func (b *KRMBlueprintTest) getDefaultBuildDir() string {
buildDir := path.Join(utils.GetWD(b.t), tmpBuildDir)
err := os.MkdirAll(buildDir, 0755)
if err != nil {
b.t.Fatalf("unable to create %s :%v", buildDir, err)
}
exampleBuildDir := path.Join(buildDir, b.t.Name())
abs, err := filepath.Abs(exampleBuildDir)
if err != nil {
b.t.Fatalf("unable to get absolute path for %s :%v", exampleBuildDir, err)
}
return abs
}
// getKnownSettersFromEnv creates setters from known CommonSetters env vars.
func (b *KRMBlueprintTest) getKnownSettersFromEnv() {
setters := make(map[string]string)
for _, s := range CommonSetters {
sKey, sVal, err := kpt.GenerateSetterKVFromEnvVar(s)
// if a common env var is not set, log and continue
if err != nil {
b.logger.Logf(b.t, "Skipping env var %s: %v", s, err)
} else {
b.logger.Logf(b.t, "Setting env var %s as setter %s: %s", s, sKey, sVal)
setters[sKey] = sVal
}
}
// merge user provided setters with env discovered setters
// user provided setters override env discovered setters
b.setters = kpt.MergeSetters(setters, b.setters)
}
// setupBuildDir prepares build dir with configs from exampleDir.
func (b *KRMBlueprintTest) setupBuildDir() {
// remove buildDir if exists
err := os.RemoveAll(b.buildDir)
if err != nil {
b.t.Fatalf("unable to remove %s :%v", b.buildDir, err)
}
// copy over configs into build dir
err = copy.Copy(b.exampleDir, b.buildDir)
if err != nil {
b.t.Fatalf("unable to copy %s to %s :%v", b.exampleDir, b.buildDir, err)
}
// copy over additional resources into build dir, if present
if len(b.additionalResources) != 0 {
for _, path := range b.additionalResources {
err = copy.Copy(path, b.buildDir)
if err != nil {
b.t.Fatalf("unable to copy %s to %s :%v", path, b.buildDir, err)
}
}
}
// subsequent kpt pkg update requires a clean git repo without uncommitted changes
// init a new git repo in build dir and commit changes
git := git.NewCmdConfig(b.t, git.WithDir(b.buildDir))
git.Init()
git.AddAll()
git.Commit()
}
// updateSetters updates existing setters with user provided setters.
func (b *KRMBlueprintTest) updateSetters() {
rs, err := kpt.ReadPkgResources(b.buildDir)
if err != nil {
b.t.Fatalf("unable to read resources in %s :%v", b.buildDir, err)
}
if err := kpt.UpsertSetters(rs, b.setters); err != nil {
b.t.Fatalf("unable to upsert setters in %s :%v", b.buildDir, err)
}
err = kpt.WritePkgResources(b.buildDir, rs)
if err != nil {
b.t.Fatalf("unable to write resources in %s :%v", b.buildDir, err)
}
}
// updatePkg updates a kpt pkg to a specified commit, prow PR commit or the latest commit.
func (b *KRMBlueprintTest) updatePkg() {
g := git.NewCmdConfig(b.t, git.WithDir(b.exampleDir))
commit := b.updateCommit
// no explicit commit specified
if commit == "" {
// check if prow PR commit exists
prowCommit, found := os.LookupEnv(prowPullSha)
if found {
commit = prowCommit
} else {
commit = g.GetLatestCommit()
}
}
b.kpt.RunCmd("pkg", "update", fmt.Sprintf(".@%s", commit))
}
// DefaultInit sets up build directory, updates pkg, upserts setters and renders config.
func (b *KRMBlueprintTest) DefaultInit(assert *assert.Assertions) {
b.setupBuildDir()
if b.updatePkgs {
b.updatePkg()
}
b.updateSetters()
kpt.NewCmdConfig(b.t, kpt.WithDir(b.buildDir)).RunCmd("fn", "render")
b.kpt.RunCmd("live", "install-resource-group")
b.kpt.RunCmd("live", "init")
}
// DefaultApply installs resource-group, initializes inventory, applies pkg and polls resource statuses until current.
func (b *KRMBlueprintTest) DefaultApply(assert *assert.Assertions) {
b.kpt.RunCmd("live", "apply")
b.kpt.RunCmd("live", "status", "--output", "json", "--poll-until", "current", "--timeout", b.timeout)
}
// DefaultVerify asserts all resources are status successful
func (b *KRMBlueprintTest) DefaultVerify(assert *assert.Assertions) {
jsonOp := b.kpt.RunCmd("live", "apply", "--output", "json")
// assert each resource status is successful
resourceStatus, err := kpt.GetPkgApplyResourcesStatus(jsonOp)
assert.NoError(err, "Resource statuses should be parsable")
for _, r := range resourceStatus {
assert.Equal(kpt.ResourceOperationSuccessful, r.Status, "Status should be successful")
}
// assert count of resources applied equals count of resources successful
groupStatus, err := kpt.GetPkgApplyGroupStatus(jsonOp)
assert.NoError(err, "Group status should be parsable")
assert.Equal(groupStatus.Count, groupStatus.Successful, "All resources should be successful")
}
// DefaultTeardown destroys resources from cluster and polls until deleted.
func (b *KRMBlueprintTest) DefaultTeardown(assert *assert.Assertions) {
b.kpt.RunCmd("live", "destroy")
b.kpt.RunCmd("live", "status", "--output", "json", "--poll-until", "deleted", "--timeout", b.timeout)
}
// ShouldSkip checks if a test should be skipped
func (b *KRMBlueprintTest) ShouldSkip() bool {
return b.BlueprintTestConfig.Spec.Skip
}
// AutoDiscoverAndTest discovers KRM config from examples/fixtures and runs tests.
func AutoDiscoverAndTest(t *gotest.T) {
configs := discovery.FindTestConfigs(t, "./")
for testName, dir := range configs {
t.Run(testName, func(t *gotest.T) {
nt := NewKRMBlueprintTest(t, WithDir(dir))
nt.Test()
})
}
}
// DefineInit defines a custom init function for the blueprint.
func (b *KRMBlueprintTest) DefineInit(init func(*assert.Assertions)) {
b.init = init
}
// DefineApply defines a custom apply function for the blueprint.
func (b *KRMBlueprintTest) DefineApply(apply func(*assert.Assertions)) {
b.apply = apply
}
// DefineVerify defines a custom verify function for the blueprint.
func (b *KRMBlueprintTest) DefineVerify(verify func(*assert.Assertions)) {
b.verify = verify
}
// DefineTeardown defines a custom teardown function for the blueprint.
func (b *KRMBlueprintTest) DefineTeardown(teardown func(*assert.Assertions)) {
b.teardown = teardown
}
// Init runs the default or custom init function for the blueprint.
func (b *KRMBlueprintTest) Init(assert *assert.Assertions) {
b.init(assert)
}
// Apply runs the default or custom apply function for the blueprint.
func (b *KRMBlueprintTest) Apply(assert *assert.Assertions) {
b.apply(assert)
}
// Verify runs the default or custom verify function for the blueprint.
func (b *KRMBlueprintTest) Verify(assert *assert.Assertions) {
b.verify(assert)
}
// Teardown runs the default or custom teardown function for the blueprint.
func (b *KRMBlueprintTest) Teardown(assert *assert.Assertions) {
b.teardown(assert)
}
// Test runs init, apply, verify, teardown in order for the blueprint.
func (b *KRMBlueprintTest) Test() {
if b.ShouldSkip() {
b.logger.Logf(b.t, "Skipping test due to config %s", b.BlueprintTestConfig.Path)
b.t.SkipNow()
return
}
a := assert.New(b.t)
// run stages
utils.RunStage("init", func() { b.Init(a) })
defer utils.RunStage("teardown", func() { b.Teardown(a) })
utils.RunStage("apply", func() { b.Apply(a) })
utils.RunStage("verify", func() { b.Verify(a) })
}
// GetBuildDir returns the temporary build dir created for hydrating config. Defaults to .build/test-name.
func (b *KRMBlueprintTest) GetBuildDir() string {
if b.buildDir == "" {
b.t.Fatalf("unable to get a valid build directory")
}
return b.buildDir
}