150 lines
6.2 KiB
Go
150 lines
6.2 KiB
Go
package k8s
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
dynamicfake "k8s.io/client-go/dynamic/fake"
|
|
k8stesting "k8s.io/client-go/testing"
|
|
)
|
|
|
|
func TestGetLonghornBackupBySnapshotCoversLookupPaths(t *testing.T) {
|
|
matchingOlder := longhornBackupObject("backup-old", "snap-a", "Completed", "", 100, "2026-04-20T10:00:00Z")
|
|
matchingNewer := longhornBackupObject("backup-new", "snap-a", "Error", "storage cap exceeded", 0, "2026-04-20T11:00:00Z")
|
|
other := longhornBackupObject("backup-other", "snap-b", "Completed", "", 100, "2026-04-20T12:00:00Z")
|
|
client := &Client{Dynamic: newLonghornBackupDynamic(matchingOlder, matchingNewer, other)}
|
|
|
|
backup, found, err := client.GetLonghornBackupBySnapshot(context.Background(), "snap-a")
|
|
if err != nil || !found {
|
|
t.Fatalf("expected matching backup, found=%v err=%v", found, err)
|
|
}
|
|
if backup.Name != "backup-new" || backup.State != "Error" || backup.Error != "storage cap exceeded" || backup.Progress != 0 {
|
|
t.Fatalf("expected newest matching backup with status fields, got %#v", backup)
|
|
}
|
|
|
|
if _, found, err := client.GetLonghornBackupBySnapshot(context.Background(), "missing"); err != nil || found {
|
|
t.Fatalf("expected missing snapshot to be cleanly absent, found=%v err=%v", found, err)
|
|
}
|
|
}
|
|
|
|
func TestGetLonghornBackupBySnapshotValidatesClientAndInput(t *testing.T) {
|
|
client := &Client{Dynamic: newLonghornBackupDynamic()}
|
|
if _, _, err := client.GetLonghornBackupBySnapshot(context.Background(), " "); err == nil || !strings.Contains(err.Error(), "snapshot name is required") {
|
|
t.Fatalf("expected snapshot validation error, got %v", err)
|
|
}
|
|
|
|
client.Dynamic = nil
|
|
if _, _, err := client.GetLonghornBackupBySnapshot(context.Background(), "snap-a"); err == nil || !strings.Contains(err.Error(), "dynamic Kubernetes client is unavailable") {
|
|
t.Fatalf("expected missing dynamic client error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetLonghornBackupBySnapshotWrapsListErrorAndFallbackTimestamp(t *testing.T) {
|
|
client := &Client{Dynamic: newLonghornBackupDynamic(longhornBackupObject("backup-a", "snap-a", "Completed", "", 100, ""))}
|
|
|
|
backup, found, err := client.GetLonghornBackupBySnapshot(context.Background(), "snap-a")
|
|
if err != nil || !found {
|
|
t.Fatalf("expected fallback timestamp backup, found=%v err=%v", found, err)
|
|
}
|
|
if backup.CreatedAt == "" || backup.SnapshotName != "snap-a" {
|
|
t.Fatalf("expected fallback timestamp and snapshot name, got %#v", backup)
|
|
}
|
|
|
|
failingDynamic := newLonghornBackupDynamic()
|
|
failingDynamic.PrependReactor("list", "backups", func(k8stesting.Action) (bool, runtime.Object, error) {
|
|
return true, nil, errors.New("list exploded")
|
|
})
|
|
client.Dynamic = failingDynamic
|
|
if _, _, err := client.GetLonghornBackupBySnapshot(context.Background(), "snap-a"); err == nil || !strings.Contains(err.Error(), "list exploded") {
|
|
t.Fatalf("expected wrapped list error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListLonghornBackupsFiltersSoteriaLabelsAndSorts(t *testing.T) {
|
|
older := longhornBackupObject("backup-older", "snap-a", "Completed", "", 100, "2026-04-20T10:00:00Z")
|
|
newer := longhornBackupObject("backup-newer", "snap-b", "Error", "storage cap exceeded", 0, "2026-04-20T11:00:00Z")
|
|
setLonghornBackupLabels(newer, "ops", "cache")
|
|
unlabeled := longhornBackupObject("backup-unlabeled", "snap-c", "Completed", "", 100, "2026-04-20T12:00:00Z")
|
|
_ = unstructured.SetNestedStringMap(unlabeled.Object, map[string]string{}, "spec", "labels")
|
|
|
|
client := &Client{Dynamic: newLonghornBackupDynamic(older, newer, unlabeled)}
|
|
backups, err := client.ListLonghornBackups(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("list Longhorn backups: %v", err)
|
|
}
|
|
if len(backups) != 2 {
|
|
t.Fatalf("expected two Soteria-labeled backups, got %#v", backups)
|
|
}
|
|
if backups[0].Name != "backup-newer" || backups[0].Namespace != "ops" || backups[0].PVC != "cache" {
|
|
t.Fatalf("expected newest ops/cache backup first, got %#v", backups[0])
|
|
}
|
|
if backups[1].Name != "backup-older" || backups[1].Namespace != "apps" || backups[1].PVC != "data" {
|
|
t.Fatalf("expected older apps/data backup second, got %#v", backups[1])
|
|
}
|
|
}
|
|
|
|
func TestListLonghornBackupsValidatesClientAndWrapsListError(t *testing.T) {
|
|
client := &Client{}
|
|
if _, err := client.ListLonghornBackups(context.Background()); err == nil || !strings.Contains(err.Error(), "dynamic Kubernetes client is unavailable") {
|
|
t.Fatalf("expected missing dynamic client error, got %v", err)
|
|
}
|
|
|
|
failingDynamic := newLonghornBackupDynamic()
|
|
failingDynamic.PrependReactor("list", "backups", func(k8stesting.Action) (bool, runtime.Object, error) {
|
|
return true, nil, errors.New("list exploded")
|
|
})
|
|
client.Dynamic = failingDynamic
|
|
if _, err := client.ListLonghornBackups(context.Background()); err == nil || !strings.Contains(err.Error(), "list exploded") {
|
|
t.Fatalf("expected wrapped list error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func newLonghornBackupDynamic(objects ...runtime.Object) *dynamicfake.FakeDynamicClient {
|
|
return dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
|
|
runtime.NewScheme(),
|
|
map[schema.GroupVersionResource]string{longhornBackupGVR: "BackupList"},
|
|
objects...,
|
|
)
|
|
}
|
|
|
|
func setLonghornBackupLabels(item *unstructured.Unstructured, namespace, pvc string) {
|
|
_ = unstructured.SetNestedStringMap(item.Object, map[string]string{
|
|
"soteria.bstein.dev/namespace": namespace,
|
|
"soteria.bstein.dev/pvc": pvc,
|
|
}, "spec", "labels")
|
|
}
|
|
|
|
func longhornBackupObject(name, snapshotName, state, errorMessage string, progress int64, createdAt string) *unstructured.Unstructured {
|
|
item := &unstructured.Unstructured{Object: map[string]any{
|
|
"apiVersion": "longhorn.io/v1beta2",
|
|
"kind": "Backup",
|
|
"metadata": map[string]any{
|
|
"name": name,
|
|
"namespace": longhornSystemNamespace,
|
|
},
|
|
"spec": map[string]any{
|
|
"snapshotName": snapshotName,
|
|
"labels": map[string]any{
|
|
"soteria.bstein.dev/namespace": "apps",
|
|
"soteria.bstein.dev/pvc": "data",
|
|
},
|
|
},
|
|
"status": map[string]any{
|
|
"state": state,
|
|
"error": errorMessage,
|
|
"progress": progress,
|
|
},
|
|
}}
|
|
if createdAt != "" {
|
|
_ = unstructured.SetNestedField(item.Object, createdAt, "status", "backupCreatedAt")
|
|
}
|
|
item.SetCreationTimestamp(metav1.Now())
|
|
return item
|
|
}
|