diff --git a/api/v1alpha1/condition_types.go b/api/v1alpha1/condition_types.go new file mode 100644 index 0000000..4e01621 --- /dev/null +++ b/api/v1alpha1/condition_types.go @@ -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" +) diff --git a/internal/controller/postgresqlinstance_controller.go b/internal/controller/postgresqlinstance_controller.go index d0e4f2d..cabbc31 100644 --- a/internal/controller/postgresqlinstance_controller.go +++ b/internal/controller/postgresqlinstance_controller.go @@ -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/controller-runtime@v0.24.1/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() { + 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 } diff --git a/internal/controller/postgresqlinstance_controller_test.go b/internal/controller/postgresqlinstance_controller_test.go index 592b6a1..515c7d6 100644 --- a/internal/controller/postgresqlinstance_controller_test.go +++ b/internal/controller/postgresqlinstance_controller_test.go @@ -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") }) }) }) diff --git a/internal/controller/postgresqltenant_controller.go b/internal/controller/postgresqltenant_controller.go index 87c44ca..d3cef12 100644 --- a/internal/controller/postgresqltenant_controller.go +++ b/internal/controller/postgresqltenant_controller.go @@ -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/controller-runtime@v0.24.1/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 } diff --git a/internal/controller/postgresqltenant_controller_test.go b/internal/controller/postgresqltenant_controller_test.go index d227b51..1f4b50f 100644 --- a/internal/controller/postgresqltenant_controller_test.go +++ b/internal/controller/postgresqltenant_controller_test.go @@ -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") }) }) }) diff --git a/internal/controller/status.go b/internal/controller/status.go new file mode 100644 index 0000000..a2d6456 --- /dev/null +++ b/internal/controller/status.go @@ -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, + }) +}