feat: 初始化 controller 状态机
Lint / Run on Ubuntu (pull_request) Successful in 4m59s
E2E Tests / Run on Ubuntu (pull_request) Failing after 54s
Tests / Run on Ubuntu (pull_request) Successful in 4m18s

This commit is contained in:
2026-09-10 08:44:39 +00:00
parent 60b63100b2
commit ed564d411f
6 changed files with 148 additions and 8 deletions
+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 ( import (
"context" "context"
"reflect"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@@ -47,9 +49,30 @@ type PostgreSQLInstanceReconciler struct {
// For more details, check Reconcile and its Result here: // For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile // - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *PostgreSQLInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 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() {
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 return ctrl.Result{}, nil
} }
@@ -85,8 +85,21 @@ var _ = Describe("PostgreSQLInstance Controller", func() {
NamespacedName: typeNamespacedName, NamespacedName: typeNamespacedName,
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic. actual := &databasev1alpha1.PostgreSQLInstance{}
// Example: If you expect a certain status condition after reconciliation, verify it here. 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 ( import (
"context" "context"
"reflect"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@@ -47,9 +49,29 @@ type PostgreSQLTenantReconciler struct {
// For more details, check Reconcile and its Result here: // For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile // - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *PostgreSQLTenantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 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 return ctrl.Result{}, nil
} }
@@ -82,8 +82,22 @@ var _ = Describe("PostgreSQLTenant Controller", func() {
NamespacedName: typeNamespacedName, NamespacedName: typeNamespacedName,
}) })
Expect(err).NotTo(HaveOccurred()) 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,
})
}