k8up-io/k8up

View on GitHub
envtest/envsuite.go

Summary

Maintainability
A
2 hrs
Test Coverage
//go:build integration

package envtest

import (
    "context"
    "os"
    "path/filepath"
    "regexp"
    "strings"

    "github.com/go-logr/logr"
    "github.com/go-logr/zapr"
    "github.com/stretchr/testify/suite"
    "go.uber.org/zap/zaptest"
    appsv1 "k8s.io/api/apps/v1"
    batchv1 "k8s.io/api/batch/v1"
    corev1 "k8s.io/api/core/v1"
    rbacv1 "k8s.io/api/rbac/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/runtime"
    "k8s.io/apimachinery/pkg/types"
    "k8s.io/apimachinery/pkg/util/rand"
    "k8s.io/apimachinery/pkg/util/validation"
    "k8s.io/client-go/rest"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/envtest"
    "sigs.k8s.io/controller-runtime/pkg/log"
    // +kubebuilder:scaffold:imports

    k8upv1 "github.com/k8up-io/k8up/v2/api/v1"
    "github.com/k8up-io/k8up/v2/operator/cfg"
)

var InvalidNSNameCharacters = regexp.MustCompile("[^a-z0-9-]")

type Suite struct {
    suite.Suite

    NS     string
    Client client.Client
    Config *rest.Config
    Env    *envtest.Environment
    Logger logr.Logger
    Ctx    context.Context
    Scheme *runtime.Scheme
}

func (ts *Suite) SetupSuite() {
    ts.Logger = zapr.NewLogger(zaptest.NewLogger(ts.T()))
    log.SetLogger(ts.Logger)
    cfg.Config = defaultConfiguration()

    ts.Ctx = context.Background()

    envtestAssets, ok := os.LookupEnv("KUBEBUILDER_ASSETS")
    if !ok {
        ts.FailNow("The environment variable KUBEBUILDER_ASSETS is undefined. Configure your IDE to set this variable when running the integration test.")
    }

    info, err := os.Stat(envtestAssets)
    absEnvtestAssets, _ := filepath.Abs(envtestAssets)
    ts.Require().NoErrorf(err, "'%s' does not seem to exist. Check KUBEBUILDER_ASSETS and make sure you run `make integration-test` before you run this test in your IDE.", absEnvtestAssets)
    ts.Require().Truef(info.IsDir(), "'%s' does not seem to be a directory. Check KUBEBUILDER_ASSETS and make sure you run `make integration-test` before you run this test in your IDE.", absEnvtestAssets)

    crds := filepath.Join(Root, "config", "crd", "apiextensions.k8s.io", "v1")
    absCrds, _ := filepath.Abs(crds)
    info, err = os.Stat(crds)
    ts.Require().NoErrorf(err, "'%s' does not seem to exist. Make sure to set the working directory to the project root.", absCrds)
    ts.Require().Truef(info.IsDir(), "'%s' does not seem to be a directory. Make sure to set the working directory to the project root.", absCrds)

    ts.Logger.Info("envtest directories", "crd", absCrds, "binary assets", absEnvtestAssets)

    testEnv := &envtest.Environment{
        ErrorIfCRDPathMissing: true,
        CRDDirectoryPaths:     []string{crds},
        BinaryAssetsDirectory: envtestAssets,
    }

    config, err := testEnv.Start()
    ts.Require().NoError(err)
    ts.Require().NotNil(config)

    registerCRDs(ts)

    k8sClient, err := client.New(config, client.Options{
        Scheme: ts.Scheme,
    })
    ts.Require().NoError(err)
    ts.Require().NotNil(k8sClient)

    ts.Env = testEnv
    ts.Config = config
    ts.Client = k8sClient
}

func registerCRDs(ts *Suite) {
    ts.Scheme = runtime.NewScheme()
    ts.Require().NoError(appsv1.AddToScheme(ts.Scheme))
    ts.Require().NoError(batchv1.AddToScheme(ts.Scheme))
    ts.Require().NoError(corev1.AddToScheme(ts.Scheme))
    ts.Require().NoError(k8upv1.AddToScheme(ts.Scheme))
    ts.Require().NoError(rbacv1.AddToScheme(ts.Scheme))

    // +kubebuilder:scaffold:scheme
}

func (ts *Suite) TearDownSuite() {
    err := ts.Env.Stop()
    ts.Require().NoErrorf(err, "error while stopping test environment")
    ts.Logger.Info("test environment stopped")
}

// NewNS instantiates a new Namespace object with the given name.
func (ts *Suite) NewNS(nsName string) *corev1.Namespace {
    ts.Assert().Emptyf(validation.IsDNS1123Label(nsName), "'%s' does not appear to be a valid name for a namespace", nsName)

    return &corev1.Namespace{
        ObjectMeta: metav1.ObjectMeta{
            Name: nsName,
        },
    }
}

