cli/bptest/convert.go (219 lines of code) (raw):
package bptest
import (
"bytes"
"embed"
"encoding/json"
"fmt"
"html/template"
"os"
"path"
"strings"
"github.com/iancoleman/strcase"
cb "google.golang.org/api/cloudbuild/v1"
"sigs.k8s.io/yaml"
)
const (
intTestPath = "test/integration"
intTestBuildFilePath = "build/int.cloudbuild.yaml"
inspecInputsFile = "inspec.yml"
tmplSuffix = ".tmpl"
goModFilename = "go.mod"
bptTestFilename = "blueprint_test.go"
)
var (
//go:embed templates
templateFiles embed.FS
kitchenCFTStageMapping = map[string]string{
"create": stages[0],
"converge": stages[2],
"verify": stages[3],
"destroy": stages[4],
}
)
type inspecInputs struct {
Name string `yaml:"name"`
Attributes []struct {
Name string `yaml:"name"`
} `yaml:"attributes"`
}
// convertKitchenTests converts all kitchen tests to blueprint tests and updates build files
func convertKitchenTests() error {
cwd, err := os.Getwd()
if err != nil {
return err
}
// write go mod
goMod, err := getTmplFileContents(goModFilename)
if err != nil {
return err
}
err = writeFile(path.Join(intTestPath, goModFilename), fmt.Sprintf(goMod, path.Base(cwd)))
if err != nil {
return fmt.Errorf("error writing go mod file: %w", err)
}
// write discover test
discoverTest, err := getTmplFileContents(discoverTestFilename)
if err != nil {
return err
}
err = writeFile(path.Join(intTestPath, discoverTestFilename), discoverTest)
if err != nil {
return fmt.Errorf("error writing discover_test.go: %w", err)
}
testDirs, err := getCurrentTestDirs()
if err != nil {
return fmt.Errorf("error getting current test dirs: %w", err)
}
for _, dir := range testDirs {
err = convertTest(path.Join(intTestPath, dir))
if err != nil {
return fmt.Errorf("error converting %s: %w", dir, err)
}
}
// remove kitchen
err = os.Remove(".kitchen.yml")
if err != nil {
return fmt.Errorf("error removing .kitchen.yml: %w", err)
}
// convert build file
// We use build to identify commands to update and update the commands in the buildFile.
// This minimizes unnecessary diffs in build yaml due to round tripping.
build, buildFile, err := getBuildFromFile(intTestBuildFilePath)
if err != nil {
return fmt.Errorf("error unmarshalling %s: %w", intTestBuildFilePath, err)
}
newBuildFile, err := transformBuild(build, buildFile)
if err != nil {
return fmt.Errorf("error transforming buildfile: %w", err)
}
return writeFile(intTestBuildFilePath, newBuildFile)
}
// getCurrentTestDirs returns current test dirs in intTestPath
func getCurrentTestDirs() ([]string, error) {
files, err := os.ReadDir(intTestPath)
if err != nil {
return nil, err
}
var dirs []string
for _, f := range files {
if f.IsDir() {
dirs = append(dirs, f.Name())
}
}
return dirs, nil
}
// convertTest converts a kitchen test in dir to blueprint test
func convertTest(dir string) error {
// read inspec.yaml
f, err := os.ReadFile(path.Join(dir, inspecInputsFile))
if err != nil {
return fmt.Errorf("error reading inspec file: %w", err)
}
var inspec inspecInputs
err = yaml.Unmarshal(f, &inspec)
if err != nil {
return fmt.Errorf("error unmarshalling inspec file: %w", err)
}
// get inspec input attributes
var inputs []string
for _, i := range inspec.Attributes {
inputs = append(inputs, i.Name)
}
// get bpt skeleton
testName := path.Base(dir)
bpTest, err := getBPTestFromTmpl(testName, inputs)
if err != nil {
return fmt.Errorf("error creating blueprint test: %w", err)
}
// remove old test
err = os.RemoveAll(dir)
if err != nil {
return fmt.Errorf("error removing old test dir: %w", err)
}
// write bpt
err = os.MkdirAll(dir, os.ModePerm)
if err != nil {
return fmt.Errorf("error creating test dir: %w", err)
}
return writeFile(path.Join(dir, fmt.Sprintf("%s_test.go", strcase.ToSnake(testName))), bpTest)
}
// getTmplFileContents returns contents of embedded file f
func getTmplFileContents(f string) (string, error) {
tmplF := path.Join("templates", fmt.Sprintf("%s%s", f, tmplSuffix))
contents, err := templateFiles.ReadFile(tmplF)
if err != nil {
return "", fmt.Errorf("error reading %s : %w", tmplF, err)
}
return string(contents), nil
}
// getTestFnName returns the go test function name
func getTestFnName(name string) string {
return fmt.Sprintf("Test%s", strcase.ToCamel(name))
}
// getBPTestFromTmpl returns a skeleton blueprint test
func getBPTestFromTmpl(testName string, inputs []string) (string, error) {
pkgName := strcase.ToSnake(testName)
fnName := getTestFnName(testName)
tmpl, err := getTmplFileContents(bptTestFilename)
if err != nil {
return "", err
}
t, err := template.New("test").Funcs(template.FuncMap{"toLowerCamel": strcase.ToLowerCamel}).Parse(tmpl)
if err != nil {
return "", err
}
var tpl bytes.Buffer
err = t.Execute(&tpl, struct {
PkgName string
FnName string
Inputs []string
}{
PkgName: pkgName,
FnName: fnName,
Inputs: inputs,
},
)
if err != nil {
return "", err
}
return tpl.String(), nil
}
// writeFile writes content to file path
func writeFile(p string, content string) error {
return os.WriteFile(p, []byte(content), os.ModePerm)
}
// transformBuild transforms cloudbuild file contents with kitchen commands to CFT cli commands
func transformBuild(b *cb.Build, f string) (string, error) {
for _, step := range b.Steps {
// test commands have at least two args
if len(step.Args) < 2 {
continue
}
cmd := step.Args[len(step.Args)-1]
// skip if not a kitchen command
kitchenCmdIndex := strings.Index(cmd, "kitchen_do")
if kitchenCmdIndex == -1 {
continue
}
kitchenCmd := cmd[kitchenCmdIndex:]
newCmd, err := getCFTCmd(kitchenCmd)
if err != nil {
return "", err
}
f = strings.ReplaceAll(f, cmd, newCmd)
}
return f, nil
}
// getCFTCmd returns an equivalent CFT command for a kitchen command
func getCFTCmd(kitchenCmd string) (string, error) {
if !strings.Contains(kitchenCmd, "kitchen_do") {
return "", fmt.Errorf("invalid kitchen command: %s", kitchenCmd)
}
cmdArr := strings.Split(kitchenCmd, " ")
cftCmd := []string{"cft", "test", "run"}
// cmd of form kitchen_do verb
if len(cmdArr) == 2 {
kitchenStage := cmdArr[len(cmdArr)-1]
cftCmd = append(cftCmd, []string{"all", "--stage", kitchenCFTStageMapping[kitchenStage]}...)
} else if len(cmdArr) == 3 {
// cmd of form kitchen_do verb test-name
kitchenTestName := cmdArr[len(cmdArr)-1]
kitchenStage := cmdArr[len(cmdArr)-2]
cftTestName := getTestFnName(strings.TrimSuffix(kitchenTestName, "-local"))
cftCmd = append(cftCmd, []string{cftTestName, "--stage", kitchenCFTStageMapping[kitchenStage]}...)
} else {
return "", fmt.Errorf("unknown kitchen command: %s", kitchenCmd)
}
cftCmd = append(cftCmd, "--verbose")
return strings.Join(cftCmd, " "), nil
}
// getBuildFromFile unmarshalls a cloudbuild file
func getBuildFromFile(fp string) (*cb.Build, string, error) {
f, err := os.ReadFile(fp)
if err != nil {
return nil, "", err
}
j, err := yaml.YAMLToJSON(f)
if err != nil {
return nil, "", err
}
var b cb.Build
if err = json.Unmarshal(j, &b); err != nil {
fmt.Println(err.Error())
}
return &b, string(f), nil
}