package k8s import ( "context" "fmt" "strconv" "strings" "time" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func (c *Client) copySecret(ctx context.Context, srcNS, srcName, dstNS, dstName string, labels map[string]string) (*corev1.Secret, error) { secret, err := c.Clientset.CoreV1().Secrets(srcNS).Get(ctx, srcName, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("read secret %s/%s: %w", srcNS, srcName, err) } copy := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: dstName, Namespace: dstNS, Labels: labels, }, Type: secret.Type, Data: secret.Data, } created, err := c.Clientset.CoreV1().Secrets(dstNS).Create(ctx, copy, metav1.CreateOptions{}) if err != nil { return nil, fmt.Errorf("create secret %s/%s: %w", dstNS, dstName, err) } return created, nil } func (c *Client) bindSecretToJob(ctx context.Context, namespace, secretName string, job *batchv1.Job) error { secret, err := c.Clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{}) if err != nil { return err } controller := true secret.OwnerReferences = append(secret.OwnerReferences, metav1.OwnerReference{ APIVersion: "batch/v1", Kind: "Job", Name: job.Name, UID: job.UID, Controller: &controller, }) _, err = c.Clientset.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}) return err } func jobName(action, suffix string) string { base := sanitizeName(fmt.Sprintf("soteria-%s-%s", action, suffix)) timestamp := time.Now().UTC().Format("20060102-150405") name := fmt.Sprintf("%s-%s", base, timestamp) if len(name) <= 63 { return name } trimmed := base maxBase := 63 - len(timestamp) - 1 if maxBase < 1 { maxBase = 1 } if len(trimmed) > maxBase { trimmed = trimmed[:maxBase] } return fmt.Sprintf("%s-%s", trimmed, timestamp) } func sanitizeName(value string) string { value = strings.ToLower(value) value = strings.ReplaceAll(value, "_", "-") value = strings.ReplaceAll(value, ".", "-") value = strings.ReplaceAll(value, " ", "-") value = strings.Trim(value, "-") return value } func dedupeEnabled(raw *bool) bool { if raw == nil { return true } return *raw } func keepLastWithDefault(raw *int) int { if raw == nil { return 0 } if *raw < 0 { return 0 } return *raw } func resticRepositoryForBackup(base, namespace, pvc string, dedupe bool) string { if dedupe { return strings.TrimSpace(base) } ns := sanitizeRepositorySegment(namespace) pvcName := sanitizeRepositorySegment(pvc) suffix := strings.Trim(strings.Join([]string{"isolated", ns, pvcName}, "/"), "/") return appendRepositoryPath(base, suffix) } func sanitizeRepositorySegment(value string) string { sanitized := sanitizeName(value) if sanitized == "" { return "unknown" } return sanitized } func appendRepositoryPath(base, suffix string) string { base = strings.TrimSpace(base) suffix = strings.Trim(suffix, "/") if base == "" || suffix == "" { return base } backendPrefix := "" location := base if idx := strings.Index(base, ":"); idx > 0 { backendPrefix = base[:idx+1] location = base[idx+1:] } location = strings.TrimRight(location, "/") if location == "" { return base } return backendPrefix + location + "/" + suffix } func parseBoolWithDefault(raw string, fallback bool) bool { value := strings.ToLower(strings.TrimSpace(raw)) if value == "" { return fallback } switch value { case "1", "true", "yes", "on": return true case "0", "false", "no", "off": return false default: return fallback } } func parseIntWithDefault(raw string, fallback int) int { parsed, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil { return fallback } if parsed < 0 { return fallback } return parsed }