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 #29

Closed
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
239 changes: 150 additions & 89 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,87 +60,144 @@ 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")
applyYAMLFilesFromDirectory(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")
deleteYAMLFilesFromDirectory(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")
templateFolder := path.Join(exampleFolderPath, exampleFolder, "input", "templates")
applyYAMLFilesFromDirectory(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")
applyYAMLFilesFromDirectory(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() {
JustBeforeEach(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")
applyYAMLFilesFromDirectory(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")
deleteYAMLFilesFromDirectory(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)
}
})
})
})

// 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
}

// Apply all YAML files defined in the given directory, dir, as well as all sub-directories
// Ignores paths specified in excludePaths
func applyYAMLFilesFromDirectory(ctx context.Context, dir string, excludePaths []string) {
if excludePaths == nil {
excludePaths = []string{}
}

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

// Remove resourceVersion if set
obj.SetResourceVersion("")
// 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 @@ -149,44 +206,57 @@ func applyYAMLFilesFromDirectory(ctx context.Context, dir string) {
}
Expect(err).NotTo(HaveOccurred())
}
}

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

// 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 YAML files defined in the given directory, dir, as well as all sub-directories
func deleteYAMLFilesFromDirectory(ctx context.Context, dir string) {
files, err := os.ReadDir(dir)
Expect(err).NotTo(HaveOccurred())
err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error {
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)
// 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 @@ -265,13 +335,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))
}
}
Loading