feat: 初始化 controller 状态机 #3

Merged
panxiao81 merged 2 commits from feature/status-state-machine into main 2026-09-10 09:17:10 +00:00
6 changed files with 148 additions and 8 deletions
Showing only changes of commit ed564d411f - Show all commits
+34
View File
@@ -0,0 +1,34 @@
/*
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 v1alpha1
const (
// ConditionTypeReady is the stable condition consumers use for readiness.
ConditionTypeReady = "Ready"
ReasonReconciling = "Reconciling"
ReasonReady = "Ready"
ReasonInvalidSpec = "InvalidSpec"
ReasonImmutableField = "ImmutableField"
ReasonDependencyUnavailable = "DependencyUnavailable"
ReasonAuthenticationFailed = "AuthenticationFailed"
ReasonInsufficientPrivileges = "InsufficientPrivileges"
ReasonInstanceNotReady = "InstanceNotReady"
ReasonConflict = "Conflict"
ReasonProvisioningFailed = "ProvisioningFailed"
ReasonCredentialProjectionFailed = "CredentialProjectionFailed"
)
@@ -18,7 +18,9 @@ package controller
import (
"context"
"reflect"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -47,9 +49,30 @@ type PostgreSQLInstanceReconciler struct {
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *PostgreSQLInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = logf.FromContext(ctx)
logger := logf.FromContext(ctx)
instance := &databasev1alpha1.PostgreSQLInstance{}
if err := r.Get(ctx, req.NamespacedName, instance); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// TODO(user): your logic here
before := instance.DeepCopy()
if !instance.DeletionTimestamp.IsZero() {
panxiao81 marked this conversation as resolved Outdated
Outdated
Review

我们是不是应该用策略模式搞个状态机实现类?
直接传当前状态进去跳转对应状态实现代码比这样好读

我们是不是应该用策略模式搞个状态机实现类? 直接传当前状态进去跳转对应状态实现代码比这样好读
instance.Status.Phase = databasev1alpha1.PostgreSQLInstancePhaseDeleting
setReconcilingCondition(&instance.Status.Conditions, instance.Generation, "instance deletion is reconciling")
} else if instance.Status.Phase == "" {
instance.Status.Phase = databasev1alpha1.PostgreSQLInstancePhaseValidating
setReconcilingCondition(&instance.Status.Conditions, instance.Generation, "instance dependencies are being validated")
}
if !reflect.DeepEqual(before.Status, instance.Status) {
if err := r.Status().Patch(ctx, instance, client.MergeFrom(before)); err != nil {
if apierrors.IsConflict(err) {
logger.V(1).Info("instance status changed concurrently; retrying")
}
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
@@ -85,8 +85,21 @@ var _ = Describe("PostgreSQLInstance Controller", func() {
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
actual := &databasev1alpha1.PostgreSQLInstance{}
Expect(k8sClient.Get(ctx, typeNamespacedName, actual)).To(Succeed())
Expect(actual.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLInstancePhaseValidating))
Expect(actual.Status.Conditions).To(ConsistOf(And(
HaveField("Type", databasev1alpha1.ConditionTypeReady),
HaveField("Status", metav1.ConditionUnknown),
HaveField("Reason", databasev1alpha1.ReasonReconciling),
HaveField("ObservedGeneration", actual.Generation),
)))
resourceVersion := actual.ResourceVersion
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient.Get(ctx, typeNamespacedName, actual)).To(Succeed())
Expect(actual.ResourceVersion).To(Equal(resourceVersion), "an unchanged status must not be patched")
})
})
})
@@ -18,7 +18,9 @@ package controller
import (
"context"
"reflect"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -47,9 +49,29 @@ type PostgreSQLTenantReconciler struct {
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *PostgreSQLTenantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = logf.FromContext(ctx)
logger := logf.FromContext(ctx)
tenant := &databasev1alpha1.PostgreSQLTenant{}
if err := r.Get(ctx, req.NamespacedName, tenant); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// TODO(user): your logic here
before := tenant.DeepCopy()
if !tenant.DeletionTimestamp.IsZero() {
tenant.Status.Phase = databasev1alpha1.PostgreSQLTenantPhaseDeleting
setReconcilingCondition(&tenant.Status.Conditions, tenant.Generation, "tenant deletion is reconciling")
} else if tenant.Status.Phase == "" {
tenant.Status.Phase = databasev1alpha1.PostgreSQLTenantPhasePending
setReconcilingCondition(&tenant.Status.Conditions, tenant.Generation, "tenant is waiting for its instance")
}
if !reflect.DeepEqual(before.Status, tenant.Status) {
if err := r.Status().Patch(ctx, tenant, client.MergeFrom(before)); err != nil {
if apierrors.IsConflict(err) {
logger.V(1).Info("tenant status changed concurrently; retrying")
}
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
@@ -82,8 +82,22 @@ var _ = Describe("PostgreSQLTenant Controller", func() {
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
actual := &databasev1alpha1.PostgreSQLTenant{}
Expect(k8sClient.Get(ctx, typeNamespacedName, actual)).To(Succeed())
Expect(actual.Status.Phase).To(Equal(databasev1alpha1.PostgreSQLTenantPhasePending))
Expect(actual.Status.Conditions).To(ConsistOf(And(
HaveField("Type", databasev1alpha1.ConditionTypeReady),
HaveField("Status", metav1.ConditionUnknown),
HaveField("Reason", databasev1alpha1.ReasonReconciling),
HaveField("ObservedGeneration", actual.Generation),
)))
resourceVersion := actual.ResourceVersion
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient.Get(ctx, typeNamespacedName, actual)).To(Succeed())
Expect(actual.ResourceVersion).To(Equal(resourceVersion), "an unchanged status must not be patched")
})
})
})
+34
View File
@@ -0,0 +1,34 @@
/*
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 (
apiMeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1"
)
func setReconcilingCondition(conditions *[]metav1.Condition, generation int64, message string) {
apiMeta.SetStatusCondition(conditions, metav1.Condition{
Type: databasev1alpha1.ConditionTypeReady,
Status: metav1.ConditionUnknown,
ObservedGeneration: generation,
Reason: databasev1alpha1.ReasonReconciling,
Message: message,
})
}