package controller import ( "context" "crypto/rand" "encoding/hex" "fmt" "time" cephv1 "git.unkin.net/unkin/cephrgw-operator/api/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) const ( annPrefix = "backups.unkin.net/" annSchedule = annPrefix + "schedule" annDestination = annPrefix + "destination" annPurge = annPrefix + "purge-on-delete" // requeueSteady re-syncs healthy objects so drift heals without waiting for // an event. requeueSteady = 10 * time.Minute // requeueShort backs off while waiting for a dependency (e.g. the // BucketAccess to become Ready or a destination to appear). requeueShort = 30 * time.Second ) // baseReconciler holds the shared client, scheme, recorder and destinations // ConfigMap location used by both the PVC and CNPG Cluster controllers. type baseReconciler struct { client.Client Scheme *runtime.Scheme Recorder record.EventRecorder DestNamespace string DestConfigMap string } func (b *baseReconciler) resolveDest(ctx context.Context, dest string) (Destination, error) { return resolveDestination(ctx, b.Client, b.DestNamespace, b.DestConfigMap, dest) } // ensureBucketStack provisions the ObjectStoreUser, Bucket and BucketAccess for // an annotated object (all owner-referenced to it) and reports whether the // BucketAccess is Ready together with the name of its credential Secret. func (b *baseReconciler) ensureBucketStack(ctx context.Context, owner client.Object, dest Destination) (ready bool, credSecret string, err error) { ns := owner.GetNamespace() obj := owner.GetName() bName := bucketName(ns, obj) user := &cephv1.ObjectStoreUser{ObjectMeta: metav1.ObjectMeta{Name: userName(obj), Namespace: ns}} if _, err = controllerutil.CreateOrUpdate(ctx, b.Client, user, func() error { user.Spec.DisplayName = fmt.Sprintf("autobackup %s/%s", ns, obj) return controllerutil.SetControllerReference(owner, user, b.Scheme) }); err != nil { return false, "", err } bucket := &cephv1.Bucket{ObjectMeta: metav1.ObjectMeta{Name: bName, Namespace: ns}} if _, err = controllerutil.CreateOrUpdate(ctx, b.Client, bucket, func() error { bucket.Spec.OwnerRef = user.Name bucket.Spec.BucketName = bName bucket.Spec.PlacementTarget = dest.PlacementTarget bucket.Spec.Zonegroup = dest.Zonegroup bucket.Spec.Versioning = true // Keep bucket data by default; deleting the owner GCs the CRs but the // underlying objects are retained for safety. bucket.Spec.RetainOnDelete = true return controllerutil.SetControllerReference(owner, bucket, b.Scheme) }); err != nil { return false, "", err } access := &cephv1.BucketAccess{ObjectMeta: metav1.ObjectMeta{Name: accessName(obj), Namespace: ns}} if _, err = controllerutil.CreateOrUpdate(ctx, b.Client, access, func() error { access.Spec.BucketRef = bName access.Spec.Level = cephv1.AccessReadWrite access.Spec.SecretName = credSecretName(obj) return controllerutil.SetControllerReference(owner, access, b.Scheme) }); err != nil { return false, "", err } if access.Status.Phase != "Ready" || access.Status.SecretName == "" { return false, "", nil } return true, access.Status.SecretName, nil } // ensureResticSecret creates (once) an owner-referenced Secret holding a random // restic repository password under key "password". func (b *baseReconciler) ensureResticSecret(ctx context.Context, owner client.Object) (string, error) { name := resticSecretName(owner.GetName()) sec := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: owner.GetNamespace()}} _, err := controllerutil.CreateOrUpdate(ctx, b.Client, sec, func() error { sec.Type = corev1.SecretTypeOpaque if _, ok := sec.Data["password"]; !ok { pw, err := randomPassword(32) if err != nil { return err } if sec.Data == nil { sec.Data = map[string][]byte{} } sec.Data["password"] = []byte(pw) } return controllerutil.SetControllerReference(owner, sec, b.Scheme) }) return name, err } func randomPassword(n int) (string, error) { buf := make([]byte, n) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } // deleteIfExists best-effort deletes obj, ignoring a NotFound result. func deleteIfExists(ctx context.Context, c client.Client, obj client.Object) error { return client.IgnoreNotFound(c.Delete(ctx, obj)) } // bucketStackObjects lists the cephrgw CRs and credential Secret created for an // object, for explicit purge-on-delete teardown. func bucketStackObjects(ns, obj string) []client.Object { meta := func(name string) metav1.ObjectMeta { return metav1.ObjectMeta{Name: name, Namespace: ns} } return []client.Object{ &cephv1.BucketAccess{ObjectMeta: meta(accessName(obj))}, &cephv1.Bucket{ObjectMeta: meta(bucketName(ns, obj))}, &cephv1.ObjectStoreUser{ObjectMeta: meta(userName(obj))}, &corev1.Secret{ObjectMeta: meta(credSecretName(obj))}, &corev1.Secret{ObjectMeta: meta(resticSecretName(obj))}, } }