Skip to content

Commit

Permalink
Collect, restore and cleanup global resources in e2es
Browse files Browse the repository at this point in the history
  • Loading branch information
tnozicka committed Aug 29, 2024
1 parent 4578109 commit 2d8f761
Show file tree
Hide file tree
Showing 10 changed files with 508 additions and 272 deletions.
74 changes: 42 additions & 32 deletions pkg/cmd/tests/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/scylladb/scylla-operator/test/e2e/framework"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/rest"
)

Expand Down Expand Up @@ -42,26 +43,26 @@ var supportedBroadcastAddressTypes = []scyllav1.BroadcastAddressType{
type TestFrameworkOptions struct {
genericclioptions.ClientConfigSet

ArtifactsDir string
DeleteTestingNSPolicyUntyped string
DeleteTestingNSPolicy framework.DeleteTestingNSPolicyType
IngressController *IngressControllerOptions
ScyllaClusterOptionsUntyped *ScyllaClusterOptions
scyllaClusterOptions *framework.ScyllaClusterOptions
ObjectStorageBucket string
GCSServiceAccountKeyPath string
S3CredentialsFilePath string
objectStorageType framework.ObjectStorageType
gcsServiceAccountKey []byte
s3CredentialsFile []byte
ArtifactsDir string
CleanupNSPolicyUntyped string
CleanupPolicy framework.CleanupPolicyType
IngressController *IngressControllerOptions
ScyllaClusterOptionsUntyped *ScyllaClusterOptions
scyllaClusterOptions *framework.ScyllaClusterOptions
ObjectStorageBucket string
GCSServiceAccountKeyPath string
S3CredentialsFilePath string
objectStorageType framework.ObjectStorageType
gcsServiceAccountKey []byte
s3CredentialsFile []byte
}

func NewTestFrameworkOptions(streams genericclioptions.IOStreams, userAgent string) *TestFrameworkOptions {
return &TestFrameworkOptions{
ClientConfigSet: genericclioptions.NewClientConfigSet(userAgent),
ArtifactsDir: "",
DeleteTestingNSPolicyUntyped: string(framework.DeleteTestingNSPolicyAlways),
IngressController: &IngressControllerOptions{},
ClientConfigSet: genericclioptions.NewClientConfigSet(userAgent),
ArtifactsDir: "",
CleanupNSPolicyUntyped: string(framework.CleanupPolicyAlways),
IngressController: &IngressControllerOptions{},
ScyllaClusterOptionsUntyped: &ScyllaClusterOptions{
NodeServiceType: string(scyllav1.NodeServiceTypeHeadless),
NodesBroadcastAddressType: string(scyllav1.BroadcastAddressTypePodIP),
Expand All @@ -81,11 +82,20 @@ func (o *TestFrameworkOptions) AddFlags(cmd *cobra.Command) {
o.ClientConfigSet.AddFlags(cmd)

cmd.PersistentFlags().StringVarP(&o.ArtifactsDir, "artifacts-dir", "", o.ArtifactsDir, "A directory for storing test artifacts. No data is collected until set.")
cmd.PersistentFlags().StringVarP(&o.DeleteTestingNSPolicyUntyped, "delete-namespace-policy", "", o.DeleteTestingNSPolicyUntyped, fmt.Sprintf("Namespace deletion policy. Allowed values are [%s].", strings.Join(
cmd.PersistentFlags().StringVarP(&o.CleanupNSPolicyUntyped, "delete-namespace-policy", "", o.CleanupNSPolicyUntyped, fmt.Sprintf("Namespace deletion policy. Allowed values are [%s].", strings.Join(
[]string{
string(framework.DeleteTestingNSPolicyAlways),
string(framework.DeleteTestingNSPolicyNever),
string(framework.DeleteTestingNSPolicyOnSuccess),
string(framework.CleanupPolicyAlways),
string(framework.CleanupPolicyNever),
string(framework.CleanupPolicyOnSuccess),
},
", ",
)))
utilruntime.Must(cmd.PersistentFlags().MarkDeprecated("delete-namespace-policy", "--delete-namespace-policy is deprecated - please use --cleanup-policy instead"))
cmd.PersistentFlags().StringVarP(&o.CleanupNSPolicyUntyped, "cleanup-policy", "", o.CleanupNSPolicyUntyped, fmt.Sprintf("Cleanup policy. Allowed values are [%s].", strings.Join(
[]string{
string(framework.CleanupPolicyAlways),
string(framework.CleanupPolicyNever),
string(framework.CleanupPolicyOnSuccess),
},
", ",
)))
Expand Down Expand Up @@ -118,10 +128,10 @@ func (o *TestFrameworkOptions) Validate(args []string) error {
errors = append(errors, err)
}

switch p := framework.DeleteTestingNSPolicyType(o.DeleteTestingNSPolicyUntyped); p {
case framework.DeleteTestingNSPolicyAlways,
framework.DeleteTestingNSPolicyOnSuccess,
framework.DeleteTestingNSPolicyNever:
switch p := framework.CleanupPolicyType(o.CleanupNSPolicyUntyped); p {
case framework.CleanupPolicyAlways,
framework.CleanupPolicyOnSuccess,
framework.CleanupPolicyNever:
default:
errors = append(errors, fmt.Errorf("invalid DeleteTestingNSPolicy: %q", p))
}
Expand Down Expand Up @@ -174,7 +184,7 @@ func (o *TestFrameworkOptions) Complete(args []string) error {
return err
}

o.DeleteTestingNSPolicy = framework.DeleteTestingNSPolicyType(o.DeleteTestingNSPolicyUntyped)
o.CleanupPolicy = framework.CleanupPolicyType(o.CleanupNSPolicyUntyped)

// Trim spaces so we can reason later if the dir is set or not
o.ArtifactsDir = strings.TrimSpace(o.ArtifactsDir)
Expand Down Expand Up @@ -216,13 +226,13 @@ func (o *TestFrameworkOptions) Complete(args []string) error {
RestConfigs: slices.ConvertSlice(o.ClientConfigs, func(cc genericclioptions.ClientConfig) *rest.Config {
return cc.RestConfig
}),
ArtifactsDir: o.ArtifactsDir,
DeleteTestingNSPolicy: o.DeleteTestingNSPolicy,
ScyllaClusterOptions: o.scyllaClusterOptions,
ObjectStorageType: o.objectStorageType,
ObjectStorageBucket: o.ObjectStorageBucket,
GCSServiceAccountKey: o.gcsServiceAccountKey,
S3CredentialsFile: o.s3CredentialsFile,
ArtifactsDir: o.ArtifactsDir,
CleanupPolicy: o.CleanupPolicy,
ScyllaClusterOptions: o.scyllaClusterOptions,
ObjectStorageType: o.objectStorageType,
ObjectStorageBucket: o.ObjectStorageBucket,
GCSServiceAccountKey: o.gcsServiceAccountKey,
S3CredentialsFile: o.s3CredentialsFile,
}

if o.IngressController != nil {
Expand Down
8 changes: 3 additions & 5 deletions pkg/naming/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import (
)

func ManualRef(namespace, name string) string {
if len(namespace) == 0 {
return name
}
return fmt.Sprintf("%s/%s", namespace, name)
}

func ObjRef(obj metav1.Object) string {
namespace := obj.GetNamespace()
if len(namespace) == 0 {
return obj.GetName()
}

return ManualRef(obj.GetNamespace(), obj.GetName())
}

Expand Down
228 changes: 228 additions & 0 deletions test/e2e/framework/cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package framework

import (
"context"
"fmt"
"path/filepath"

g "github.com/onsi/ginkgo/v2"
o "github.com/onsi/gomega"
"github.com/scylladb/scylla-operator/pkg/gather/collect"
"github.com/scylladb/scylla-operator/pkg/naming"
"github.com/scylladb/scylla-operator/pkg/pointer"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
cacheddiscovery "k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2"
)

type CleanupInterface interface {
Print(ctx context.Context)
Collect(ctx context.Context, artifactsDir string, ginkgoNamespace string)
Cleanup(ctx context.Context)
}

type NamespaceCleaner struct {
Client kubernetes.Interface
DynamicClient dynamic.Interface
NS *corev1.Namespace
}

var _ CleanupInterface = &NamespaceCleaner{}

func (nd *NamespaceCleaner) Print(ctx context.Context) {
// Print events if the test failed.
if g.CurrentSpecReport().Failed() {
By(fmt.Sprintf("Collecting events from namespace %q.", nd.NS.Name))
DumpEventsInNamespace(ctx, nd.Client, nd.NS.Name)
}
}

func (nd *NamespaceCleaner) Collect(ctx context.Context, artifactsDir string, _ string) {
By(fmt.Sprintf("Collecting dumps from namespace %q.", nd.NS.Name))

err := DumpNamespace(ctx, cacheddiscovery.NewMemCacheClient(nd.Client.Discovery()), nd.DynamicClient, nd.Client.CoreV1(), artifactsDir, nd.NS.Name)
o.Expect(err).NotTo(o.HaveOccurred())
}

func (nd *NamespaceCleaner) Cleanup(ctx context.Context) {
By("Destroying namespace %q.", nd.NS.Name)
err := nd.Client.CoreV1().Namespaces().Delete(
ctx,
nd.NS.Name,
metav1.DeleteOptions{
GracePeriodSeconds: pointer.Ptr[int64](0),
PropagationPolicy: pointer.Ptr(metav1.DeletePropagationForeground),
Preconditions: &metav1.Preconditions{
UID: &nd.NS.UID,
},
},
)
o.Expect(err).NotTo(o.HaveOccurred())

// We have deleted only the namespace object, but it can still there with deletionTimestamp set.
By("Waiting for namespace %q to be removed.", nd.NS.Name)
err = WaitForObjectDeletion(ctx, nd.DynamicClient, corev1.SchemeGroupVersion.WithResource("namespaces"), "", nd.NS.Name, &nd.NS.UID)
o.Expect(err).NotTo(o.HaveOccurred())
klog.InfoS("Namespace removed.", "Namespace", nd.NS.Name)
}

type RestoreStrategy string

const (
RestoreStrategyRecreate RestoreStrategy = "Recreate"
RestoreStrategyUpdate RestoreStrategy = "Update"
)

type RestoringCleaner struct {
client kubernetes.Interface
dynamicClient dynamic.Interface
resourceInfo collect.ResourceInfo
object *unstructured.Unstructured
strategy RestoreStrategy
}

var _ CleanupInterface = &RestoringCleaner{}

func NewRestoringCleaner(ctx context.Context, client kubernetes.Interface, dynamicClient dynamic.Interface, resourceInfo collect.ResourceInfo, namespace string, name string, strategy RestoreStrategy) *RestoringCleaner {
g.By(fmt.Sprintf("Snapshotting object %s %q", resourceInfo.Resource, naming.ManualRef(namespace, name)))

if resourceInfo.Scope.Name() == meta.RESTScopeNameNamespace {
o.Expect(namespace).NotTo(o.BeEmpty())
}

obj, err := dynamicClient.Resource(resourceInfo.Resource).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
klog.InfoS("No existing object found", "GVR", resourceInfo.Resource, "Instance", naming.ManualRef(namespace, name))
obj = &unstructured.Unstructured{
Object: map[string]interface{}{},
}
obj.SetNamespace(namespace)
obj.SetName(name)
obj.SetUID("")
} else {
o.Expect(err).NotTo(o.HaveOccurred())
klog.InfoS("Snapshotted object", "GVR", resourceInfo.Resource, "Instance", naming.ManualRef(namespace, name), "UID", obj.GetUID())
}

return &RestoringCleaner{
client: client,
dynamicClient: dynamicClient,
resourceInfo: resourceInfo,
object: obj,
strategy: strategy,
}
}

func (r *RestoringCleaner) getCleansedObject() *unstructured.Unstructured {
obj := r.object.DeepCopy()
obj.SetResourceVersion("")
obj.SetUID("")
obj.SetCreationTimestamp(metav1.Time{})
obj.SetDeletionTimestamp(nil)
return obj
}

func (r *RestoringCleaner) Print(ctx context.Context) {}

func (r *RestoringCleaner) Collect(ctx context.Context, clusterArtifactsDir string, ginkgoNamespace string) {
artifactsDir := clusterArtifactsDir
if len(artifactsDir) != 0 && r.resourceInfo.Scope.Name() == meta.RESTScopeNameRoot {
// We have to prevent global object dumps being overwritten with each "It" block.
artifactsDir = filepath.Join(artifactsDir, "cluster-scoped-per-ns", ginkgoNamespace)
}

By(fmt.Sprintf("Collecting global %s %q for namespace %q.", r.resourceInfo.Resource, naming.ObjRef(r.object), ginkgoNamespace))

err := DumpResource(
ctx,
r.client.Discovery(),
r.dynamicClient,
r.client.CoreV1(),
artifactsDir,
&r.resourceInfo,
r.object.GetNamespace(),
r.object.GetName(),
)
if apierrors.IsNotFound(err) {
klog.V(2).InfoS("Skipping object collection because it no longer exists", "Ref", naming.ObjRef(r.object), "Resource", r.resourceInfo.Resource)
} else {
o.Expect(err).NotTo(o.HaveOccurred())
}
}

func (r *RestoringCleaner) DeleteObject(ctx context.Context, ignoreNotFound bool) {
By("Deleting object %s %q.", r.resourceInfo.Resource, naming.ObjRef(r.object))
err := r.dynamicClient.Resource(r.resourceInfo.Resource).Namespace(r.object.GetNamespace()).Delete(
ctx,
r.object.GetName(),
metav1.DeleteOptions{
GracePeriodSeconds: pointer.Ptr[int64](0),
PropagationPolicy: pointer.Ptr(metav1.DeletePropagationForeground),
},
)
if apierrors.IsNotFound(err) && ignoreNotFound {
return
}
o.Expect(err).NotTo(o.HaveOccurred())

// We have deleted only the object, but it can still be there with deletionTimestamp set.
By("Waiting for object %s %q to be removed.", r.resourceInfo.Resource, naming.ObjRef(r.object))
err = WaitForObjectDeletion(ctx, r.dynamicClient, r.resourceInfo.Resource, r.object.GetNamespace(), r.object.GetName(), nil)
o.Expect(err).NotTo(o.HaveOccurred())
By("Object %s %q has been removed.", r.resourceInfo.Resource, naming.ObjRef(r.object))
}

func (r *RestoringCleaner) RecreateObject(ctx context.Context) {
r.DeleteObject(ctx, true)

_, err := r.dynamicClient.Resource(r.resourceInfo.Resource).Namespace(r.object.GetNamespace()).Create(ctx, r.getCleansedObject(), metav1.CreateOptions{})
o.Expect(err).NotTo(o.HaveOccurred())
}

func (r *RestoringCleaner) ReplaceObject(ctx context.Context) {
var err error
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
var freshObj *unstructured.Unstructured
freshObj, err = r.dynamicClient.Resource(r.resourceInfo.Resource).Namespace(r.object.GetNamespace()).Get(ctx, r.object.GetName(), metav1.GetOptions{})
if apierrors.IsNotFound(err) {
_, err = r.dynamicClient.Resource(r.resourceInfo.Resource).Namespace(r.object.GetNamespace()).Create(ctx, r.getCleansedObject(), metav1.CreateOptions{})
return err
}

obj := r.getCleansedObject()
obj.SetResourceVersion(freshObj.GetResourceVersion())

o.Expect(err).NotTo(o.HaveOccurred())
_, err = r.dynamicClient.Resource(r.resourceInfo.Resource).Namespace(obj.GetNamespace()).Update(ctx, obj, metav1.UpdateOptions{})
return err
})
o.Expect(err).NotTo(o.HaveOccurred())
}

func (r *RestoringCleaner) RestoreObject(ctx context.Context) {
By("Restoring original object %s %q.", r.resourceInfo.Resource, naming.ObjRef(r.object))
switch r.strategy {
case RestoreStrategyRecreate:
r.RecreateObject(ctx)
case RestoreStrategyUpdate:
r.ReplaceObject(ctx)
default:
g.Fail(fmt.Sprintf("unexpected strategy %q", r.strategy))
}
}

func (r *RestoringCleaner) Cleanup(ctx context.Context) {
if len(r.object.GetUID()) == 0 {
r.DeleteObject(ctx, true)
return
}

r.RestoreObject(ctx)
}
Loading

0 comments on commit 2d8f761

Please sign in to comment.