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

✅ Add integration test for Template Controller to check deletion #35

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
247 changes: 154 additions & 93 deletions internal/controller/template_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"os"
"path"
"path/filepath"
"strings"
"slices"
"time"

sigyaml "sigs.k8s.io/yaml"
Expand Down Expand Up @@ -36,7 +36,7 @@ var _ = Describe("Template Controller Integration Test", func() {
BeforeEach(func() {
ctx = context.Background()

By("ensuring the namespace exists")
By("ensuring the templates namespace exists")
namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespaceName,
Expand All @@ -60,57 +60,97 @@ var _ = Describe("Template Controller Integration Test", func() {
return fmt.Errorf("namespace not active yet")
}, 10*time.Second, 2*time.Second).Should(Succeed())

})

AfterEach(func() {
By("deleting all applied resources")
By("creating input resources")
exampleFolderPath := path.Join("../", "../", "examples")
exampleFolders, err := utils.GetSubDirs(exampleFolderPath)
Expect(err).NotTo(HaveOccurred())
for _, exampleFolder := range exampleFolders {
deleteExampleResources(ctx, path.Join(exampleFolderPath, exampleFolder, "input"))
inputFolder := path.Join(exampleFolderPath, exampleFolder, "input")
templateFolder := path.Join(inputFolder, "templates")
applyResourcesFromYAMLInDir(ctx, inputFolder, []string{templateFolder})
}

})

It("should apply and verify examples", func() {

AfterEach(func() {
By("deleting input resources and templates")
exampleFolderPath := path.Join("../", "../", "examples")
exampleFolders, err := utils.GetSubDirs(exampleFolderPath)
Expect(err).NotTo(HaveOccurred())
for _, exampleFolder := range exampleFolders {
By(fmt.Sprintf("Running example %s", exampleFolder))
applyExampleResources(ctx, path.Join(exampleFolderPath, exampleFolder))
inputFolder := path.Join(exampleFolderPath, exampleFolder, "input")
deleteResourcesFromYAMLInDir(ctx, inputFolder)
}
})

// another eventuallly block to check if the output.yaml file has been created
By("verifying that the output.yaml file has been created")
for _, exampleFolder := range exampleFolders {
exampleFolderPath := path.Join("..", "..", "examples", exampleFolder)
verifyExampleOutput(exampleFolderPath, "out.yaml")
}
Context("When Template is applied", func() {
It("creates expected managed resources", func() {
exampleFolderPath := path.Join("../", "../", "examples")
exampleFolders, err := utils.GetSubDirs(exampleFolderPath)
Expect(err).NotTo(HaveOccurred())

for _, exampleFolder := range exampleFolders {
By(fmt.Sprintf("Running example %s", exampleFolder))

By("applying example templates")
jamesdobson marked this conversation as resolved.
Show resolved Hide resolved
templateFolder := path.Join(exampleFolderPath, exampleFolder, "input", "templates")
applyResourcesFromYAMLInDir(ctx, templateFolder, nil)

By("verifying that the output.yaml file has been created")
verifyExampleOutput(path.Join(exampleFolderPath, exampleFolder), "out.yaml")
}
})

It("creates expected managed resources again to test for race conditions", func() {
exampleFolderPath := path.Join("../", "../", "examples")
exampleFolders, err := utils.GetSubDirs(exampleFolderPath)
Expect(err).NotTo(HaveOccurred())

for _, exampleFolder := range exampleFolders {
By(fmt.Sprintf("Running example %s", exampleFolder))

By("applying example templates")
templateFolder := path.Join(exampleFolderPath, exampleFolder, "input", "templates")
applyResourcesFromYAMLInDir(ctx, templateFolder, nil)

By("verifying that the output.yaml file has been created")
verifyExampleOutput(path.Join(exampleFolderPath, exampleFolder), "out.yaml")
}
})
})
It("repeate apply and verify examples to check some race conditions", func() {

exampleFolderPath := path.Join("../", "../", "examples")
exampleFolders, err := utils.GetSubDirs(exampleFolderPath)
Expect(err).NotTo(HaveOccurred())
for _, exampleFolder := range exampleFolders {
By(fmt.Sprintf("Running example %s", exampleFolder))
applyExampleResources(ctx, path.Join(exampleFolderPath, exampleFolder))
}
Context("When Template is deleted", func() {
BeforeEach(func() {
By("applying example templates")
exampleFolderPath := path.Join("../", "../", "examples")
exampleFolders, err := utils.GetSubDirs(exampleFolderPath)
Expect(err).NotTo(HaveOccurred())
for _, exampleFolder := range exampleFolders {
templateFolder := path.Join(exampleFolderPath, exampleFolder, "input", "templates")
applyResourcesFromYAMLInDir(ctx, templateFolder, nil)
}
})

// another eventuallly block to check if the output.yaml file has been created
By("verifying that the output.yaml file has been created")
for _, exampleFolder := range exampleFolders {
exampleFolderPath := path.Join("..", "..", "examples", exampleFolder)
verifyExampleOutput(exampleFolderPath, "out.yaml")
}
It("deletes managed resources", func() {
exampleFolderPath := path.Join("../", "../", "examples")
exampleFolders, err := utils.GetSubDirs(exampleFolderPath)
Expect(err).NotTo(HaveOccurred())

for _, exampleFolder := range exampleFolders {
By("deleting example template")
templateFolder := path.Join(exampleFolderPath, exampleFolder, "input", "templates")
deleteResourcesFromYAMLInDir(ctx, templateFolder)

By("verifying expected managed resources got deleted")
expectedOutputPath := path.Join(exampleFolderPath, exampleFolder, "expected", "out.yaml")
expectedOutputResource, err := getK8sClientObject(expectedOutputPath)
Expect(err).NotTo(HaveOccurred())
waitForDeletion(ctx, expectedOutputResource, 10*time.Second, 5*time.Millisecond)
}
})
})
It("should make sure that if the result of a query is changes,the resource that was created should be updated", func() { //nolint:errcheck
applyExampleResources(ctx, path.Join("test_resources", "delete_scenario"))

It("should update managed resources if the result of a query changes", func() {
applyResourcesFromYAMLInDir(ctx, path.Join("test_resources", "delete_scenario"), nil)
configmaps := []string{"test-aws-auth-tenant-acme", "test-aws-auth-base", "test-aws-auth-tenant-umbrella"}
Eventually(func() error {

Expand Down Expand Up @@ -140,40 +180,57 @@ var _ = Describe("Template Controller Integration Test", func() {
}
return nil
}, 10*time.Second, 2*time.Second).Should(Succeed())

})
})

// Function to apply resources from an example
func applyExampleResources(ctx context.Context, examplePath string) {
inputPath := filepath.Join(examplePath, "input")
// get all folders in the input directory
folders, err := utils.GetSubDirs(inputPath)
// Returns True if the given path refers to a YAML file
func isYAMLFile(path string) bool {
fileInfo, err := os.Stat(path)
Expect(err).NotTo(HaveOccurred())
for _, folder := range folders {
applyYAMLFilesFromDirectory(ctx, path.Join(inputPath, folder))
}

return !fileInfo.IsDir() && (filepath.Ext(fileInfo.Name()) == ".yaml" || filepath.Ext(fileInfo.Name()) == ".yml")
}

// Function to apply all YAML files from a directory
func applyYAMLFilesFromDirectory(ctx context.Context, dir string) {
files, err := os.ReadDir(dir)
// Get Kubernetes object defined by the YAML in the given path
func getK8sClientObject(path string) (*unstructured.Unstructured, error) {
if isValidPath := isYAMLFile(path); !isValidPath {
return nil, fmt.Errorf("Invalid path: %s should be a YAML file", path)
}

content, err := os.ReadFile(path)
Expect(err).NotTo(HaveOccurred())

for _, file := range files {
if !file.IsDir() && (strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml")) {
filePath := filepath.Join(dir, file.Name())
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
obj := &unstructured.Unstructured{}
_, _, err = decoder.Decode(content, nil, obj)
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(filePath)
Expect(err).NotTo(HaveOccurred())
// Remove resourceVersion if set
obj.SetResourceVersion("")

decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
obj := &unstructured.Unstructured{}
_, _, err = decoder.Decode(content, nil, obj)
Expect(err).NotTo(HaveOccurred())
return obj, nil
}

// Remove resourceVersion if set
obj.SetResourceVersion("")
// Apply all resources specified by the YAML files in the given directory, dir, as well as all sub-directories
// Ignores paths specified in excludePaths
func applyResourcesFromYAMLInDir(ctx context.Context, dir string, excludePaths []string) {
if excludePaths == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see this the first time, I haven't used the slices package much, but I think nil will just work as an empty slice, see: https://go.dev/play/p/o6KpQfftdPf

So hopefully you can remove this if block and it will still work.

excludePaths = []string{}
}

err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error {
Expect(err).NotTo(HaveOccurred())

// Ignore paths specified in excludePaths
if slices.Contains(excludePaths, path) {
return filepath.SkipDir
}

// Entry is a YAML file - Apply manifest
if isYAMLFile(path) {
// Get K8s resource from YAML file
obj, err := getK8sClientObject(path)
Expect(err).NotTo(HaveOccurred())

// Apply the resource to the cluster
err = k8sClient.Create(ctx, obj)
Expand All @@ -182,44 +239,57 @@ func applyYAMLFilesFromDirectory(ctx context.Context, dir string) {
}
Expect(err).NotTo(HaveOccurred())
}
}
}

func deleteYAMLFilesFromDirectory(ctx context.Context, dir string) {
files, err := os.ReadDir(dir)
// Entry is a directory - Continue traversal
return nil
})
Expect(err).NotTo(HaveOccurred())
}

for _, file := range files {
if !file.IsDir() && (strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml")) {
filePath := filepath.Join(dir, file.Name())
fileData, err := os.ReadFile(filePath)
Expect(err).NotTo(HaveOccurred())
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
obj := &unstructured.Unstructured{}
_, _, err = decoder.Decode(fileData, nil, obj)
// Waits until the given K8s Client resource is deleted, or times out
func waitForDeletion(ctx context.Context, obj *unstructured.Unstructured, timeout, interval time.Duration) {
fmt.Printf("Waiting for deletion of resource %s/%s\n", obj.GetKind(), obj.GetName())
Eventually(func() error {
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)
if errors.IsNotFound(err) {
fmt.Printf("Resource %s/%s deleted done\n", obj.GetKind(), obj.GetName())
return nil
}
if err != nil {
return err
}

fmt.Printf("Resource %s/%s not deleted yet\n", obj.GetKind(), obj.GetName())
return fmt.Errorf("Resource %s/%s still exists", obj.GetKind(), obj.GetName())
}, timeout, interval).Should(Succeed())
}

// Delete all resources specified by the YAML files in the given directory, dir, as well as all sub-directories
func deleteResourcesFromYAMLInDir(ctx context.Context, dir string) {
err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error {
Expect(err).NotTo(HaveOccurred())

// Entry is a YAML file - Delete manifest
if isYAMLFile(path) {
// Get K8s resource from YAML file
obj, err := getK8sClientObject(path)
Expect(err).NotTo(HaveOccurred())

fmt.Printf("Deleting %s/%s\n", obj.GetKind(), obj.GetName())
err = k8sClient.Delete(ctx, obj)
if errors.IsNotFound(err) {
continue
return nil
}
Expect(err).NotTo(HaveOccurred())
fmt.Printf("Deleting %s/%s\n", obj.GetKind(), obj.GetName())

// Wait for the resource to be deleted
Eventually(func() error {
err = k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)
if errors.IsNotFound(err) {
fmt.Printf("Resource %s/%s deleted done\n", obj.GetKind(), obj.GetName())
return nil
}
if err != nil {
return err
}
fmt.Printf("Resource %s/%s not deleted yet\n", obj.GetKind(), obj.GetName())
return fmt.Errorf("resource still exists")
}, 10*time.Second, 5*time.Millisecond).Should(Succeed())
waitForDeletion(ctx, obj, 10*time.Second, 5*time.Millisecond)
}
}

// Entry is a directory - Continue traversal
return nil
})
Expect(err).NotTo(HaveOccurred())
}

func verifyExampleOutput(exampleFolder string, exampleFile string) {
Expand Down Expand Up @@ -298,13 +368,4 @@ func verifyExampleOutput(exampleFolder string, exampleFile string) {

return nil
}, 10*time.Second, 2*time.Second).Should(Succeed())

}

func deleteExampleResources(ctx context.Context, inputPath string) {
folders, err := utils.GetSubDirs(inputPath)
Expect(err).NotTo(HaveOccurred())
for _, folder := range folders {
deleteYAMLFilesFromDirectory(ctx, path.Join(inputPath, folder))
}
}