215 lines
8.7 KiB
Go
215 lines
8.7 KiB
Go
/*
|
|
Copyright 2026.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1"
|
|
)
|
|
|
|
type fakeInstanceInitializer struct {
|
|
validateVersion string
|
|
registryVersion string
|
|
err error
|
|
}
|
|
|
|
const testPostgreSQLVersion = "17.6"
|
|
|
|
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("persists validation intent before touching unavailable dependencies", func() {
|
|
instance := &databasev1alpha1.PostgreSQLInstance{}
|
|
machine := newInstanceStateMachine(fakeInstanceInitializer{err: errors.New("must not be called")})
|
|
result, err := machine.reconcile(context.Background(), instance)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(result.RequeueAfter).To(BeNumerically(">", 0))
|
|
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseValidating))
|
|
Expect(instance.Status.Conditions[0].Status).To(Equal(metav1.ConditionUnknown))
|
|
})
|
|
|
|
It("persists a new validation checkpoint when a Ready Instance generation changes", func() {
|
|
instance := &databasev1alpha1.PostgreSQLInstance{
|
|
ObjectMeta: metav1.ObjectMeta{Generation: 4},
|
|
Status: databasev1alpha1.PostgreSQLInstanceStatus{
|
|
Phase: databasev1alpha1.PostgreSQLInstancePhaseReady, ObservedGeneration: 3,
|
|
},
|
|
}
|
|
machine := newInstanceStateMachine(fakeInstanceInitializer{err: errors.New("must not be called")})
|
|
_, err := machine.reconcile(context.Background(), instance)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseValidating))
|
|
Expect(instance.Status.ObservedGeneration).To(Equal(int64(3)))
|
|
})
|
|
|
|
It("detects a dependency outage after Ready and recovers through registry initialization", func() {
|
|
instance := &databasev1alpha1.PostgreSQLInstance{
|
|
ObjectMeta: metav1.ObjectMeta{Generation: 3},
|
|
Status: databasev1alpha1.PostgreSQLInstanceStatus{
|
|
Phase: databasev1alpha1.PostgreSQLInstancePhaseReady, ObservedGeneration: 3,
|
|
},
|
|
}
|
|
machine := newInstanceStateMachine(fakeInstanceInitializer{err: errors.New("unavailable")})
|
|
_, err := machine.reconcile(context.Background(), instance)
|
|
Expect(err).To(MatchError("unavailable"))
|
|
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseValidating))
|
|
Expect(instance.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
|
|
machine.initializer = fakeInstanceInitializer{
|
|
validateVersion: testPostgreSQLVersion,
|
|
registryVersion: testPostgreSQLVersion,
|
|
}
|
|
_, err = machine.reconcile(context.Background(), instance)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseInitializingRegistry))
|
|
_, err = machine.reconcile(context.Background(), instance)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(instance.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
|
|
result, err := machine.reconcile(context.Background(), instance)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseReady))
|
|
Expect(result.RequeueAfter).To(Equal(time.Minute))
|
|
})
|
|
|
|
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},
|
|
}
|
|
machine := newInstanceStateMachine(fakeInstanceInitializer{
|
|
validateVersion: testPostgreSQLVersion,
|
|
registryVersion: testPostgreSQLVersion,
|
|
})
|
|
_, err := machine.reconcile(context.Background(), instance)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseInitializingRegistry))
|
|
_, err = machine.reconcile(context.Background(), instance)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(instance.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseReady))
|
|
Expect(instance.Status.PostgreSQLVersion).To(Equal(testPostgreSQLVersion))
|
|
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},
|
|
}
|
|
machine := newInstanceStateMachine(fakeInstanceInitializer{err: errors.New("unavailable")})
|
|
_, err := machine.reconcile(context.Background(), instance)
|
|
Expect(err).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) {
|
|
_, err := newInstanceStateMachine(nil).reconcile(context.Background(), instance)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(instance.Status.Phase).To(Equal(expected))
|
|
if hasMessage {
|
|
Expect(instance.Status.Conditions).NotTo(BeEmpty())
|
|
} else {
|
|
Expect(instance.Status.Conditions).To(BeEmpty())
|
|
}
|
|
},
|
|
Entry("starts validation from an empty checkpoint",
|
|
&databasev1alpha1.PostgreSQLInstance{},
|
|
databasev1alpha1.PostgreSQLInstancePhaseValidating,
|
|
true,
|
|
),
|
|
Entry("keeps an active phase until its handler can observe dependencies",
|
|
&databasev1alpha1.PostgreSQLInstance{Status: databasev1alpha1.PostgreSQLInstanceStatus{
|
|
Phase: databasev1alpha1.PostgreSQLInstancePhaseInitializingRegistry,
|
|
}},
|
|
databasev1alpha1.PostgreSQLInstancePhaseInitializingRegistry,
|
|
false,
|
|
),
|
|
Entry("routes deletion independently of the previous phase",
|
|
deletingInstance(),
|
|
databasev1alpha1.PostgreSQLInstancePhaseDeleting,
|
|
true,
|
|
),
|
|
)
|
|
|
|
DescribeTable("dispatches Tenant phases",
|
|
func(tenant *databasev1alpha1.PostgreSQLTenant, expected databasev1alpha1.PostgreSQLTenantPhase, hasMessage bool) {
|
|
result := newTenantStateMachine().reconcile(tenant)
|
|
Expect(result.phase).To(Equal(expected))
|
|
if hasMessage {
|
|
Expect(result.reconcilingMessage).NotTo(BeEmpty())
|
|
} else {
|
|
Expect(result.reconcilingMessage).To(BeEmpty())
|
|
}
|
|
},
|
|
Entry("starts pending from an empty checkpoint",
|
|
&databasev1alpha1.PostgreSQLTenant{},
|
|
databasev1alpha1.PostgreSQLTenantPhasePending,
|
|
true,
|
|
),
|
|
Entry("keeps an active phase until its handler can observe dependencies",
|
|
&databasev1alpha1.PostgreSQLTenant{Status: databasev1alpha1.PostgreSQLTenantStatus{
|
|
Phase: databasev1alpha1.PostgreSQLTenantPhaseCredentialCreated,
|
|
}},
|
|
databasev1alpha1.PostgreSQLTenantPhaseCredentialCreated,
|
|
false,
|
|
),
|
|
Entry("routes deletion independently of the previous phase",
|
|
deletingTenant(),
|
|
databasev1alpha1.PostgreSQLTenantPhaseDeleting,
|
|
true,
|
|
),
|
|
)
|
|
})
|
|
|
|
func deletingInstance() *databasev1alpha1.PostgreSQLInstance {
|
|
deletionTimestamp := metav1.NewTime(time.Now())
|
|
return &databasev1alpha1.PostgreSQLInstance{
|
|
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &deletionTimestamp},
|
|
Status: databasev1alpha1.PostgreSQLInstanceStatus{
|
|
Phase: databasev1alpha1.PostgreSQLInstancePhaseReady,
|
|
},
|
|
}
|
|
}
|
|
|
|
func deletingTenant() *databasev1alpha1.PostgreSQLTenant {
|
|
deletionTimestamp := metav1.NewTime(time.Now())
|
|
return &databasev1alpha1.PostgreSQLTenant{
|
|
ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &deletionTimestamp},
|
|
Status: databasev1alpha1.PostgreSQLTenantStatus{
|
|
Phase: databasev1alpha1.PostgreSQLTenantPhaseReady,
|
|
},
|
|
}
|
|
}
|