diff --git a/build/crd/crunchy/generated/postgres-operator.crunchydata.com_postgresclusters.yaml b/build/crd/crunchy/generated/postgres-operator.crunchydata.com_postgresclusters.yaml index 5be36f1ce..a443f3873 100644 --- a/build/crd/crunchy/generated/postgres-operator.crunchydata.com_postgresclusters.yaml +++ b/build/crd/crunchy/generated/postgres-operator.crunchydata.com_postgresclusters.yaml @@ -8352,6 +8352,8 @@ spec: properties: pgAudit: type: boolean + pgRepack: + type: boolean pgStatMonitor: type: boolean pgStatStatements: diff --git a/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml b/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml index 8667d110b..5d0cafbae 100644 --- a/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml +++ b/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml @@ -8066,6 +8066,8 @@ spec: properties: pg_audit: type: boolean + pg_repack: + type: boolean pg_stat_monitor: type: boolean pgvector: diff --git a/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml b/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml index 9a3d3af3a..d6385c7b5 100644 --- a/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml +++ b/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml @@ -8471,6 +8471,8 @@ spec: properties: pg_audit: type: boolean + pg_repack: + type: boolean pg_stat_monitor: type: boolean pgvector: diff --git a/config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml b/config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml index 1fcedcebb..6ef7b124f 100644 --- a/config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml +++ b/config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml @@ -8312,6 +8312,8 @@ spec: properties: pgAudit: type: boolean + pgRepack: + type: boolean pgStatMonitor: type: boolean pgStatStatements: diff --git a/deploy/bundle.yaml b/deploy/bundle.yaml index a021a49b9..72eeab952 100644 --- a/deploy/bundle.yaml +++ b/deploy/bundle.yaml @@ -8768,6 +8768,8 @@ spec: properties: pg_audit: type: boolean + pg_repack: + type: boolean pg_stat_monitor: type: boolean pgvector: @@ -34002,6 +34004,8 @@ spec: properties: pgAudit: type: boolean + pgRepack: + type: boolean pgStatMonitor: type: boolean pgStatStatements: diff --git a/deploy/cr.yaml b/deploy/cr.yaml index 9321457f9..e3c687051 100644 --- a/deploy/cr.yaml +++ b/deploy/cr.yaml @@ -508,6 +508,7 @@ spec: # pg_stat_monitor: true # pg_audit: true # pgvector: false +# pg_repack: false # custom: # - name: pg_cron # version: 1.6.1 diff --git a/deploy/crd.yaml b/deploy/crd.yaml index b91217fc2..558b8e976 100644 --- a/deploy/crd.yaml +++ b/deploy/crd.yaml @@ -8768,6 +8768,8 @@ spec: properties: pg_audit: type: boolean + pg_repack: + type: boolean pg_stat_monitor: type: boolean pgvector: @@ -34002,6 +34004,8 @@ spec: properties: pgAudit: type: boolean + pgRepack: + type: boolean pgStatMonitor: type: boolean pgStatStatements: diff --git a/deploy/cw-bundle.yaml b/deploy/cw-bundle.yaml index 3f97a69c3..627b0c31d 100644 --- a/deploy/cw-bundle.yaml +++ b/deploy/cw-bundle.yaml @@ -8768,6 +8768,8 @@ spec: properties: pg_audit: type: boolean + pg_repack: + type: boolean pg_stat_monitor: type: boolean pgvector: @@ -34002,6 +34004,8 @@ spec: properties: pgAudit: type: boolean + pgRepack: + type: boolean pgStatMonitor: type: boolean pgStatStatements: diff --git a/internal/controller/postgrescluster/postgres.go b/internal/controller/postgrescluster/postgres.go index b9a35d62d..22e9e5453 100644 --- a/internal/controller/postgrescluster/postgres.go +++ b/internal/controller/postgrescluster/postgres.go @@ -30,6 +30,7 @@ import ( "github.com/percona/percona-postgresql-operator/internal/logging" "github.com/percona/percona-postgresql-operator/internal/naming" "github.com/percona/percona-postgresql-operator/internal/pgaudit" + "github.com/percona/percona-postgresql-operator/internal/pgrepack" "github.com/percona/percona-postgresql-operator/internal/pgstatmonitor" "github.com/percona/percona-postgresql-operator/internal/pgstatstatements" "github.com/percona/percona-postgresql-operator/internal/pgvector" @@ -245,7 +246,7 @@ func (r *Reconciler) reconcilePostgresDatabases( // Calculate a hash of the SQL that should be executed in PostgreSQL. // K8SPG-375, K8SPG-577, K8SPG-699 - var pgAuditOK, pgStatMonitorOK, pgStatStatementsOK, pgvectorOK, postgisInstallOK bool + var pgAuditOK, pgStatMonitorOK, pgStatStatementsOK, pgvectorOK, pgRepackOK, postgisInstallOK bool create := func(ctx context.Context, exec postgres.Executor) error { // validate version string before running it in database _, err := gover.NewVersion(cluster.Labels[naming.LabelVersion]) @@ -319,6 +320,19 @@ func (r *Reconciler) reconcilePostgresDatabases( } } + // K8SPG-574 + if cluster.Spec.Extensions.PGRepack { + if pgRepackOK = pgrepack.EnableInPostgreSQL(ctx, exec) == nil; !pgRepackOK { + r.Recorder.Event(cluster, corev1.EventTypeWarning, "pgRepackDisabled", + "Unable to install pg_repack") + } + } else { + if pgRepackOK = pgrepack.DisableInPostgreSQL(ctx, exec) == nil; !pgRepackOK { + r.Recorder.Event(cluster, corev1.EventTypeWarning, "pgRepackEnabled", + "Unable to disable pg_repack") + } + } + // Enabling PostGIS extensions is a one-way operation // e.g., you can take a PostgresCluster and turn it into a PostGISCluster, // but you cannot reverse the process, as that would potentially remove an extension @@ -364,7 +378,7 @@ func (r *Reconciler) reconcilePostgresDatabases( err = errors.WithStack(create(logging.NewContext(ctx, log), podExecutor)) } // K8SPG-472 - if err == nil && pgStatMonitorOK && pgAuditOK && pgvectorOK && postgisInstallOK { + if err == nil && pgStatMonitorOK && pgAuditOK && pgvectorOK && postgisInstallOK && pgRepackOK { cluster.Status.DatabaseRevision = revision } @@ -659,7 +673,6 @@ func (r *Reconciler) reconcilePostgresDataVolume( instanceSpec *v1beta1.PostgresInstanceSetSpec, instance *appsv1.StatefulSet, clusterVolumes []corev1.PersistentVolumeClaim, sourceCluster *v1beta1.PostgresCluster, ) (*corev1.PersistentVolumeClaim, error) { - labelMap := map[string]string{ naming.LabelCluster: cluster.Name, naming.LabelInstanceSet: instanceSpec.Name, @@ -743,7 +756,8 @@ func (r *Reconciler) reconcilePostgresDataVolume( // setVolumeSize compares the potential sizes from the instance spec, status // and limit and sets the appropriate current value. func (r *Reconciler) setVolumeSize(ctx context.Context, cluster *v1beta1.PostgresCluster, - pvc *corev1.PersistentVolumeClaim, instanceSpecName string) { + pvc *corev1.PersistentVolumeClaim, instanceSpecName string, +) { log := logging.FromContext(ctx) // Store the limit for this instance set. This value will not change below. @@ -818,7 +832,6 @@ func (r *Reconciler) reconcileTablespaceVolumes( instanceSpec *v1beta1.PostgresInstanceSetSpec, instance *appsv1.StatefulSet, clusterVolumes []corev1.PersistentVolumeClaim, ) (tablespaceVolumes []*corev1.PersistentVolumeClaim, err error) { - if !feature.Enabled(ctx, feature.TablespaceVolumes) { return } @@ -893,7 +906,6 @@ func (r *Reconciler) reconcilePostgresWALVolume( instanceSpec *v1beta1.PostgresInstanceSetSpec, instance *appsv1.StatefulSet, observed *Instance, clusterVolumes []corev1.PersistentVolumeClaim, ) (*corev1.PersistentVolumeClaim, error) { - labelMap := map[string]string{ naming.LabelCluster: cluster.Name, naming.LabelInstanceSet: instanceSpec.Name, @@ -992,7 +1004,8 @@ func (r *Reconciler) reconcilePostgresWALVolume( // DatabaseInitSQL is defined, the function will find the primary pod and run // SQL from the defined ConfigMap func (r *Reconciler) reconcileDatabaseInitSQL(ctx context.Context, - cluster *v1beta1.PostgresCluster, instances *observedInstances) error { + cluster *v1beta1.PostgresCluster, instances *observedInstances, +) error { log := logging.FromContext(ctx) // Spec is not defined, unset status and return diff --git a/internal/pgrepack/postgres.go b/internal/pgrepack/postgres.go new file mode 100644 index 000000000..16fd7104b --- /dev/null +++ b/internal/pgrepack/postgres.go @@ -0,0 +1,39 @@ +package pgrepack + +import ( + "context" + + "github.com/percona/percona-postgresql-operator/internal/logging" + "github.com/percona/percona-postgresql-operator/internal/postgres" +) + +// EnableInPostgreSQL installs pg_repack triggers into every database. +func EnableInPostgreSQL(ctx context.Context, exec postgres.Executor) error { + log := logging.FromContext(ctx) + + stdout, stderr, err := exec.ExecInAllDatabases(ctx, + `SET client_min_messages = WARNING; CREATE EXTENSION IF NOT EXISTS pg_repack; ALTER EXTENSION pg_repack UPDATE;`, + map[string]string{ + "ON_ERROR_STOP": "on", // Abort when any one command fails. + "QUIET": "on", // Do not print successful commands to stdout. + }) + + log.V(1).Info("enabled pg_repack", "stdout", stdout, "stderr", stderr) + + return err +} + +func DisableInPostgreSQL(ctx context.Context, exec postgres.Executor) error { + log := logging.FromContext(ctx) + + stdout, stderr, err := exec.ExecInAllDatabases(ctx, + `SET client_min_messages = WARNING; DROP EXTENSION IF EXISTS pg_repack;`, + map[string]string{ + "ON_ERROR_STOP": "on", // Abort when any one command fails. + "QUIET": "on", // Do not print successful commands to stdout. + }) + + log.V(1).Info("disabled pg_repack", "stdout", stdout, "stderr", stderr) + + return err +} diff --git a/internal/pgrepack/postgres_test.go b/internal/pgrepack/postgres_test.go new file mode 100644 index 000000000..dae4b5c58 --- /dev/null +++ b/internal/pgrepack/postgres_test.go @@ -0,0 +1,61 @@ +package pgrepack + +import ( + "context" + "errors" + "io" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestEnableInPostgreSQL(t *testing.T) { + expected := errors.New("whoops") + exec := func( + _ context.Context, stdin io.Reader, stdout, stderr io.Writer, command ...string, + ) error { + assert.Assert(t, stdout != nil, "should capture stdout") + assert.Assert(t, stderr != nil, "should capture stderr") + + assert.Assert(t, strings.Contains(strings.Join(command, "\n"), + `SELECT datname FROM pg_catalog.pg_database`, + ), "expected all databases and templates") + + b, err := io.ReadAll(stdin) + assert.NilError(t, err) + assert.Equal(t, string(b), strings.Trim(` +SET client_min_messages = WARNING; CREATE EXTENSION IF NOT EXISTS pg_repack; ALTER EXTENSION pg_repack UPDATE; + `, "\t\n")) + + return expected + } + + ctx := context.Background() + assert.Equal(t, expected, EnableInPostgreSQL(ctx, exec)) +} + +func TestDisableInPostgreSQL(t *testing.T) { + expected := errors.New("whoops") + exec := func( + _ context.Context, stdin io.Reader, stdout, stderr io.Writer, command ...string, + ) error { + assert.Assert(t, stdout != nil, "should capture stdout") + assert.Assert(t, stderr != nil, "should capture stderr") + + assert.Assert(t, strings.Contains(strings.Join(command, "\n"), + `SELECT datname FROM pg_catalog.pg_database`, + ), "expected all databases and templates") + + b, err := io.ReadAll(stdin) + assert.NilError(t, err) + assert.Equal(t, string(b), strings.Trim(` +SET client_min_messages = WARNING; DROP EXTENSION IF EXISTS pg_repack; + `, "\t\n")) + + return expected + } + + ctx := context.Background() + assert.Equal(t, expected, DisableInPostgreSQL(ctx, exec)) +} diff --git a/pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go b/pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go index a7552e19b..2d4fafcae 100644 --- a/pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go +++ b/pkg/apis/pgv2.percona.com/v2/perconapgcluster_types.go @@ -229,6 +229,9 @@ func (cr *PerconaPGCluster) Default() { if cr.Spec.Extensions.BuiltIn.PGVector == nil { cr.Spec.Extensions.BuiltIn.PGVector = &f } + if cr.Spec.Extensions.BuiltIn.PGRepack == nil { + cr.Spec.Extensions.BuiltIn.PGRepack = &f + } if cr.CompareVersion("2.6.0") >= 0 && cr.Spec.AutoCreateUserSchema == nil { cr.Spec.AutoCreateUserSchema = &t @@ -352,6 +355,7 @@ func (cr *PerconaPGCluster) ToCrunchy(ctx context.Context, postgresCluster *crun postgresCluster.Spec.Extensions.PGStatMonitor = *cr.Spec.Extensions.BuiltIn.PGStatMonitor postgresCluster.Spec.Extensions.PGAudit = *cr.Spec.Extensions.BuiltIn.PGAudit postgresCluster.Spec.Extensions.PGVector = *cr.Spec.Extensions.BuiltIn.PGVector + postgresCluster.Spec.Extensions.PGRepack = *cr.Spec.Extensions.BuiltIn.PGRepack postgresCluster.Spec.TLSOnly = cr.Spec.TLSOnly @@ -596,6 +600,7 @@ type BuiltInExtensionsSpec struct { PGStatMonitor *bool `json:"pg_stat_monitor,omitempty"` PGAudit *bool `json:"pg_audit,omitempty"` PGVector *bool `json:"pgvector,omitempty"` + PGRepack *bool `json:"pg_repack,omitempty"` } type ExtensionsSpec 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 1c482ad48..abf35a50b 100644 --- a/pkg/apis/pgv2.percona.com/v2/zz_generated.deepcopy.go +++ b/pkg/apis/pgv2.percona.com/v2/zz_generated.deepcopy.go @@ -54,6 +54,11 @@ func (in *BuiltInExtensionsSpec) DeepCopyInto(out *BuiltInExtensionsSpec) { *out = new(bool) **out = **in } + if in.PGRepack != nil { + in, out := &in.PGRepack, &out.PGRepack + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BuiltInExtensionsSpec. diff --git a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go index bb9dc2924..53ad03fa6 100644 --- a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go +++ b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go @@ -194,6 +194,7 @@ type ExtensionsSpec struct { PGAudit bool `json:"pgAudit,omitempty"` PGStatStatements bool `json:"pgStatStatements,omitempty"` PGVector bool `json:"pgvector,omitempty"` + PGRepack bool `json:"pgRepack,omitempty"` } // DataSource defines data sources for a new PostgresCluster.