feat: reconcile PostgreSQLInstance dependencies

This commit is contained in:
2026-09-10 18:15:50 +00:00
parent 0101de3ffa
commit 97f66669a6
9 changed files with 484 additions and 10 deletions
@@ -18,7 +18,9 @@ package controller
import (
"context"
"errors"
"reflect"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
@@ -32,7 +34,15 @@ import (
// PostgreSQLInstanceReconciler reconciles a PostgreSQLInstance object
type PostgreSQLInstanceReconciler struct {
client.Client
Scheme *runtime.Scheme
Scheme *runtime.Scheme
Initializer PostgreSQLInstanceInitializer
Timeout time.Duration
}
// PostgreSQLInstanceInitializer is the external dependency boundary used by the Instance state machine.
type PostgreSQLInstanceInitializer interface {
Validate(context.Context, *databasev1alpha1.PostgreSQLInstance) (string, error)
InitializeRegistry(context.Context, *databasev1alpha1.PostgreSQLInstance) (string, error)
}
// +kubebuilder:rbac:groups=database.ddupan.top,resources=postgresqlinstances,verbs=get;list;watch;create;update;patch;delete
@@ -62,6 +72,12 @@ func (r *PostgreSQLInstanceReconciler) Reconcile(ctx context.Context, req ctrl.R
setReconcilingCondition(&instance.Status.Conditions, instance.Generation, phaseResult.reconcilingMessage)
}
var reconcileErr error
if r.Initializer != nil && instance.DeletionTimestamp.IsZero() &&
before.Status.Phase != "" && before.Status.Phase != databasev1alpha1.PostgreSQLInstancePhasePending {
reconcileErr = r.reconcileDependencies(ctx, instance)
}
if !reflect.DeepEqual(before.Status, instance.Status) {
if err := r.Status().Patch(ctx, instance, client.MergeFrom(before)); err != nil {
if apierrors.IsConflict(err) {
@@ -71,7 +87,56 @@ func (r *PostgreSQLInstanceReconciler) Reconcile(ctx context.Context, req ctrl.R
}
}
return ctrl.Result{}, nil
return ctrl.Result{}, reconcileErr
}
func (r *PostgreSQLInstanceReconciler) reconcileDependencies(
ctx context.Context,
instance *databasev1alpha1.PostgreSQLInstance,
) error {
if r.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.Timeout)
defer cancel()
}
if instance.Status.Phase == databasev1alpha1.PostgreSQLInstancePhaseReady &&
instance.Status.ObservedGeneration != instance.Generation {
instance.Status.Phase = databasev1alpha1.PostgreSQLInstancePhaseValidating
setReconcilingCondition(&instance.Status.Conditions, instance.Generation, "instance dependencies are being validated")
return nil
}
var version string
var err error
switch instance.Status.Phase {
case databasev1alpha1.PostgreSQLInstancePhaseValidating:
version, err = r.Initializer.Validate(ctx, instance)
if err == nil {
instance.Status.PostgreSQLVersion = version
instance.Status.Phase = databasev1alpha1.PostgreSQLInstancePhaseInitializingRegistry
setReconcilingCondition(&instance.Status.Conditions, instance.Generation, "PostgreSQL registry is being initialized")
}
case databasev1alpha1.PostgreSQLInstancePhaseInitializingRegistry:
version, err = r.Initializer.InitializeRegistry(ctx, instance)
if err == nil {
instance.Status.PostgreSQLVersion = version
instance.Status.Phase = databasev1alpha1.PostgreSQLInstancePhaseReady
instance.Status.ObservedGeneration = instance.Generation
setReadyCondition(&instance.Status.Conditions, instance.Generation, "instance dependencies are ready")
}
}
if err != nil {
instance.Status.ObservedGeneration = instance.Generation
reason := databasev1alpha1.ReasonDependencyUnavailable
var categorized interface{ ConditionReason() string }
if errors.As(err, &categorized) {
reason = categorized.ConditionReason()
}
setFailedCondition(&instance.Status.Conditions, instance.Generation, reason,
"instance dependency validation failed")
}
return err
}
// SetupWithManager sets up the controller with the Manager.
+48
View File
@@ -17,6 +17,8 @@ limitations under the License.
package controller
import (
"context"
"errors"
"time"
. "github.com/onsi/ginkgo/v2"
@@ -26,7 +28,53 @@ import (
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1"
)
type fakeInstanceInitializer struct {
validateVersion string
registryVersion string
err error
}
func (f fakeInstanceInitializer) Validate(context.Context, *databasev1alpha1.PostgreSQLInstance) (string, error) {
return f.validateVersion, f.err
}
func (f fakeInstanceInitializer) InitializeRegistry(context.Context, *databasev1alpha1.PostgreSQLInstance) (string, error) {
return f.registryVersion, f.err
}
var _ = Describe("phase handler state machines", func() {
It("advances an Instance through external validation and registry initialization", func() {
instance := &databasev1alpha1.PostgreSQLInstance{
ObjectMeta: metav1.ObjectMeta{Generation: 3},
Status: databasev1alpha1.PostgreSQLInstanceStatus{Phase: databasev1alpha1.PostgreSQLInstancePhaseValidating},
}
reconciler := &PostgreSQLInstanceReconciler{Initializer: fakeInstanceInitializer{
validateVersion: "17.6", registryVersion: "17.6",
}}
Expect(reconciler.reconcileDependencies(context.Background(), instance)).To(Succeed())
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseInitializingRegistry))
Expect(reconciler.reconcileDependencies(context.Background(), instance)).To(Succeed())
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseReady))
Expect(instance.Status.PostgreSQLVersion).To(Equal("17.6"))
Expect(instance.Status.ObservedGeneration).To(Equal(int64(3)))
Expect(instance.Status.Conditions).To(ConsistOf(And(
HaveField("Status", metav1.ConditionTrue), HaveField("Reason", databasev1alpha1.ReasonReady),
)))
})
It("keeps the safe phase when a dependency is unavailable", func() {
instance := &databasev1alpha1.PostgreSQLInstance{
ObjectMeta: metav1.ObjectMeta{Generation: 2},
Status: databasev1alpha1.PostgreSQLInstanceStatus{Phase: databasev1alpha1.PostgreSQLInstancePhaseValidating},
}
reconciler := &PostgreSQLInstanceReconciler{Initializer: fakeInstanceInitializer{err: errors.New("unavailable")}}
Expect(reconciler.reconcileDependencies(context.Background(), instance)).To(MatchError("unavailable"))
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseValidating))
Expect(instance.Status.Conditions).To(ConsistOf(And(
HaveField("Status", metav1.ConditionFalse), HaveField("Reason", databasev1alpha1.ReasonDependencyUnavailable),
)))
})
DescribeTable("dispatches Instance phases",
func(instance *databasev1alpha1.PostgreSQLInstance, expected databasev1alpha1.PostgreSQLInstancePhase, hasMessage bool) {
result := newInstanceStateMachine().reconcile(instance)
+14
View File
@@ -32,3 +32,17 @@ func setReconcilingCondition(conditions *[]metav1.Condition, generation int64, m
Message: message,
})
}
func setReadyCondition(conditions *[]metav1.Condition, generation int64, message string) {
apiMeta.SetStatusCondition(conditions, metav1.Condition{
Type: databasev1alpha1.ConditionTypeReady, Status: metav1.ConditionTrue,
ObservedGeneration: generation, Reason: databasev1alpha1.ReasonReady, Message: message,
})
}
func setFailedCondition(conditions *[]metav1.Condition, generation int64, reason, message string) {
apiMeta.SetStatusCondition(conditions, metav1.Condition{
Type: databasev1alpha1.ConditionTypeReady, Status: metav1.ConditionFalse,
ObservedGeneration: generation, Reason: reason, Message: message,
})
}