// EnsureNS creates a new Namespace object using Suite.Client.
func (ts *Suite) EnsureNS(nsName string) {
    ns := ts.NewNS(nsName)
    ts.T().Logf("creating namespace '%s'", nsName)
    ts.Require().NoError(ts.Client.Create(ts.Ctx, ns))
}

// EnsureResources ensures that the given resources are existing in the suite. Each error will fail the test.
func (ts *Suite) EnsureResources(resources ...client.Object) {
    for _, resource := range resources {
        ts.T().Logf("creating resource '%s/%s'", resource.GetNamespace(), resource.GetName())
        ts.Require().NoError(ts.Client.Create(ts.Ctx, resource))
    }
}

// UpdateResources ensures that the given resources are updated in the suite. Each error will fail the test.
func (ts *Suite) UpdateResources(resources ...client.Object) {
    for _, resource := range resources {
        ts.T().Logf("updating resource '%s/%s'", resource.GetNamespace(), resource.GetName())
        ts.Require().NoError(ts.Client.Update(ts.Ctx, resource))
    }
}

// UpdateStatus ensures that the Status property of the given resources are updated in the suite. Each error will fail the test.
func (ts *Suite) UpdateStatus(resources ...client.Object) {
    for _, resource := range resources {
        ts.T().Logf("updating status '%s/%s'", resource.GetNamespace(), resource.GetName())
        ts.Require().NoError(ts.Client.Status().Update(ts.Ctx, resource))
    }
}

// SetCondition sets the given condition and updates the status.
// Errors will fail the test.
func (ts *Suite) SetCondition(
    resource client.Object,
    conditions *[]metav1.Condition,
    cType k8upv1.ConditionType,
    status metav1.ConditionStatus,
    reason k8upv1.ConditionReason) {

    meta.SetStatusCondition(conditions, metav1.Condition{
        Type:    cType.String(),
        Status:  status,
        Reason:  reason.String(),
        Message: reason.String(),
    })
    ts.UpdateStatus(resource)
}

// DeleteResources deletes the given resources are updated from the suite. Each error will fail the test.
func (ts *Suite) DeleteResources(resources ...client.Object) {
    for _, resource := range resources {
        ts.T().Logf("deleting '%s/%s'", resource.GetNamespace(), resource.GetName())
        ts.Require().NoError(ts.Client.Delete(ts.Ctx, resource))
    }
}

// FetchResource fetches the given object name and stores the result in the given object.
// Test fails on errors.
func (ts *Suite) FetchResource(name types.NamespacedName, object client.Object) {
    ts.Require().NoError(ts.Client.Get(ts.Ctx, name, object))
}

// FetchResources fetches resources and puts the items into the given list with the given list options.
// Test fails on errors.
func (ts *Suite) FetchResources(objectList client.ObjectList, opts ...client.ListOption) {
    ts.Require().NoError(ts.Client.List(ts.Ctx, objectList, opts...))
}

// SetupTest is invoked just before every test starts
func (ts *Suite) SetupTest() {
    ts.NS = rand.String(8)
    ts.EnsureNS(ts.NS)
}

// SanitizeNameForNS first converts the given name to lowercase using strings.ToLower
// and then remove all characters but `a-z` (only lower case), `0-9` and the `-` (dash).
func (ts *Suite) SanitizeNameForNS(name string) string {
    return InvalidNSNameCharacters.ReplaceAllString(strings.ToLower(name), "")
}

// IsResourceExisting tries to fetch the given resource and returns true if it exists.
// It will consider still-existing object with a deletion timestamp as non-existing.
// Any other errors will fail the test.
func (ts *Suite) IsResourceExisting(ctx context.Context, obj client.Object) bool {
    err := ts.Client.Get(ctx, k8upv1.MapToNamespacedName(obj), obj)
    if apierrors.IsNotFound(err) {
        return false
    }
    ts.Assert().NoError(err)
    return obj.GetDeletionTimestamp() == nil
}

// defaultConfiguration retrieves the config with sane defaults
func defaultConfiguration() *cfg.Configuration {
    return &cfg.Configuration{
        MountPath:                        "/data",
        BackupAnnotation:                 "k8up.io/backup",
        BackupCommandAnnotation:          "k8up.io/backupcommand",
        FileExtensionAnnotation:          "k8up.io/file-extension",
        ServiceAccount:                   "pod-executor",
        BackupCheckSchedule:              "0 0 * * 0",
        GlobalFailedJobsHistoryLimit:     3,
        GlobalSuccessfulJobsHistoryLimit: 3,
        BackupImage:                      "ghcr.io/k8up-io/k8up:latest",
        BackupCommandRestic:              []string{"/usr/local/bin/k8up", "restic"},
        PodExecRoleName:                  "pod-executor",
        RestartPolicy:                    "OnFailure",
        MetricsBindAddress:               ":8080",
        PodFilter:                        "backupPod=true",
        EnableLeaderElection:             true,
        PodVarDir:                        "/k8up",
    }
}