From 70abdf07cd30f97bb254eac0ba574cbb5fbd06e7 Mon Sep 17 00:00:00 2001 From: Joaquim Moreno Prusi Date: Mon, 29 May 2023 16:55:18 +0200 Subject: [PATCH] Expose logs/resources after test run Signed-off-by: Joaquim Moreno Prusi --- Makefile | 4 + go.mod | 4 +- test/e2e/e2e_suite_test.go | 15 ++- test/e2e/install_test.go | 186 ++++++++++++++++++++++++++++++++++++- 4 files changed, 204 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 9667eea95..9a588fe28 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,10 @@ export XDG_DATA_HOME ?= /tmp/.local/share # bingo manages consistent tooling versions for things like kind, kustomize, etc. include .bingo/Variables.mk +# ARTIFACT_PATH is the absolute path to the directory where the operator-controller e2e tests will store the artifacts +# for example: ARTIFACT_PATH=/tmp/artifacts make test +export ARTIFACT_PATH ?= + OPERATOR_CONTROLLER_NAMESPACE ?= operator-controller-system KIND_CLUSTER_NAME ?= operator-controller diff --git a/go.mod b/go.mod index 445daa2e3..5c25ca403 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,8 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.25.0 + gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.26.1 k8s.io/apiextensions-apiserver v0.26.1 k8s.io/apimachinery v0.26.1 k8s.io/client-go v0.26.1 @@ -132,9 +134,7 @@ require ( google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.26.1 // indirect k8s.io/apiserver v0.26.1 // indirect k8s.io/klog/v2 v2.80.1 // indirect k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index d5dc9d4e0..fc7e9f623 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -8,6 +8,9 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/env" "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -61,6 +64,13 @@ var _ = BeforeSuite(func() { Expect(catalogd.AddToScheme(scheme)).To(Succeed()) var err error + + err = appsv1.AddToScheme(scheme) + Expect(err).ToNot(HaveOccurred()) + + err = corev1.AddToScheme(scheme) + Expect(err).ToNot(HaveOccurred()) + c, err = client.New(cfg, client.Options{Scheme: scheme}) Expect(err).To(Not(HaveOccurred())) @@ -87,7 +97,10 @@ var _ = BeforeSuite(func() { var _ = AfterSuite(func() { ctx := context.Background() - + if basePath := env.GetString("ARTIFACT_PATH", ""); basePath != "" { + // get all the artifacts from the test run and save them to the artifact path + getArtifactsOutput(ctx, basePath) + } Expect(c.Delete(ctx, operatorCatalog)).To(Succeed()) Eventually(func(g Gomega) { err := c.Get(ctx, types.NamespacedName{Name: operatorCatalog.Name}, &catalogd.Catalog{}) diff --git a/test/e2e/install_test.go b/test/e2e/install_test.go index a598cc2f8..80c2dfaea 100644 --- a/test/e2e/install_test.go +++ b/test/e2e/install_test.go @@ -3,20 +3,38 @@ package e2e import ( "context" "fmt" + "io" + "os" + "path/filepath" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - + catalogd "github.com/operator-framework/catalogd/pkg/apis/core/v1beta1" + operatorv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" + "gopkg.in/yaml.v2" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" + kubeclient "k8s.io/client-go/kubernetes" + "k8s.io/utils/env" + "sigs.k8s.io/controller-runtime/pkg/client" catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" - operatorv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" +const ( + defaultTimeout = 30 * time.Second + defaultPoll = 1 * time.Second + testCatalogRef = "localhost/testdata/catalogs/test-catalog:e2e" + testCatalogName = "test-catalog" + artifactName = "operator-controller-e2e" ) var _ = Describe("Operator Install", func() { @@ -213,6 +231,10 @@ var _ = Describe("Operator Install", func() { }) AfterEach(func() { + if basePath := env.GetString("ARTIFACT_PATH", ""); basePath != "" { + // get all the artifacts from the test run and save them to the artifact path + getArtifactsOutput(ctx, basePath) + } Expect(c.Delete(ctx, operator)).To(Succeed()) Eventually(func(g Gomega) { err := c.Get(ctx, types.NamespacedName{Name: operator.Name}, &operatorv1alpha1.Operator{}) @@ -222,3 +244,163 @@ var _ = Describe("Operator Install", func() { }) }) + +// getArtifactsOutput gets all the artifacts from the test run and saves them to the artifact path. +// right now it will save: +// - operators +// - pods logs +// - deployments +// - bundle +// - bundledeployments +// - catalogsources + +func getArtifactsOutput(ctx context.Context, basePath string) { + kubeClient, err := kubeclient.NewForConfig(cfg) + Expect(err).To(Not(HaveOccurred())) + + // sanitize the artifact name for use as a directory name + testName := strings.ReplaceAll(strings.ToLower(CurrentSpecReport().LeafNodeText), " ", "-") + // Get the test description and sanitize it for use as a directory name + artifactPath := filepath.Join(basePath, artifactName, fmt.Sprint(time.Now().UnixNano()), testName) + + // Create the full artifact path + err = os.MkdirAll(artifactPath, 0755) + Expect(err).To(Not(HaveOccurred())) + + // Get all namespaces + namespaces := corev1.NamespaceList{} + if err := c.List(ctx, &namespaces); err != nil { + GinkgoWriter.Printf("Failed to list namespaces %w", err) + } + + // get all operators save them to the artifact path. + operators := operatorv1alpha1.OperatorList{} + if err := c.List(ctx, &operators, client.InNamespace("")); err != nil { + GinkgoWriter.Printf("Failed to list operators %w", err) + } + for _, operator := range operators.Items { + // Save operator to artifact path + operatorYaml, err := yaml.Marshal(operator) + if err != nil { + GinkgoWriter.Printf("Failed to marshal operator %w", err) + continue + } + if err := os.WriteFile(filepath.Join(artifactPath, operator.Name+"-operator.yaml"), operatorYaml, 0644); err != nil { + GinkgoWriter.Printf("Failed to write operator to file %w", err) + } + } + + // get all catalogsources save them to the artifact path. + catalogsources := catalogd.CatalogList{} + if err := c.List(ctx, &catalogsources, client.InNamespace("")); err != nil { + GinkgoWriter.Printf("Failed to list catalogsources %w", err) + } + for _, catalogsource := range catalogsources.Items { + // Save catalogsource to artifact path + catalogsourceYaml, err := yaml.Marshal(catalogsource) + if err != nil { + GinkgoWriter.Printf("Failed to marshal catalogsource %w", err) + continue + } + if err := os.WriteFile(filepath.Join(artifactPath, catalogsource.Name+"-catalogsource.yaml"), catalogsourceYaml, 0644); err != nil { + GinkgoWriter.Printf("Failed to write catalogsource to file %w", err) + } + } + + // Get all Bundles in the namespace and save them to the artifact path. + bundles := rukpakv1alpha1.BundleList{} + if err := c.List(ctx, &bundles, client.InNamespace("")); err != nil { + GinkgoWriter.Printf("Failed to list bundles %w", err) + } + for _, bundle := range bundles.Items { + // Save bundle to artifact path + bundleYaml, err := yaml.Marshal(bundle) + if err != nil { + GinkgoWriter.Printf("Failed to marshal bundle %w", err) + continue + } + if err := os.WriteFile(filepath.Join(artifactPath, bundle.Name+"-bundle.yaml"), bundleYaml, 0644); err != nil { + GinkgoWriter.Printf("Failed to write bundle to file %w", err) + } + } + + // Get all BundleDeployments in the namespace and save them to the artifact path. + bundleDeployments := rukpakv1alpha1.BundleDeploymentList{} + if err := c.List(ctx, &bundleDeployments, client.InNamespace("")); err != nil { + GinkgoWriter.Printf("Failed to list bundleDeployments %w", err) + } + for _, bundleDeployment := range bundleDeployments.Items { + // Save bundleDeployment to artifact path + bundleDeploymentYaml, err := yaml.Marshal(bundleDeployment) + if err != nil { + GinkgoWriter.Printf("Failed to marshal bundleDeployment %w", err) + continue + } + if err := os.WriteFile(filepath.Join(artifactPath, bundleDeployment.Name+"-bundleDeployment.yaml"), bundleDeploymentYaml, 0644); err != nil { + GinkgoWriter.Printf("Failed to write bundleDeployment to file %w", err) + } + } + + for _, namespace := range namespaces.Items { + // let's ignore kube-* namespaces. + if strings.Contains(namespace.Name, "kube-") { + continue + } + + namespacedArtifactPath := filepath.Join(artifactPath, namespace.Name) + if err := os.Mkdir(namespacedArtifactPath, 0755); err != nil { + GinkgoWriter.Printf("Failed to create namespaced artifact path %w", err) + continue + } + + // get all deployments in the namespace and save them to the artifact path. + deployments := appsv1.DeploymentList{} + if err := c.List(ctx, &deployments, client.InNamespace(namespace.Name)); err != nil { + GinkgoWriter.Printf("Failed to list deployments %w in namespace: %q", err, namespace.Name) + continue + } + + for _, deployment := range deployments.Items { + // Save deployment to artifact path + deploymentYaml, err := yaml.Marshal(deployment) + if err != nil { + GinkgoWriter.Printf("Failed to marshal deployment %w", err) + continue + } + if err := os.WriteFile(filepath.Join(namespacedArtifactPath, deployment.Name+"-deployment.yaml"), deploymentYaml, 0644); err != nil { + GinkgoWriter.Printf("Failed to write deployment to file %w", err) + } + } + + // Get logs from all pods in all namespaces + pods := corev1.PodList{} + if err := c.List(ctx, &pods, client.InNamespace(namespace.Name)); err != nil { + GinkgoWriter.Printf("Failed to list pods %w in namespace: %q", err, namespace.Name) + } + for _, pod := range pods.Items { + if pod.Status.Phase != v1.PodRunning && pod.Status.Phase != v1.PodSucceeded && pod.Status.Phase != v1.PodFailed { + continue + } + for _, container := range pod.Spec.Containers { + logs, err := kubeClient.CoreV1().Pods(namespace.Name).GetLogs(pod.Name, &v1.PodLogOptions{Container: container.Name}).Stream(ctx) + if err != nil { + GinkgoWriter.Printf("Failed to get logs for pod %q in namespace %q: %w", pod.Name, namespace.Name, err) + continue + } + defer logs.Close() + + outFile, err := os.Create(filepath.Join(namespacedArtifactPath, pod.Name+"-"+container.Name+"-logs.txt")) + if err != nil { + GinkgoWriter.Printf("Failed to create file for pod %q in namespace %q: %w", pod.Name, namespace.Name, err) + continue + } + defer outFile.Close() + + if _, err := io.Copy(outFile, logs); err != nil { + GinkgoWriter.Printf("Failed to copy logs for pod %q in namespace %q: %w", pod.Name, namespace.Name, err) + continue + } + } + } + } +}