package tests import ( "fmt" "io/ioutil" "path/filepath" "strings" "testing" "github.com/ghodss/yaml" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct" "sigs.k8s.io/kustomize/v3/k8sdeps/transformer" "sigs.k8s.io/kustomize/v3/pkg/fs" "sigs.k8s.io/kustomize/v3/pkg/loader" "sigs.k8s.io/kustomize/v3/pkg/plugins" "sigs.k8s.io/kustomize/v3/pkg/resmap" "sigs.k8s.io/kustomize/v3/pkg/resource" "sigs.k8s.io/kustomize/v3/pkg/target" "sigs.k8s.io/kustomize/v3/pkg/validators" ) type KustomizeTestCase struct { // Package is the path to the kustomize directory to run kustomize in Package string // Expected is a path to a directory containing the expected resources Expected string } // RunTestCase runs the specified test case func RunTestCase(t *testing.T, testCase *KustomizeTestCase) { expected := map[string]*expectedResource{} // Read all the YAML files containing expected resources and parse them. files, err := ioutil.ReadDir(testCase.Expected) if err != nil { t.Fatal(err) } for _, f := range files { contents, err := ioutil.ReadFile(filepath.Join(testCase.Expected, f.Name())) if err != nil { t.Fatalf("Err: %v", err) } u := &unstructured.Unstructured{} if err := yaml.Unmarshal([]byte(contents), u); err != nil { t.Fatalf("Error: %v", err) } r := &expectedResource{ fileName: f.Name(), yaml: string(contents), u: u, } expected[r.Key()] = r } fsys := fs.MakeRealFS() // We don't want to enforce the security check. // This is equivalent to running: // kustomize build --load_restrictor none lrc := loader.RestrictionNone _loader, loaderErr := loader.NewLoader(lrc, validators.MakeFakeValidator(), testCase.Package, fsys) if loaderErr != nil { t.Fatalf("could not load kustomize loader: %v", loaderErr) } rf := resmap.NewFactory(resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl()), transformer.NewFactoryImpl()) pc := plugins.DefaultPluginConfig() kt, err := target.NewKustTarget(_loader, rf, transformer.NewFactoryImpl(), plugins.NewLoader(pc, rf)) if err != nil { t.Fatalf("Unexpected construction error %v", err) } actual, err := kt.MakeCustomizedResMap() if err != nil { t.Fatalf("Err: %v", err) } actualNames := map[string]bool{} // Check that all the actual resources match the expected resources for _, r := range actual.Resources() { rKey := key(r.GetKind(), r.GetName()) actualNames[rKey] = true e, ok := expected[rKey] if !ok { t.Errorf("Actual output included an unexpected resource; resource: %v", rKey) continue } actualYaml, err := r.AsYAML() if err != nil { t.Errorf("Could not generate YAML for resource: %v; error: %v", rKey, err) continue } // Ensure the actual YAML matches. if string(actualYaml) != e.yaml { ReportDiffAndFail(t, actualYaml, e.yaml) } } // Make sure we aren't missing any expected resources for name, _ := range expected { if _, ok := actualNames[name]; !ok { t.Errorf("Actual resources is missing expected resource: %v", name) } } } func convertToArray(x string) ([]string, int) { a := strings.Split(strings.TrimSuffix(x, "\n"), "\n") maxLen := 0 for i, v := range a { z := tabToSpace(v) if len(z) > maxLen { maxLen = len(z) } a[i] = z } return a, maxLen } // expectedResource represents an expected Kubernetes resource to be generated // from the kustomize package. type expectedResource struct { fileName string yaml string u *unstructured.Unstructured } // key generates a name to index resources by. It is used to match expected and actual resources. func key(kind string, name string) string { return kind + "." + name } // Key returns a unique identifier fo this resource that can be used to index it func (r *expectedResource) Key() string { if r.u == nil { return "" } return key(r.u.GetKind(), r.u.GetName()) } // Pretty printing of file differences. func ReportDiffAndFail(t *testing.T, actual []byte, expected string) { sE, maxLen := convertToArray(expected) sA, _ := convertToArray(string(actual)) fmt.Println("===== ACTUAL BEGIN ========================================") fmt.Print(string(actual)) fmt.Println("===== ACTUAL END ==========================================") format := fmt.Sprintf("%%s %%-%ds %%s\n", maxLen+4) limit := 0 if len(sE) < len(sA) { limit = len(sE) } else { limit = len(sA) } fmt.Printf(format, " ", "EXPECTED", "ACTUAL") fmt.Printf(format, " ", "--------", "------") for i := 0; i < limit; i++ { fmt.Printf(format, hint(sE[i], sA[i]), sE[i], sA[i]) } if len(sE) < len(sA) { for i := len(sE); i < len(sA); i++ { fmt.Printf(format, "X", "", sA[i]) } } else { for i := len(sA); i < len(sE); i++ { fmt.Printf(format, "X", sE[i], "") } } t.Fatalf("Expected not equal to actual") } func tabToSpace(input string) string { var result []string for _, i := range input { if i == 9 { result = append(result, " ") } else { result = append(result, string(i)) } } return strings.Join(result, "") } func hint(a, b string) string { if a == b { return " " } return "X" }