soteria/internal/k8s/longhorn_backups.go

121 lines
4.0 KiB
Go

package k8s
import (
"context"
"fmt"
"sort"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const longhornSystemNamespace = "longhorn-system"
var longhornBackupGVR = schema.GroupVersionResource{
Group: "longhorn.io",
Version: "v1beta2",
Resource: "backups",
}
// LonghornBackupSummary is the subset of the Longhorn Backup CRD that Soteria
// needs while waiting for an async backup to reach a terminal state.
type LonghornBackupSummary struct {
Name string
SnapshotName string
Namespace string
PVC string
State string
Error string
Progress int64
CreatedAt string
}
// ListLonghornBackups returns Soteria-owned Longhorn Backup CRDs.
func (c *Client) ListLonghornBackups(ctx context.Context) ([]LonghornBackupSummary, error) {
if c.Dynamic == nil {
return nil, fmt.Errorf("dynamic Kubernetes client is unavailable")
}
list, err := c.Dynamic.Resource(longhornBackupGVR).Namespace(longhornSystemNamespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("list Longhorn backups: %w", err)
}
items := make([]LonghornBackupSummary, 0, len(list.Items))
for _, item := range list.Items {
summary := summarizeLonghornBackup(item)
if summary.Namespace != "" && summary.PVC != "" {
items = append(items, summary)
}
}
sortLonghornBackupsNewestFirst(items)
return items, nil
}
// GetLonghornBackupBySnapshot returns the newest Longhorn Backup CRD for a snapshot.
func (c *Client) GetLonghornBackupBySnapshot(ctx context.Context, snapshotName string) (LonghornBackupSummary, bool, error) {
snapshotName = strings.TrimSpace(snapshotName)
if snapshotName == "" {
return LonghornBackupSummary{}, false, fmt.Errorf("snapshot name is required")
}
if c.Dynamic == nil {
return LonghornBackupSummary{}, false, fmt.Errorf("dynamic Kubernetes client is unavailable")
}
list, err := c.Dynamic.Resource(longhornBackupGVR).Namespace(longhornSystemNamespace).List(ctx, metav1.ListOptions{})
if err != nil {
return LonghornBackupSummary{}, false, fmt.Errorf("list Longhorn backups: %w", err)
}
matches := make([]LonghornBackupSummary, 0, len(list.Items))
for _, item := range list.Items {
summary := summarizeLonghornBackup(item)
if summary.SnapshotName == snapshotName {
matches = append(matches, summary)
}
}
if len(matches) == 0 {
return LonghornBackupSummary{}, false, nil
}
sortLonghornBackupsNewestFirst(matches)
return matches[0], true, nil
}
func summarizeLonghornBackup(item unstructured.Unstructured) LonghornBackupSummary {
snapshotName, _, _ := unstructured.NestedString(item.Object, "spec", "snapshotName")
state, _, _ := unstructured.NestedString(item.Object, "status", "state")
errorMessage, _, _ := unstructured.NestedString(item.Object, "status", "error")
progress, _, _ := unstructured.NestedInt64(item.Object, "status", "progress")
specLabels, _, _ := unstructured.NestedStringMap(item.Object, "spec", "labels")
createdAt, _, _ := unstructured.NestedString(item.Object, "status", "backupCreatedAt")
if strings.TrimSpace(createdAt) == "" {
createdAt, _, _ = unstructured.NestedString(item.Object, "status", "snapshotCreatedAt")
}
if strings.TrimSpace(createdAt) == "" {
createdAt = item.GetCreationTimestamp().UTC().Format("2006-01-02T15:04:05Z")
}
return LonghornBackupSummary{
Name: item.GetName(),
SnapshotName: strings.TrimSpace(snapshotName),
Namespace: strings.TrimSpace(specLabels["soteria.bstein.dev/namespace"]),
PVC: strings.TrimSpace(specLabels["soteria.bstein.dev/pvc"]),
State: strings.TrimSpace(state),
Error: strings.TrimSpace(errorMessage),
Progress: progress,
CreatedAt: strings.TrimSpace(createdAt),
}
}
func sortLonghornBackupsNewestFirst(items []LonghornBackupSummary) {
sort.Slice(items, func(i, j int) bool {
if items[i].CreatedAt == items[j].CreatedAt {
return items[i].Name > items[j].Name
}
return items[i].CreatedAt > items[j].CreatedAt
})
}