diff --git a/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml b/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml index 3ecad50abb..3b0dfd5196 100644 --- a/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml +++ b/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml @@ -18321,6 +18321,62 @@ spec: !has(u.grantPublicSchemaAccess) || !u.grantPublicSchemaAccess)' status: properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array host: type: string installedCustomExtensions: diff --git a/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml b/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml index 91112c8f71..1b97dff6da 100644 --- a/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml +++ b/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml @@ -18728,6 +18728,62 @@ spec: !has(u.grantPublicSchemaAccess) || !u.grantPublicSchemaAccess)' status: properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array host: type: string installedCustomExtensions: diff --git a/deploy/bundle.yaml b/deploy/bundle.yaml index b22290c749..64cdb850d0 100644 --- a/deploy/bundle.yaml +++ b/deploy/bundle.yaml @@ -19025,6 +19025,62 @@ spec: !has(u.grantPublicSchemaAccess) || !u.grantPublicSchemaAccess)' status: properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array host: type: string installedCustomExtensions: diff --git a/deploy/crd.yaml b/deploy/crd.yaml index 73d9b6489d..bd5bd24781 100644 --- a/deploy/crd.yaml +++ b/deploy/crd.yaml @@ -19025,6 +19025,62 @@ spec: !has(u.grantPublicSchemaAccess) || !u.grantPublicSchemaAccess)' status: properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array host: type: string installedCustomExtensions: diff --git a/deploy/cw-bundle.yaml b/deploy/cw-bundle.yaml index b9226f369a..3fbb77be04 100644 --- a/deploy/cw-bundle.yaml +++ b/deploy/cw-bundle.yaml @@ -19025,6 +19025,62 @@ spec: !has(u.grantPublicSchemaAccess) || !u.grantPublicSchemaAccess)' status: properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array host: type: string installedCustomExtensions: diff --git a/go.mod b/go.mod index ef680788f2..c99bb2d081 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.37.0 - github.com/pganalyze/pg_query_go/v5 v5.1.0 + github.com/pganalyze/pg_query_go/v6 v6.1.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 github.com/xdg-go/stringprep v1.0.4 diff --git a/go.sum b/go.sum index a2bd55bc50..872fdacf2a 100644 --- a/go.sum +++ b/go.sum @@ -169,8 +169,8 @@ github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/pganalyze/pg_query_go/v5 v5.1.0 h1:MlxQqHZnvA3cbRQYyIrjxEjzo560P6MyTgtlaf3pmXg= -github.com/pganalyze/pg_query_go/v5 v5.1.0/go.mod h1:FsglvxidZsVN+Ltw3Ai6nTgPVcK2BPukH3jCDEqc1Ug= +github.com/pganalyze/pg_query_go/v6 v6.1.0 h1:jG5ZLhcVgL1FAw4C/0VNQaVmX1SUJx71wBGdtTtBvls= +github.com/pganalyze/pg_query_go/v6 v6.1.0/go.mod h1:nvTHIuoud6e1SfrUaFwHqT0i4b5Nr+1rPWVds3B5+50= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/internal/postgres/users.go b/internal/postgres/users.go index 71187583e0..c878ba1a1d 100644 --- a/internal/postgres/users.go +++ b/internal/postgres/users.go @@ -10,7 +10,7 @@ import ( "encoding/json" "strings" - pg_query "github.com/pganalyze/pg_query_go/v5" + pg_query "github.com/pganalyze/pg_query_go/v6" "github.com/percona/percona-postgresql-operator/internal/feature" "github.com/percona/percona-postgresql-operator/internal/logging" diff --git a/percona/controller/pgbackup/controller.go b/percona/controller/pgbackup/controller.go index 203b354ea9..5b4e6a4f32 100644 --- a/percona/controller/pgbackup/controller.go +++ b/percona/controller/pgbackup/controller.go @@ -10,6 +10,8 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -78,7 +80,7 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re pgBackup.Default() - if !pgBackup.DeletionTimestamp.IsZero() { + if !pgBackup.DeletionTimestamp.IsZero() || pgBackup.Status.State == v2.BackupFailed { if _, err := runFinalizers(ctx, r.Client, pgBackup); err != nil { return reconcile.Result{}, errors.Wrap(err, "failed to run finalizers") } @@ -91,12 +93,18 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re } } + pgCluster := new(v2.PerconaPGCluster) + if err := r.Client.Get(ctx, types.NamespacedName{Name: pgBackup.Spec.PGCluster, Namespace: request.Namespace}, pgCluster); err != nil { + if !k8serrors.IsNotFound(err) { + return reconcile.Result{}, errors.Wrap(err, "get PostgresCluster") + } + pgCluster = nil + } + switch pgBackup.Status.State { case v2.BackupNew: - pgCluster := &v2.PerconaPGCluster{} - err := r.Client.Get(ctx, types.NamespacedName{Name: pgBackup.Spec.PGCluster, Namespace: request.Namespace}, pgCluster) - if err != nil { - return reconcile.Result{}, errors.Wrap(err, "get PostgresCluster") + if pgCluster == nil { + return reconcile.Result{}, errors.Errorf("PostgresCluster %s is not found", pgBackup.Spec.PGCluster) } if !pgCluster.Spec.Backups.IsEnabled() { @@ -123,7 +131,7 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re } // start backup only if backup job doesn't exist - _, err = findBackupJob(ctx, r.Client, pgCluster, pgBackup) + _, err := findBackupJob(ctx, r.Client, pgCluster, pgBackup) if err != nil { if !errors.Is(err, ErrBackupJobNotFound) { return reconcile.Result{}, errors.Wrap(err, "find backup job") @@ -147,12 +155,7 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re return reconcile.Result{}, errors.Errorf("%s repo not defined", pgBackup.Spec.RepoName) } - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - bcp := new(v2.PerconaPGBackup) - if err := r.Client.Get(ctx, client.ObjectKeyFromObject(pgBackup), bcp); err != nil { - return errors.Wrap(err, "get PGBackup") - } - + if err := updateStatus(ctx, r.Client, pgBackup, func(bcp *v2.PerconaPGBackup) { bcp.Status.Destination = getDestination(pgCluster, pgBackup) bcp.Status.Image = pgCluster.Spec.Backups.PGBackRest.Image bcp.Status.Repo = repo @@ -170,8 +173,6 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re } bcp.Status.State = v2.BackupStarting - - return r.Client.Status().Update(ctx, bcp) }); err != nil { return reconcile.Result{}, errors.Wrap(err, "update PGBackup status") } @@ -179,10 +180,8 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re log.Info("Backup is starting", "backup", pgBackup.Name, "cluster", pgCluster.Name) return reconcile.Result{}, nil case v2.BackupStarting: - pgCluster := &v2.PerconaPGCluster{} - err := r.Client.Get(ctx, types.NamespacedName{Name: pgBackup.Spec.PGCluster, Namespace: request.Namespace}, pgCluster) - if err != nil { - return reconcile.Result{}, errors.Wrap(err, "get PostgresCluster") + if pgCluster == nil { + return reconcile.Result{}, errors.Errorf("PostgresCluster %s is not found", pgBackup.Spec.PGCluster) } job, err := findBackupJob(ctx, r.Client, pgCluster, pgBackup) @@ -190,6 +189,10 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re if errors.Is(err, ErrBackupJobNotFound) { log.Info("Waiting for backup to start") + if err := failIfClusterIsNotReady(ctx, r.Client, pgCluster, pgBackup); err != nil { + return reconcile.Result{}, errors.Wrap(err, "fail if cluster is not ready for backup") + } + return reconcile.Result{RequeueAfter: time.Second * 5}, nil } return reconcile.Result{}, errors.Wrap(err, "find backup job") @@ -216,16 +219,9 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re return reconcile.Result{}, errors.Wrap(err, "update PGBackup") } - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - bcp := new(v2.PerconaPGBackup) - if err := r.Client.Get(ctx, types.NamespacedName{Name: pgBackup.Name, Namespace: pgBackup.Namespace}, bcp); err != nil { - return errors.Wrap(err, "get PGBackup") - } - + if err := updateStatus(ctx, r.Client, pgBackup, func(bcp *v2.PerconaPGBackup) { bcp.Status.State = v2.BackupRunning bcp.Status.JobName = job.Name - - return r.Client.Status().Update(ctx, bcp) }); err != nil { return reconcile.Result{}, errors.Wrap(err, "update PGBackup status") } @@ -249,14 +245,6 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re return reconcile.Result{RequeueAfter: time.Second * 5}, nil } - pgCluster := &v2.PerconaPGCluster{} - if err := r.Client.Get(ctx, types.NamespacedName{Name: pgBackup.Spec.PGCluster, Namespace: request.Namespace}, pgCluster); err != nil { - if !k8serrors.IsNotFound(err) { - return reconcile.Result{}, errors.Wrap(err, "get PostgresCluster") - } - pgCluster = nil - } - // We need to perform the same steps as in the delete-backup finalizer once the backup has finished or failed. // After that, the finalizer is no longer needed, that's why the RunFinalizer function is used here. done, err := controller.RunFinalizer(ctx, r.Client, pgBackup, pNaming.FinalizerDeleteBackup, deleteBackupFinalizer(r.Client, pgCluster)) @@ -268,29 +256,17 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re return reconcile.Result{RequeueAfter: time.Second * 5}, nil } - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - bcp := new(v2.PerconaPGBackup) - if err := r.Client.Get(ctx, types.NamespacedName{Name: pgBackup.Name, Namespace: pgBackup.Namespace}, bcp); err != nil { - return errors.Wrap(err, "get PGBackup") - } - + if err := updateStatus(ctx, r.Client, pgBackup, func(bcp *v2.PerconaPGBackup) { bcp.Status.CompletedAt = job.Status.CompletionTime bcp.Status.State = status - - return r.Client.Status().Update(ctx, bcp) }); err != nil { return reconcile.Result{}, errors.Wrap(err, "update PGBackup status") } return reconcile.Result{}, nil case v2.BackupSucceeded: - pgCluster := &v2.PerconaPGCluster{} - err := r.Client.Get(ctx, types.NamespacedName{Name: pgBackup.Spec.PGCluster, Namespace: request.Namespace}, pgCluster) - if err != nil { - if k8serrors.IsNotFound(err) { - return reconcile.Result{}, nil - } - return reconcile.Result{}, errors.Wrap(err, "get PostgresCluster") + if pgCluster == nil { + return reconcile.Result{}, nil } execCli, err := clientcmd.NewClient() @@ -302,15 +278,8 @@ func (r *PGBackupReconciler) Reconcile(ctx context.Context, request reconcile.Re if err == nil { log.Info("Got latest restorable timestamp", "timestamp", latestRestorableTime) - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - bcp := new(v2.PerconaPGBackup) - if err := r.Client.Get(ctx, client.ObjectKeyFromObject(pgBackup), bcp); err != nil { - return errors.Wrap(err, "get PGBackup") - } - + if err := updateStatus(ctx, r.Client, pgBackup, func(bcp *v2.PerconaPGBackup) { bcp.Status.LatestRestorableTime.Time = latestRestorableTime - - return r.Client.Status().Update(ctx, bcp) }); err != nil { return reconcile.Result{}, errors.Wrap(err, "update PGBackup status") } @@ -353,6 +322,9 @@ func deleteBackupFinalizer(c client.Client, pg *v2.PerconaPGCluster) func(ctx co if client.IgnoreNotFound(err) != nil { return errors.Wrap(err, "get backup job") } + if k8serrors.IsNotFound(err) { + job = nil + } rr, err := finishBackup(ctx, c, pgBackup, job) if err != nil { @@ -478,16 +450,9 @@ func updatePGBackrestInfo(ctx context.Context, c client.Client, pod *corev1.Pod, stanzaName = info.Name if pgBackup.Status.BackupName == "" { - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - bcp := new(v2.PerconaPGBackup) - if err := c.Get(ctx, types.NamespacedName{Name: pgBackup.Name, Namespace: pgBackup.Namespace}, bcp); err != nil { - return errors.Wrap(err, "get PGBackup") - } - + if err := updateStatus(ctx, c, pgBackup, func(bcp *v2.PerconaPGBackup) { bcp.Status.BackupName = backup.Label bcp.Status.BackupType = backup.Type - - return c.Status().Update(ctx, bcp) }); err != nil { return errors.Wrap(err, "update PGBackup status") } @@ -505,7 +470,7 @@ func updatePGBackrestInfo(ctx context.Context, c client.Client, pod *corev1.Pod, } func finishBackup(ctx context.Context, c client.Client, pgBackup *v2.PerconaPGBackup, job *batchv1.Job) (*reconcile.Result, error) { - if checkBackupJob(job) == v2.BackupSucceeded { + if job != nil && checkBackupJob(job) == v2.BackupSucceeded { readyPod, err := controller.GetReadyInstancePod(ctx, c, pgBackup.Spec.PGCluster, pgBackup.Namespace) if err != nil { return nil, errors.Wrap(err, "get ready instance pod") @@ -516,7 +481,7 @@ func finishBackup(ctx context.Context, c client.Client, pgBackup *v2.PerconaPGBa } } - if job.Labels[naming.LabelPGBackRestBackup] != string(naming.BackupManual) { + if job != nil && job.Labels[naming.LabelPGBackRestBackup] != string(naming.BackupManual) { return nil, nil } @@ -574,21 +539,23 @@ func finishBackup(ctx context.Context, c client.Client, pgBackup *v2.PerconaPGBa // Remove PGBackRest labels to prevent the job from being // deleted by the cleanupRepoResources method. - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - j := new(batchv1.Job) - if err := c.Get(ctx, client.ObjectKeyFromObject(job), j); err != nil { - if k8serrors.IsNotFound(err) { - return nil + if job != nil { + if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + j := new(batchv1.Job) + if err := c.Get(ctx, client.ObjectKeyFromObject(job), j); err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + return errors.Wrap(err, "get job") + } + for k := range naming.PGBackRestLabels(pgBackup.Spec.PGCluster) { + delete(j.Labels, k) } - return errors.Wrap(err, "get job") - } - for k := range naming.PGBackRestLabels(pgBackup.Spec.PGCluster) { - delete(j.Labels, k) - } - return c.Update(ctx, j) - }); err != nil { - return nil, errors.Wrap(err, "update backup job labels") + return c.Update(ctx, j) + }); err != nil { + return nil, errors.Wrap(err, "update backup job labels") + } } if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { @@ -611,7 +578,7 @@ func finishBackup(ctx context.Context, c client.Client, pgBackup *v2.PerconaPGBa return &reconcile.Result{RequeueAfter: time.Second * 5}, nil } - if checkBackupJob(job) != v2.BackupSucceeded { + if job != nil && checkBackupJob(job) != v2.BackupSucceeded { // Remove all crunchy labels to prevent the job from being included in // repoResources.manualBackupJobs used in reconcileManualBackup method. if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { @@ -715,3 +682,49 @@ func checkBackupJob(job *batchv1.Job) v2.PGBackupState { return v2.BackupRunning } } + +func failIfClusterIsNotReady(ctx context.Context, cl client.Client, pgCluster *v2.PerconaPGCluster, pgBackup *v2.PerconaPGBackup) error { + log := logging.FromContext(ctx) + + condition := meta.FindStatusCondition(pgCluster.Status.Conditions, pNaming.ConditionClusterIsReadyForBackup) + if condition == nil || condition.Status == metav1.ConditionTrue || condition.LastTransitionTime.Time.Add(2*time.Minute).After(time.Now()) { + return nil + } + + crunchyCluster := new(v1beta1.PostgresCluster) + if err := cl.Get(ctx, client.ObjectKeyFromObject(pgCluster), crunchyCluster); err != nil { + return errors.Wrap(err, "get PostgresCluster") + } + + // Waiting for the crunchy cluster to receive the annotations added by the startBackup function. + _, ok := crunchyCluster.Annotations[pNaming.ToCrunchyAnnotation(naming.PGBackRestBackup)] + if !ok { + return nil + } + _, ok = crunchyCluster.Annotations[pNaming.ToCrunchyAnnotation(pNaming.AnnotationBackupInProgress)] + if !ok { + return nil + } + + log.Info("Cluster is not ready for backup for too long. Setting it's state to Failed") + + if err := updateStatus(ctx, cl, pgBackup, func(bcp *v2.PerconaPGBackup) { + bcp.Status.State = v2.BackupFailed + }); err != nil { + return errors.Wrap(err, "update PGBackup status") + } + return nil +} + +func updateStatus(ctx context.Context, cl client.Client, pgBackup *v2.PerconaPGBackup, updateFunc func(bcp *v2.PerconaPGBackup)) error { + return retry.RetryOnConflict(retry.DefaultBackoff, func() error { + bcp := new(v2.PerconaPGBackup) + if err := cl.Get(ctx, client.ObjectKeyFromObject(pgBackup), bcp); err != nil { + return errors.Wrap(err, "get PGBackup") + } + + updateFunc(bcp) + + return cl.Status().Update(ctx, bcp) + }) +} diff --git a/percona/controller/pgbackup/controller_test.go b/percona/controller/pgbackup/controller_test.go new file mode 100644 index 0000000000..bcb13bf8b1 --- /dev/null +++ b/percona/controller/pgbackup/controller_test.go @@ -0,0 +1,161 @@ +package pgbackup + +import ( + "context" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/percona/percona-postgresql-operator/internal/naming" + pNaming "github.com/percona/percona-postgresql-operator/percona/naming" + v2 "github.com/percona/percona-postgresql-operator/pkg/apis/pgv2.percona.com/v2" + "github.com/percona/percona-postgresql-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" +) + +func TestFailIfClusterIsNotReady(t *testing.T) { + ctx := context.Background() + + cr, err := readDefaultCR("ready-for-backup", "ready-for-backup") + if err != nil { + t.Fatal(err) + } + cr.Annotations[pNaming.AnnotationBackupInProgress] = "some-backup" + cr.Annotations[naming.PGBackRestBackup] = "some-backup" + + tests := []struct { + name string + cr *v2.PerconaPGCluster + pgBackup *v2.PerconaPGBackup + expectedState v2.PGBackupState + }{ + { + name: "no condition", + cr: cr.DeepCopy(), + pgBackup: &v2.PerconaPGBackup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-backup", + Namespace: cr.Namespace, + }, + Status: v2.PerconaPGBackupStatus{ + State: v2.BackupStarting, + }, + }, + expectedState: v2.BackupStarting, + }, + { + name: "true condition", + cr: func() *v2.PerconaPGCluster { + cr := cr.DeepCopy() + cr.Status.Conditions = append(cr.Status.Conditions, metav1.Condition{ + Type: pNaming.ConditionClusterIsReadyForBackup, + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + }) + return cr + }(), + pgBackup: &v2.PerconaPGBackup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-backup", + Namespace: cr.Namespace, + }, + Status: v2.PerconaPGBackupStatus{ + State: v2.BackupStarting, + }, + }, + expectedState: v2.BackupStarting, + }, + { + name: "false condition with current time", + cr: func() *v2.PerconaPGCluster { + cr := cr.DeepCopy() + cr.Status.Conditions = append(cr.Status.Conditions, metav1.Condition{ + Type: pNaming.ConditionClusterIsReadyForBackup, + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.Now(), + }) + return cr + }(), + pgBackup: &v2.PerconaPGBackup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-backup", + Namespace: cr.Namespace, + }, + Status: v2.PerconaPGBackupStatus{ + State: v2.BackupStarting, + }, + }, + expectedState: v2.BackupStarting, + }, + { + name: "false condition with old time", + cr: func() *v2.PerconaPGCluster { + cr := cr.DeepCopy() + cr.Status.Conditions = append(cr.Status.Conditions, metav1.Condition{ + Type: pNaming.ConditionClusterIsReadyForBackup, + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.Time{Time: time.Now().Add(-time.Hour)}, + }) + return cr + }(), + pgBackup: &v2.PerconaPGBackup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-backup", + Namespace: cr.Namespace, + }, + Status: v2.PerconaPGBackupStatus{ + State: v2.BackupStarting, + }, + }, + expectedState: v2.BackupFailed, + }, + { + name: "false condition with old time without annotations", + cr: func() *v2.PerconaPGCluster { + cr := cr.DeepCopy() + cr.Annotations = make(map[string]string) + cr.Status.Conditions = append(cr.Status.Conditions, metav1.Condition{ + Type: pNaming.ConditionClusterIsReadyForBackup, + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.Time{Time: time.Now().Add(-time.Hour)}, + }) + return cr + }(), + pgBackup: &v2.PerconaPGBackup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-backup", + Namespace: cr.Namespace, + }, + Status: v2.PerconaPGBackupStatus{ + State: v2.BackupStarting, + }, + }, + expectedState: v2.BackupStarting, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cl, err := buildFakeClient(ctx, tt.cr, tt.pgBackup) + if err != nil { + t.Fatal(err) + } + + crunchyCluster := new(v1beta1.PostgresCluster) + if err := cl.Get(ctx, client.ObjectKeyFromObject(tt.cr), crunchyCluster); err != nil { + t.Fatal(err) + } + if err := failIfClusterIsNotReady(ctx, cl, tt.cr, tt.pgBackup); err != nil { + t.Fatal(err) + } + bcp := new(v2.PerconaPGBackup) + if err := cl.Get(ctx, client.ObjectKeyFromObject(tt.pgBackup), bcp); err != nil { + t.Fatal(err) + } + if bcp.Status.State != tt.expectedState { + t.Fatalf("expected state: %s; got: %s", tt.expectedState, bcp.Status.State) + } + }) + } +} diff --git a/percona/controller/pgbackup/testutils_test.go b/percona/controller/pgbackup/testutils_test.go new file mode 100644 index 0000000000..77f9a11a18 --- /dev/null +++ b/percona/controller/pgbackup/testutils_test.go @@ -0,0 +1,79 @@ +package pgbackup + +import ( + "context" + "os" + "path/filepath" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/percona/percona-postgresql-operator/internal/naming" + pNaming "github.com/percona/percona-postgresql-operator/percona/naming" + v2 "github.com/percona/percona-postgresql-operator/pkg/apis/pgv2.percona.com/v2" + "github.com/percona/percona-postgresql-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" +) + +type fakeClient struct { + client.Client +} + +var _ = client.Client(new(fakeClient)) + +func buildFakeClient(ctx context.Context, cr *v2.PerconaPGCluster, objs ...client.Object) (client.Client, error) { + s := scheme.Scheme + + if err := v1beta1.AddToScheme(s); err != nil { + return nil, err + } + if err := v2.AddToScheme(s); err != nil { + return nil, err + } + + objs = append(objs, cr) + cr.Default() + postgresCluster, err := cr.ToCrunchy(ctx, nil, s) + if err != nil { + return nil, err + } + objs = append(objs, postgresCluster) + + dcs := &corev1.Endpoints{ObjectMeta: naming.PatroniDistributedConfiguration(postgresCluster)} + dcs.Annotations = map[string]string{ + "initialize": "system-identifier", + } + objs = append(objs, dcs) + + cl := new(fakeClient) + cl.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(objs...).WithStatusSubresource(objs...).Build() + + return cl, nil +} + +func readDefaultCR(name, namespace string) (*v2.PerconaPGCluster, error) { + data, err := os.ReadFile(filepath.Join("..", "..", "..", "deploy", "cr.yaml")) + if err != nil { + return nil, err + } + + cr := &v2.PerconaPGCluster{} + + if err := yaml.Unmarshal(data, cr); err != nil { + return nil, err + } + + cr.Name = name + if cr.Annotations == nil { + cr.Annotations = make(map[string]string) + } + cr.Spec.InitContainer = &v1beta1.InitContainerSpec{ + Image: "some-image", + } + cr.Annotations[pNaming.AnnotationCustomPatroniVersion] = "4.0.0" + cr.Namespace = namespace + cr.Status.Postgres.Version = cr.Spec.PostgresVersion + return cr, nil +} diff --git a/percona/controller/pgcluster/schedule.go b/percona/controller/pgcluster/schedule.go index 3c8d19997f..4515380cf7 100644 --- a/percona/controller/pgcluster/schedule.go +++ b/percona/controller/pgcluster/schedule.go @@ -6,12 +6,14 @@ import ( "github.com/go-logr/logr" "github.com/pkg/errors" k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "github.com/percona/percona-postgresql-operator/internal/controller/postgrescluster" "github.com/percona/percona-postgresql-operator/internal/logging" "github.com/percona/percona-postgresql-operator/internal/naming" + pNaming "github.com/percona/percona-postgresql-operator/percona/naming" v2 "github.com/percona/percona-postgresql-operator/pkg/apis/pgv2.percona.com/v2" "github.com/percona/percona-postgresql-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" ) @@ -105,6 +107,11 @@ func (r *PGClusterReconciler) createScheduledBackup(log logr.Logger, backupName, log.Info("Cluster is not ready. Can't start scheduled backup") return nil } + condition := meta.FindStatusCondition(cr.Status.Conditions, pNaming.ConditionClusterIsReadyForBackup) + if condition != nil && condition.Status == metav1.ConditionFalse { + log.Info("ReadyForBackup condition is set to false. Can't start scheduled backup") + return nil + } pb := &v2.PerconaPGBackup{ ObjectMeta: metav1.ObjectMeta{ diff --git a/percona/controller/pgcluster/status.go b/percona/controller/pgcluster/status.go index 0dd3893fdb..238155cc2b 100644 --- a/percona/controller/pgcluster/status.go +++ b/percona/controller/pgcluster/status.go @@ -5,9 +5,13 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" + "github.com/percona/percona-postgresql-operator/internal/controller/postgrescluster" + pNaming "github.com/percona/percona-postgresql-operator/percona/naming" v2 "github.com/percona/percona-postgresql-operator/pkg/apis/pgv2.percona.com/v2" "github.com/percona/percona-postgresql-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" ) @@ -118,6 +122,8 @@ func (r *PGClusterReconciler) updateStatus(ctx context.Context, cr *v2.PerconaPG cluster.Status.State = r.getState(cr, &cluster.Status, status) + updateConditions(cluster, status) + return r.Client.Status().Update(ctx, cluster) }); err != nil { return errors.Wrap(err, "update PerconaPGCluster status") @@ -125,3 +131,31 @@ func (r *PGClusterReconciler) updateStatus(ctx context.Context, cr *v2.PerconaPG return nil } + +func updateConditions(cr *v2.PerconaPGCluster, status *v1beta1.PostgresClusterStatus) { + setClusterNotReadyCondition := func(status metav1.ConditionStatus, reason string) { + existing := meta.FindStatusCondition(cr.Status.Conditions, pNaming.ConditionClusterIsReadyForBackup) + if existing == nil || existing.Status != status || existing.Reason != reason { + _ = meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{ + Type: pNaming.ConditionClusterIsReadyForBackup, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + }) + } + } + + repoCondition := meta.FindStatusCondition(status.Conditions, postgrescluster.ConditionRepoHostReady) + if repoCondition == nil || repoCondition.Status != metav1.ConditionTrue { + setClusterNotReadyCondition(metav1.ConditionFalse, postgrescluster.ConditionRepoHostReady) + return + } + + backupCondition := meta.FindStatusCondition(status.Conditions, postgrescluster.ConditionReplicaCreate) + if backupCondition == nil || backupCondition.Status != metav1.ConditionTrue { + setClusterNotReadyCondition(metav1.ConditionFalse, postgrescluster.ConditionReplicaCreate) + return + } + + setClusterNotReadyCondition(metav1.ConditionTrue, "AllConditionsAreTrue") +} diff --git a/percona/controller/pgcluster/status_test.go b/percona/controller/pgcluster/status_test.go index b4f56f0f93..7a8c3f88c7 100644 --- a/percona/controller/pgcluster/status_test.go +++ b/percona/controller/pgcluster/status_test.go @@ -11,11 +11,14 @@ import ( . "github.com/onsi/gomega" gs "github.com/onsi/gomega/gstruct" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/percona/percona-postgresql-operator/internal/controller/postgrescluster" + pNaming "github.com/percona/percona-postgresql-operator/percona/naming" v2 "github.com/percona/percona-postgresql-operator/pkg/apis/pgv2.percona.com/v2" "github.com/percona/percona-postgresql-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" ) @@ -330,6 +333,123 @@ var _ = Describe("PG Cluster status", Ordered, func() { }) }) }) + + Context("Update PG cluster status.conditions", Ordered, func() { + crName := ns + "-conditions" + crNamespacedName := types.NamespacedName{Name: crName, Namespace: ns} + + cr, err := readDefaultCR(crName, ns) + It("should read default cr.yaml and create PerconaPGCluster", func() { + Expect(err).NotTo(HaveOccurred()) + status := cr.Status + Expect(k8sClient.Create(ctx, cr)).Should(Succeed()) + cr.Status = status + Expect(k8sClient.Status().Update(ctx, cr)).Should(Succeed()) + }) + + It("should reconcile and create Crunchy PostgreCluster", func() { + _, err = reconciler(cr).Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + }) + + When("conditions are not set", func() { + It("should reconcile and update conditions", func() { + _, err = reconciler(cr).Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + }) + It("ReadyForBackup condition should be False", func() { + Eventually(func() bool { + err := k8sClient.Get(ctx, crNamespacedName, cr) + return err == nil + }, time.Second*15, time.Millisecond*250).Should(BeTrue()) + + condition := meta.FindStatusCondition(cr.Status.Conditions, pNaming.ConditionClusterIsReadyForBackup) + Expect(condition).Should(Not(BeNil())) + Expect(condition.Status).Should(Equal(metav1.ConditionFalse)) + }) + }) + + When("ConditionRepoHostReady is false", func() { + It("ReadyForBackup condition should be False", func() { + updateCrunchyPGClusterStatus(ctx, crNamespacedName, func(pgc *v1beta1.PostgresCluster) { + _ = meta.SetStatusCondition(&pgc.Status.Conditions, metav1.Condition{ + Type: postgrescluster.ConditionRepoHostReady, + Status: metav1.ConditionFalse, + Reason: "test", + }) + _ = meta.SetStatusCondition(&pgc.Status.Conditions, metav1.Condition{ + Type: postgrescluster.ConditionReplicaCreate, + Status: metav1.ConditionTrue, + Reason: "test", + }) + }) + _, err = reconciler(cr).Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + Eventually(func() bool { + err := k8sClient.Get(ctx, crNamespacedName, cr) + return err == nil + }, time.Second*15, time.Millisecond*250).Should(BeTrue()) + + condition := meta.FindStatusCondition(cr.Status.Conditions, pNaming.ConditionClusterIsReadyForBackup) + Expect(condition.Status).Should(Equal(metav1.ConditionFalse)) + }) + }) + + When("ConditionReplicaCreate is false", func() { + It("ReadyForBackup condition should be False", func() { + updateCrunchyPGClusterStatus(ctx, crNamespacedName, func(pgc *v1beta1.PostgresCluster) { + _ = meta.SetStatusCondition(&pgc.Status.Conditions, metav1.Condition{ + Type: postgrescluster.ConditionRepoHostReady, + Status: metav1.ConditionTrue, + Reason: "test", + }) + _ = meta.SetStatusCondition(&pgc.Status.Conditions, metav1.Condition{ + Type: postgrescluster.ConditionReplicaCreate, + Status: metav1.ConditionFalse, + Reason: "test", + }) + }) + _, err = reconciler(cr).Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + Eventually(func() bool { + err := k8sClient.Get(ctx, crNamespacedName, cr) + return err == nil + }, time.Second*15, time.Millisecond*250).Should(BeTrue()) + + condition := meta.FindStatusCondition(cr.Status.Conditions, pNaming.ConditionClusterIsReadyForBackup) + Expect(condition.Status).Should(Equal(metav1.ConditionFalse)) + }) + }) + + When("both are true", func() { + It("ReadyForBackup condition should be True", func() { + updateCrunchyPGClusterStatus(ctx, crNamespacedName, func(pgc *v1beta1.PostgresCluster) { + _ = meta.SetStatusCondition(&pgc.Status.Conditions, metav1.Condition{ + Type: postgrescluster.ConditionRepoHostReady, + Status: metav1.ConditionTrue, + Reason: "test", + }) + _ = meta.SetStatusCondition(&pgc.Status.Conditions, metav1.Condition{ + Type: postgrescluster.ConditionReplicaCreate, + Status: metav1.ConditionTrue, + Reason: "test", + }) + }) + _, err = reconciler(cr).Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + Eventually(func() bool { + err := k8sClient.Get(ctx, crNamespacedName, cr) + return err == nil + }, time.Second*15, time.Millisecond*250).Should(BeTrue()) + + condition := meta.FindStatusCondition(cr.Status.Conditions, pNaming.ConditionClusterIsReadyForBackup) + Expect(condition.Status).Should(Equal(metav1.ConditionTrue)) + }) + }) + }) }) func reconcileAndAssertState(ctx context.Context, nn types.NamespacedName, cr *v2.PerconaPGCluster, expectedState v2.AppState) { diff --git a/percona/k8s/testutils_test.go b/percona/k8s/testutils_test.go index ab9f773c69..de3297c5a3 100644 --- a/percona/k8s/testutils_test.go +++ b/percona/k8s/testutils_test.go @@ -30,7 +30,7 @@ func (f *fakeClient) Patch(ctx context.Context, obj client.Object, patch client. if !k8serrors.IsNotFound(err) { return err } - if err := f.Client.Create(ctx, obj); err != nil { + if err := f.Create(ctx, obj); err != nil { return err } return f.Client.Patch(ctx, obj, patch, options...) diff --git a/percona/naming/conditions.go b/percona/naming/conditions.go new file mode 100644 index 0000000000..d8e5405eb0 --- /dev/null +++ b/percona/naming/conditions.go @@ -0,0 +1,5 @@ +package naming + +const ( + ConditionClusterIsReadyForBackup = "ReadyForBackup" +) diff --git a/pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go b/pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go index 55b441acc2..bcb1d2e163 100644 --- a/pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go +++ b/pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go @@ -446,6 +446,10 @@ type PerconaPGClusterStatus struct { // +optional // +operator-sdk:csv:customresourcedefinitions:type=status InstalledCustomExtensions []string `json:"installedCustomExtensions"` + + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=status + Conditions []metav1.Condition `json:"conditions,omitempty"` } type Backups struct { diff --git a/pkg/apis/pgv2.percona.com/v2/zz_generated.deepcopy.go b/pkg/apis/pgv2.percona.com/v2/zz_generated.deepcopy.go index 812ae6a162..8a0f75ebb0 100644 --- a/pkg/apis/pgv2.percona.com/v2/zz_generated.deepcopy.go +++ b/pkg/apis/pgv2.percona.com/v2/zz_generated.deepcopy.go @@ -11,6 +11,7 @@ package v2 import ( "github.com/percona/percona-postgresql-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -792,6 +793,13 @@ func (in *PerconaPGClusterStatus) DeepCopyInto(out *PerconaPGClusterStatus) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PerconaPGClusterStatus.