diff --git a/operator/src/go.mod b/operator/src/go.mod index 8156141fc..5dbcc9faa 100644 --- a/operator/src/go.mod +++ b/operator/src/go.mod @@ -68,7 +68,7 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/cel-go v0.26.0 // indirect github.com/google/gnostic-models v0.7.1 // indirect - github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect @@ -126,5 +126,5 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect sigs.k8s.io/gateway-api v1.4.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect + sigs.k8s.io/yaml v1.6.0 ) diff --git a/operator/src/internal/cnpg/cnpg_cluster.go b/operator/src/internal/cnpg/cnpg_cluster.go index 8b9bc8e14..3ba97650d 100644 --- a/operator/src/internal/cnpg/cnpg_cluster.go +++ b/operator/src/internal/cnpg/cnpg_cluster.go @@ -24,27 +24,30 @@ import ( // GetCnpgClusterSpec renders the Cluster for a DocumentDB instance. The // documentdbImage argument overrides the resolved extension image; pass "" to -// use the image resolved from the instance. Retained for callers that supply an -// explicit extension image. +// use the image resolved from the instance. req and serviceAccountName are +// accepted for call-site compatibility but no longer consumed: object +// coordinates now come from the intent's Identity, and the service account is +// not read by the renderer. func GetCnpgClusterSpec(req ctrl.Request, documentdb *dbpreview.DocumentDB, documentdbImage, serviceAccountName, storageClass string, isPrimaryRegion bool, log logr.Logger) *cnpgv1.Cluster { intent := product.DocumentDBAdapter{}.ToClusterIntent(documentdb) if documentdbImage != "" { intent.Images.PostgresExtension = documentdbImage } - return GetCnpgClusterSpecFromIntent(req, documentdb, intent, serviceAccountName, storageClass, isPrimaryRegion, log) + return GetCnpgClusterSpecFromIntent(intent, storageClass, isPrimaryRegion, log) } // GetCnpgClusterSpecFromIntent renders a CNPG Cluster from a product-neutral -// ClusterIntent. This is the seam the reconciler drives: product-varying values -// (images, credential secret, plugin name) come from the intent rather than from -// product-specific lookups. -func GetCnpgClusterSpecFromIntent(req ctrl.Request, documentdb *dbpreview.DocumentDB, intent product.ClusterIntent, serviceAccountName, storageClass string, isPrimaryRegion bool, log logr.Logger) *cnpgv1.Cluster { - split := ComputeResourceSplit(documentdb, DefaultSplitConfig()) +// ClusterIntent plus reconcile-time inputs (storage class, region role) that +// are not part of any product's spec. This is the seam the reconciler drives: +// every render input comes from these arguments rather than from loose +// parameters or product-specific lookups. +func GetCnpgClusterSpecFromIntent(intent product.ClusterIntent, storageClass string, isPrimaryRegion bool, log logr.Logger) *cnpgv1.Cluster { + split := ComputeResourceSplitFromResource(intent.Resource, intent.Monitoring.Enabled, DefaultSplitConfig()) - sidecarPluginName := intent.SidecarInjectorPlugin + sidecarPluginName := intent.Plugins.SidecarInjectorName gatewayImage := intent.Images.Gateway - log.Info("Creating CNPG cluster with gateway image", "gatewayImage", gatewayImage, "documentdbName", documentdb.Name, "specGatewayImage", imageGateway(documentdb)) + log.Info("Creating CNPG cluster with gateway image", "gatewayImage", gatewayImage, "documentdbName", intent.Identity.Name) credentialSecretName := intent.CredentialSecret @@ -65,8 +68,8 @@ func GetCnpgClusterSpecFromIntent(req ctrl.Request, documentdb *dbpreview.Docume return &cnpgv1.Cluster{ ObjectMeta: metav1.ObjectMeta{ - Name: req.Name, - Namespace: req.Namespace, + Name: intent.Identity.Name, + Namespace: intent.Identity.Namespace, OwnerReferences: []metav1.OwnerReference{ { APIVersion: intent.Identity.APIVersion, @@ -80,7 +83,7 @@ func GetCnpgClusterSpecFromIntent(req ctrl.Request, documentdb *dbpreview.Docume }, Spec: func() cnpgv1.ClusterSpec { spec := cnpgv1.ClusterSpec{ - Instances: intent.Topology.Instances, + Instances: intent.Topology.InstancesPerNode, ImageName: intent.Images.Postgres, ImagePullSecrets: toCNPGImagePullSecrets(intent.Images.PullSecrets), PrimaryUpdateMethod: cnpgv1.PrimaryUpdateMethodSwitchover, @@ -102,26 +105,26 @@ func GetCnpgClusterSpecFromIntent(req ctrl.Request, documentdb *dbpreview.Docume addPluginParamIfSet(params, util.PLUGIN_PARAM_GATEWAY_CPU_REQUEST, split.Gateway.CPURequest) addPluginParamIfSet(params, util.PLUGIN_PARAM_GATEWAY_CPU_LIMIT, split.Gateway.CPULimit) // If TLS is ready, surface secret name to plugin so it can mount certs. - if documentdb.Status.TLS != nil && documentdb.Status.TLS.Ready && documentdb.Status.TLS.SecretName != "" { - params["gatewayTLSSecret"] = documentdb.Status.TLS.SecretName + if intent.TLS.GatewaySecretName != "" { + params["gatewayTLSSecret"] = intent.TLS.GatewaySecretName } // Pass monitoring parameters to plugin for OTel sidecar injection. // Sidecar is only injected when monitoring is enabled. // Config hash triggers operator-initiated rolling restart on config changes. if split.MonitoringEnabled { params["otelCollectorImage"] = util.DEFAULT_OTEL_COLLECTOR_IMAGE - params["otelConfigMapName"] = otelcfg.ConfigMapName(documentdb.Name) + params["otelConfigMapName"] = otelcfg.ConfigMapName(intent.Identity.Name) addPluginParamIfSet(params, util.PLUGIN_PARAM_OTEL_MEMORY_REQUEST, split.OTel.MemoryRequest) addPluginParamIfSet(params, util.PLUGIN_PARAM_OTEL_MEMORY_LIMIT, split.OTel.MemoryLimit) addPluginParamIfSet(params, util.PLUGIN_PARAM_OTEL_CPU_REQUEST, split.OTel.CPURequest) addPluginParamIfSet(params, util.PLUGIN_PARAM_OTEL_CPU_LIMIT, split.OTel.CPULimit) - if promPort := otelcfg.ResolvePrometheusPort(documentdb.Spec.Monitoring); promPort > 0 { + if promPort := otelcfg.ResolvePrometheusPort(intent.Monitoring); promPort > 0 { params["prometheusPort"] = fmt.Sprintf("%d", promPort) } // Compute config hash for change detection. The operator triggers a // rolling restart (via restart annotation) when plugin parameters // change, ensuring pods pick up new config. - if configData, err := otelcfg.GenerateConfigMapData(documentdb.Name, req.Namespace, documentdb.Spec.Monitoring); err == nil { + if configData, err := otelcfg.GenerateConfigMapData(intent.Identity.Name, intent.Identity.Namespace, intent.Monitoring); err == nil { params["otelConfigHash"] = otelcfg.HashConfigMapData(configData) } else { log.Error(err, "Failed to generate OTel config hash; config changes may not trigger rolling restart") @@ -133,10 +136,10 @@ func GetCnpgClusterSpecFromIntent(req ctrl.Request, documentdb *dbpreview.Docume Parameters: params, }} }(), - PostgresConfiguration: buildPostgresConfiguration(documentdb, extensionImageSource, split.PostgresMemoryBytes), + PostgresConfiguration: buildPostgresConfiguration(MergeParametersResolved(intent.Postgres.Parameters, intent.FeatureGates, split.PostgresMemoryBytes), extensionImageSource), Bootstrap: bootstrapConfigurationFromIntent(intent, isPrimaryRegion, log), - LogLevel: cmp.Or(documentdb.Spec.LogLevel, "info"), - Certificates: postgresCertificates(documentdb), + LogLevel: cmp.Or(intent.LogLevel, "info"), + Certificates: intent.TLS.PostgresCertificates, Backup: &cnpgv1.BackupConfiguration{ VolumeSnapshot: &cnpgv1.VolumeSnapshotConfiguration{ SnapshotOwnerReference: "backup", // Set owner reference to 'backup' so that snapshots are deleted when Backup resource is deleted @@ -146,10 +149,10 @@ func GetCnpgClusterSpecFromIntent(req ctrl.Request, documentdb *dbpreview.Docume Affinity: intent.Topology.Affinity, Resources: buildResourceRequirements(split.Postgres), } - spec.MaxStopDelay = getMaxStopDelayOrDefault(documentdb) + spec.MaxStopDelay = intent.Timeouts.StopDelay applyPostgresProcessIdentity(&spec, intent) applyIOUringSeccomp(&spec, intent) - applyOtelMonitorRole(&spec, documentdb) + applyOtelMonitorRoleFromIntent(&spec, intent.Monitoring.Enabled) return spec }(), @@ -209,8 +212,8 @@ func bootstrapConfigurationFromIntent(intent product.ClusterIntent, isPrimaryReg return defaultBootstrapConfigurationFromIntent(intent) } -// getBootstrapConfiguration adapts a DocumentDB instance onto the intent-based -// bootstrap builder. Retained for direct callers that hold the custom resource. +// getBootstrapConfiguration builds the bootstrap configuration for a DocumentDB +// instance via the intent-based builder. func getBootstrapConfiguration(documentdb *dbpreview.DocumentDB, isPrimaryRegion bool, log logr.Logger) *cnpgv1.BootstrapConfiguration { return bootstrapConfigurationFromIntent(product.DocumentDBAdapter{}.ToClusterIntent(documentdb), isPrimaryRegion, log) } @@ -231,20 +234,12 @@ func defaultBootstrapConfigurationFromIntent(intent product.ClusterIntent) *cnpg } } -// getDefaultBootstrapConfiguration adapts a DocumentDB instance onto the -// intent-based default bootstrap builder. Retained for direct callers. +// getDefaultBootstrapConfiguration builds the default bootstrap configuration for +// a DocumentDB instance via the intent-based builder. func getDefaultBootstrapConfiguration(documentdb *dbpreview.DocumentDB) *cnpgv1.BootstrapConfiguration { return defaultBootstrapConfigurationFromIntent(product.DocumentDBAdapter{}.ToClusterIntent(documentdb)) } -// getMaxStopDelayOrDefault returns StopDelay if set, otherwise util.CNPG_DEFAULT_STOP_DELAY -func getMaxStopDelayOrDefault(documentdb *dbpreview.DocumentDB) int32 { - if documentdb.Spec.Timeouts.StopDelay != 0 { - return documentdb.Spec.Timeouts.StopDelay - } - return util.CNPG_DEFAULT_STOP_DELAY -} - // parseMemoryToBytes converts a Kubernetes quantity string (e.g., "2Gi", "4096Mi") // to bytes. Returns 0 if the string is empty or "0" (meaning unlimited/unset). func parseMemoryToBytes(memoryStr string) int64 { @@ -311,22 +306,6 @@ func parsePullPolicy(value string) corev1.PullPolicy { } } -// imageGateway returns spec.image.gateway or empty string when unset. -// Nil-safe. -func imageGateway(documentdb *dbpreview.DocumentDB) string { - if documentdb == nil || documentdb.Spec.Image == nil { - return "" - } - return documentdb.Spec.Image.Gateway -} - -func postgresCertificates(documentdb *dbpreview.DocumentDB) *cnpgv1.CertificatesConfiguration { - if documentdb.Spec.TLS == nil { - return nil - } - return documentdb.Spec.TLS.Postgres -} - // toCNPGImagePullSecrets translates a list of corev1.LocalObjectReference // (the Kubernetes-native shape used on spec.imagePullSecrets) into the // CNPG-flavoured cnpgv1.LocalObjectReference shape that @@ -387,15 +366,12 @@ func applyIOUringSeccomp(spec *cnpgv1.ClusterSpec, intent product.ClusterIntent) // PostgreSQL host authentication currently uses trust, so a generated password // would not be checked. Disable the role password explicitly until authentication // is tightened rather than creating an unused credential and widening Secret RBAC. -func applyOtelMonitorRole(spec *cnpgv1.ClusterSpec, documentdb *dbpreview.DocumentDB) { - if documentdb == nil { - return - } +func applyOtelMonitorRoleFromIntent(spec *cnpgv1.ClusterSpec, monitoringEnabled bool) { if spec.Managed == nil { spec.Managed = &cnpgv1.ManagedConfiguration{} } role := absentOtelMonitorRole() - if documentdb.Spec.Monitoring != nil && documentdb.Spec.Monitoring.Enabled { + if monitoringEnabled { // The current health query is SELECT 1, so the role needs LOGIN only // and is not granted broad monitoring memberships. role = cnpgv1.RoleConfiguration{ @@ -429,7 +405,7 @@ func absentOtelMonitorRole() cnpgv1.RoleConfiguration { // stanza (mounted from spec.image.documentDB as an ImageVolumeSource), // sets a fixed AdditionalLibraries list, and applies a small set of // operator-managed GUCs. -func buildPostgresConfiguration(documentdb *dbpreview.DocumentDB, extensionImageSource corev1.ImageVolumeSource, pgMemoryBytes int64) cnpgv1.PostgresConfiguration { +func buildPostgresConfiguration(parameters map[string]string, extensionImageSource corev1.ImageVolumeSource) cnpgv1.PostgresConfiguration { pgHBA := []string{ "host all all localhost trust", "hostssl replication streaming_replica all cert", @@ -446,7 +422,7 @@ func buildPostgresConfiguration(documentdb *dbpreview.DocumentDB, extensionImage }, }, AdditionalLibraries: []string{"pg_cron", "pg_documentdb_core", "pg_documentdb"}, - Parameters: MergeParameters(documentdb, pgMemoryBytes), + Parameters: parameters, PgHBA: pgHBA, } } diff --git a/operator/src/internal/cnpg/cnpg_cluster_test.go b/operator/src/internal/cnpg/cnpg_cluster_test.go index 9a2c80f56..7d86c2929 100644 --- a/operator/src/internal/cnpg/cnpg_cluster_test.go +++ b/operator/src/internal/cnpg/cnpg_cluster_test.go @@ -17,6 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" dbpreview "github.com/documentdb/documentdb-operator/api/preview" + "github.com/documentdb/documentdb-operator/internal/product" util "github.com/documentdb/documentdb-operator/internal/utils" ) @@ -263,6 +264,10 @@ var _ = Describe("GetCnpgClusterSpec", func() { req.Namespace = "default" documentdb := &dbpreview.DocumentDB{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "default", + }, Spec: dbpreview.DocumentDBSpec{ InstancesPerNode: 3, Image: &dbpreview.ImageSpec{ @@ -1077,7 +1082,7 @@ func TestGetInheritedMetadataLabels(t *testing.T) { } } -func TestGetMaxStopDelayOrDefault(t *testing.T) { +func TestMaxStopDelayFromIntent(t *testing.T) { tests := []struct { name string documentdb *dbpreview.DocumentDB @@ -1127,7 +1132,7 @@ func TestGetMaxStopDelayOrDefault(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := getMaxStopDelayOrDefault(tt.documentdb) + result := product.DocumentDBAdapter{}.ToClusterIntent(tt.documentdb).Timeouts.StopDelay if result != tt.expected { t.Errorf("Expected %d, got %d", tt.expected, result) diff --git a/operator/src/internal/cnpg/cnpg_intent_test.go b/operator/src/internal/cnpg/cnpg_intent_test.go index 1aee58ade..28244fe62 100644 --- a/operator/src/internal/cnpg/cnpg_intent_test.go +++ b/operator/src/internal/cnpg/cnpg_intent_test.go @@ -40,11 +40,13 @@ var _ = Describe("GetCnpgClusterSpecFromIntent", func() { PostgresExtension: "reg/ext:test", Gateway: "reg/gw:test", }, - CredentialSecret: "custom-secret", - SidecarInjectorPlugin: "custom-injector.example.io", + CredentialSecret: "custom-secret", + Plugins: product.Plugins{SidecarInjectorName: "custom-injector.example.io"}, } - result := GetCnpgClusterSpecFromIntent(newRequest(), newDocumentDB(), intent, "test-sa", "", true, log) + intent.Identity.Name = "test-cluster" + intent.Identity.Namespace = "default" + result := GetCnpgClusterSpecFromIntent(intent, "", true, log) Expect(result.Spec.PostgresConfiguration.Extensions[0].ImageVolumeSource.Reference).To(Equal("reg/ext:test")) Expect(result.Spec.Plugins[0].Name).To(Equal("custom-injector.example.io")) @@ -56,7 +58,10 @@ var _ = Describe("GetCnpgClusterSpecFromIntent", func() { documentdb := newDocumentDB() intent := product.DocumentDBAdapter{}.ToClusterIntent(documentdb) - fromIntent := GetCnpgClusterSpecFromIntent(newRequest(), documentdb, intent, "test-sa", "", true, log) + intent.Identity.Name = "test-cluster" + intent.Identity.Namespace = "default" + + fromIntent := GetCnpgClusterSpecFromIntent(intent, "", true, log) fromWrapper := GetCnpgClusterSpec(newRequest(), documentdb, "", "test-sa", "", true, log) Expect(fromIntent.Spec.PostgresConfiguration.Extensions[0].ImageVolumeSource.Reference). diff --git a/operator/src/internal/cnpg/cnpg_render_golden_test.go b/operator/src/internal/cnpg/cnpg_render_golden_test.go new file mode 100644 index 000000000..fb6462cd0 --- /dev/null +++ b/operator/src/internal/cnpg/cnpg_render_golden_test.go @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package cnpg + +import ( + "flag" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/yaml" + + cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" + dbpreview "github.com/documentdb/documentdb-operator/api/preview" +) + +var updateGolden = flag.Bool("update-golden", false, "rewrite golden render fixtures") + +var goldenCerts = cnpgv1.CertificatesConfiguration{ServerCASecret: "pg-ca", ServerTLSSecret: "pg-tls"} + +// TestRenderGolden renders the full CNPG Cluster for a DocumentDB spec matrix and +// compares it against committed golden fixtures. It is the render regression +// guard for the DocumentDB -> ClusterIntent -> CNPG pipeline: any change that +// alters a rendered Cluster must be reflected in testdata/render/*.golden.yaml. +// Regenerate with: go test ./internal/cnpg -run TestRenderGolden -update-golden +func TestRenderGolden(t *testing.T) { + log := zap.New() + req := ctrl.Request{} + req.Name = "drift" + req.Namespace = "default" + + ptr := func(v int64) *int64 { return &v } + + cases := map[string]*dbpreview.DocumentDB{ + "minimal": { + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}}, + }, + }, + "custom-loglevel-and-stopdelay": { + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + LogLevel: "debug", + Timeouts: dbpreview.Timeouts{StopDelay: 120}, + Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}}, + }, + }, + "user-params": { + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}}, + Postgres: &dbpreview.PostgresSpec{ + Parameters: map[string]string{"work_mem": "64MB", "max_connections": "200"}, + }, + }, + }, + "resource-envelope": { + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{ + Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}, + Memory: "8Gi", + CPU: "4", + }, + }, + }, + "resource-overrides": { + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{ + Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}, + Gateway: &dbpreview.ComponentResources{Memory: "512Mi", CPU: "500m"}, + Database: &dbpreview.ComponentResources{Memory: "4Gi", CPU: "2"}, + }, + }, + }, + "process-identity": { + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}}, + Postgres: &dbpreview.PostgresSpec{UID: ptr(26), GID: ptr(26)}, + }, + }, + "iouring-and-changestreams": { + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}}, + FeatureGates: map[string]bool{ + string(dbpreview.FeatureGateIOUring): true, + string(dbpreview.FeatureGateChangeStreams): true, + }, + }, + }, + "postgres-tls": { + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}}, + TLS: &dbpreview.TLSConfiguration{ + Postgres: &goldenCerts, + }, + }, + }, + "gateway-tls-ready": { + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}}, + }, + Status: dbpreview.DocumentDBStatus{ + TLS: &dbpreview.TLSStatus{Ready: true, SecretName: "gw-tls-secret"}, + }, + }, + "monitoring-prometheus": { + ObjectMeta: metav1.ObjectMeta{Name: "mon-prom"}, + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}}, + Monitoring: &dbpreview.MonitoringSpec{ + Enabled: true, + Exporter: &dbpreview.ExporterSpec{ + Prometheus: &dbpreview.PrometheusExporterSpec{Port: 9090}, + }, + }, + }, + }, + "monitoring-otlp-and-prometheus": { + ObjectMeta: metav1.ObjectMeta{Name: "mon-both"}, + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"}}, + Monitoring: &dbpreview.MonitoringSpec{ + Enabled: true, + Exporter: &dbpreview.ExporterSpec{ + OTLP: &dbpreview.OTLPExporterSpec{Endpoint: "otel-collector:4317"}, + Prometheus: &dbpreview.PrometheusExporterSpec{}, + }, + }, + }, + }, + } + + for name, db := range cases { + t.Run(name, func(t *testing.T) { + cluster := GetCnpgClusterSpec(req, db, "", "test-sa", "", true, log) + got, err := yaml.Marshal(cluster) + if err != nil { + t.Fatalf("marshal cluster: %v", err) + } + path := filepath.Join("testdata", "render", name+".golden.yaml") + if *updateGolden { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, got, 0o644); err != nil { + t.Fatal(err) + } + return + } + want, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read golden (regenerate with -update-golden): %v", err) + } + if diff := cmp.Diff(string(want), string(got)); diff != "" { + t.Errorf("render drift (-want +got):\n%s", diff) + } + }) + } +} diff --git a/operator/src/internal/cnpg/pg_defaults.go b/operator/src/internal/cnpg/pg_defaults.go index 54d8eb51e..08313609e 100644 --- a/operator/src/internal/cnpg/pg_defaults.go +++ b/operator/src/internal/cnpg/pg_defaults.go @@ -7,6 +7,7 @@ import ( "fmt" dbpreview "github.com/documentdb/documentdb-operator/api/preview" + "github.com/documentdb/documentdb-operator/internal/product" ) // formatMB formats a megabyte value as a PostgreSQL size string. @@ -81,27 +82,34 @@ func StaticDefaults() map[string]string { // ProtectedParameters returns parameters that are always force-set by the // operator and cannot be overridden by users. func ProtectedParameters(documentdb *dbpreview.DocumentDB) map[string]string { + return protectedParameters(product.FeatureGates{ + IOUring: dbpreview.IsFeatureGateEnabled(documentdb, dbpreview.FeatureGateIOUring), + }) +} + +// protectedParameters returns the neutral operator-owned GUCs (always highest +// priority) resolved from the product-neutral feature gates. +func protectedParameters(gates product.FeatureGates) map[string]string { params := map[string]string{ "cron.database_name": "postgres", "max_replication_slots": "10", "max_wal_senders": "10", "max_prepared_transactions": "100", } - if dbpreview.IsFeatureGateEnabled(documentdb, dbpreview.FeatureGateChangeStreams) { - params["wal_level"] = "logical" - } - if dbpreview.IsFeatureGateEnabled(documentdb, dbpreview.FeatureGateIOUring) { + if gates.IOUring { params["io_method"] = "io_uring" } return params } -// MergeParameters merges all parameter sources in priority order (last write wins): -// 1. StaticDefaults -// 2. ComputeMemoryAwareDefaults -// 3. User overrides (documentdb.Spec.Postgres.Parameters) -// 4. ProtectedParameters (always wins) -func MergeParameters(documentdb *dbpreview.DocumentDB, memoryLimitBytes int64) map[string]string { +// MergeParametersResolved merges all parameter sources in priority order (last +// write wins): +// 1. StaticDefaults +// 2. ComputeMemoryAwareDefaults +// 3. userParams (adapter-resolved: user overrides plus product-mandated defaults +// such as change streams' wal_level=logical) +// 4. ProtectedParameters (always wins) +func MergeParametersResolved(userParams map[string]string, gates product.FeatureGates, memoryLimitBytes int64) map[string]string { result := make(map[string]string) for k, v := range StaticDefaults() { @@ -110,12 +118,10 @@ func MergeParameters(documentdb *dbpreview.DocumentDB, memoryLimitBytes int64) m for k, v := range ComputeMemoryAwareDefaults(memoryLimitBytes) { result[k] = v } - if documentdb.Spec.Postgres != nil { - for k, v := range documentdb.Spec.Postgres.Parameters { - result[k] = v - } + for k, v := range userParams { + result[k] = v } - for k, v := range ProtectedParameters(documentdb) { + for k, v := range protectedParameters(gates) { result[k] = v } diff --git a/operator/src/internal/cnpg/pg_defaults_test.go b/operator/src/internal/cnpg/pg_defaults_test.go index f9cdeea5d..69440c5c3 100644 --- a/operator/src/internal/cnpg/pg_defaults_test.go +++ b/operator/src/internal/cnpg/pg_defaults_test.go @@ -8,8 +8,16 @@ import ( . "github.com/onsi/gomega" dbpreview "github.com/documentdb/documentdb-operator/api/preview" + "github.com/documentdb/documentdb-operator/internal/product" ) +// mergeParams merges PostgreSQL parameters for a DocumentDB through the +// product-neutral MergeParametersResolved seam. +func mergeParams(documentdb *dbpreview.DocumentDB, memoryLimitBytes int64) map[string]string { + intent := product.DocumentDBAdapter{}.ToClusterIntent(documentdb) + return MergeParametersResolved(intent.Postgres.Parameters, intent.FeatureGates, memoryLimitBytes) +} + var _ = Describe("formatMB", func() { It("formats plain megabytes", func() { Expect(formatMB(512)).To(Equal("512MB")) @@ -216,8 +224,8 @@ var _ = Describe("ProtectedParameters", func() { result = ProtectedParameters(documentdb) }) - It("sets wal_level to logical", func() { - Expect(result["wal_level"]).To(Equal("logical")) + It("does not set wal_level (change streams contributes it as a resolved parameter, not a protected one)", func() { + Expect(result).NotTo(HaveKey("wal_level")) }) It("still contains other protected params", func() { @@ -261,7 +269,7 @@ var _ = Describe("MergeParameters", func() { }, }, } - result := MergeParameters(documentdb, 0) + result := mergeParams(documentdb, 0) Expect(result["max_connections"]).To(Equal("500")) }) }) @@ -277,7 +285,7 @@ var _ = Describe("MergeParameters", func() { }, }, } - result := MergeParameters(documentdb, 0) + result := mergeParams(documentdb, 0) Expect(result["cron.database_name"]).To(Equal("postgres")) }) }) @@ -287,7 +295,7 @@ var _ = Describe("MergeParameters", func() { documentdb := &dbpreview.DocumentDB{ Spec: dbpreview.DocumentDBSpec{}, } - result := MergeParameters(documentdb, 8*1024*1024*1024) + result := mergeParams(documentdb, 8*1024*1024*1024) Expect(result["shared_buffers"]).To(Equal("2GB")) }) }) @@ -307,7 +315,7 @@ var _ = Describe("MergeParameters", func() { }, }, } - result := MergeParameters(documentdb, 8*1024*1024*1024) + result := mergeParams(documentdb, 8*1024*1024*1024) // User overrides win for non-protected params Expect(result["max_connections"]).To(Equal("500")) @@ -329,7 +337,7 @@ var _ = Describe("MergeParameters", func() { documentdb := &dbpreview.DocumentDB{ Spec: dbpreview.DocumentDBSpec{}, } - result := MergeParameters(documentdb, 8*1024*1024*1024) + result := mergeParams(documentdb, 8*1024*1024*1024) Expect(result["max_connections"]).To(Equal("300")) Expect(result["shared_buffers"]).To(Equal("2GB")) @@ -342,7 +350,7 @@ var _ = Describe("MergeParameters", func() { documentdb := &dbpreview.DocumentDB{ Spec: dbpreview.DocumentDBSpec{}, } - result := MergeParameters(documentdb, 0) + result := mergeParams(documentdb, 0) Expect(result["shared_buffers"]).To(Equal("256MB")) Expect(result["effective_cache_size"]).To(Equal("512MB")) diff --git a/operator/src/internal/cnpg/resource_split.go b/operator/src/internal/cnpg/resource_split.go index 961695f1c..5e3351964 100644 --- a/operator/src/internal/cnpg/resource_split.go +++ b/operator/src/internal/cnpg/resource_split.go @@ -9,7 +9,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" - dbpreview "github.com/documentdb/documentdb-operator/api/preview" + "github.com/documentdb/documentdb-operator/internal/product" util "github.com/documentdb/documentdb-operator/internal/utils" ) @@ -84,9 +84,9 @@ func DefaultSplitConfig() SplitConfig { } } -// ComputeResourceSplit resolves how the pod memory and CPU envelopes -// (spec.resource.memory / spec.resource.cpu) are divided across the PostgreSQL, -// gateway, and (when monitoring is enabled) OTel collector containers. +// ComputeResourceSplitFromResource resolves how the pod memory and CPU envelopes +// (resource.memory / resource.cpu) are divided across the PostgreSQL, gateway, +// and (when monitoring is enabled) OTel collector containers. // // The envelope is OPTIONAL. For each dimension: // - If the envelope is set, the operator carves it: the gateway and OTel @@ -100,19 +100,16 @@ func DefaultSplitConfig() SplitConfig { // PostgreSQL remainder) can only be derived when the envelope is set, so the // omitted-envelope path requires those to be explicit — see ValidateResources. // -// Legacy behavior is preserved: when neither the envelope nor any per-container -// value is set for a dimension, that dimension is left unmanaged (no limits). -func ComputeResourceSplit(documentdb *dbpreview.DocumentDB, cfg SplitConfig) ResourceSplit { - res := documentdb.Spec.Resource - monitoring := documentdb.Spec.Monitoring != nil && documentdb.Spec.Monitoring.Enabled - +// When neither the envelope nor any per-container value is set for a dimension, +// that dimension is left unmanaged (no limits). +func ComputeResourceSplitFromResource(res product.Resource, monitoring bool, cfg SplitConfig) ResourceSplit { envelopeBytes := parseMemoryToBytes(res.Memory) split := ResourceSplit{MonitoringEnabled: monitoring} // --- OTel collector (memory) --- var otelBytes int64 if monitoring { - if componentMemSet(res.OTel) { + if neutralMemSet(res.OTel) { // Explicit override: requests == limits (Guaranteed). split.OTel.setMemory(res.OTel.Memory) otelBytes = parseMemoryToBytes(res.OTel.Memory) @@ -127,7 +124,7 @@ func ComputeResourceSplit(documentdb *dbpreview.DocumentDB, cfg SplitConfig) Res // otherwise the collector keeps its Burstable default (request floor + // a bounded limit ceiling). CPU is compressible, so the carve-out below // only reserves the request from the envelope — the limit just caps burst. - if cpu := componentCPU(res.OTel); cpu != "" { + if cpu := neutralCPU(res.OTel); cpu != "" { split.OTel.setCPU(cpu) } else { split.OTel.CPURequest = cfg.OTelCPURequest @@ -137,7 +134,7 @@ func ComputeResourceSplit(documentdb *dbpreview.DocumentDB, cfg SplitConfig) Res // --- Gateway (memory) --- var gatewayBytes int64 - if componentMemSet(res.Gateway) { + if neutralMemSet(res.Gateway) { split.Gateway.setMemory(res.Gateway.Memory) gatewayBytes = parseMemoryToBytes(res.Gateway.Memory) } else if envelopeBytes > 0 { @@ -147,14 +144,14 @@ func ComputeResourceSplit(documentdb *dbpreview.DocumentDB, cfg SplitConfig) Res // Gateway CPU: explicit override wins, else operator-level limit (request // mirrors the limit so the container is Guaranteed on CPU when bounded). - if cpu := componentCPU(res.Gateway); cpu != "" { + if cpu := neutralCPU(res.Gateway); cpu != "" { split.Gateway.setCPU(cpu) } else if cfg.GatewayCPULimit != "" { split.Gateway.setCPU(cfg.GatewayCPULimit) } // --- PostgreSQL (remainder) --- - if componentMemSet(res.Database) { + if neutralMemSet(res.Database) { split.Postgres.setMemory(res.Database.Memory) split.PostgresMemoryBytes = parseMemoryToBytes(res.Database.Memory) } else if envelopeBytes > 0 { @@ -171,7 +168,7 @@ func ComputeResourceSplit(documentdb *dbpreview.DocumentDB, cfg SplitConfig) Res // PostgreSQL CPU (sink): database override wins; otherwise the pod CPU // envelope minus the gateway and OTel CPU reservations, symmetric with the // memory carve-out so the resolved container CPUs sum to the envelope. - if cpu := componentCPU(res.Database); cpu != "" { + if cpu := neutralCPU(res.Database); cpu != "" { split.Postgres.setCPU(cpu) } else if env := normalizeCPU(res.CPU); env != "" { pgCPU := subtractCPU(env, split.Gateway.CPURequest, split.OTel.CPURequest) @@ -219,14 +216,19 @@ func subtractCPU(envelope string, reserved ...string) string { // --- helpers --- -// componentCPU returns the component's CPU override, or "" when unset/zero. -func componentCPU(c *dbpreview.ComponentResources) string { +// neutralCPU returns the component's CPU override, or "" when unset/zero. +func neutralCPU(c *product.ComponentResource) string { if c == nil { return "" } return normalizeCPU(c.CPU) } +// neutralMemSet reports whether the neutral component has an explicit memory value. +func neutralMemSet(c *product.ComponentResource) bool { + return c != nil && isSet(c.Memory) +} + // normalizeCPU returns cpu unless it is unset/zero, in which case "". func normalizeCPU(cpu string) string { if !isSet(cpu) { diff --git a/operator/src/internal/cnpg/resource_split_test.go b/operator/src/internal/cnpg/resource_split_test.go index 0a25b828c..d6b1a150c 100644 --- a/operator/src/internal/cnpg/resource_split_test.go +++ b/operator/src/internal/cnpg/resource_split_test.go @@ -7,8 +7,16 @@ import ( "testing" dbpreview "github.com/documentdb/documentdb-operator/api/preview" + "github.com/documentdb/documentdb-operator/internal/product" ) +// computeSplit resolves the pod resource carve-out for a DocumentDB through the +// product-neutral ComputeResourceSplitFromResource seam. +func computeSplit(documentdb *dbpreview.DocumentDB, cfg SplitConfig) ResourceSplit { + monitoring := documentdb.Spec.Monitoring != nil && documentdb.Spec.Monitoring.Enabled + return ComputeResourceSplitFromResource(product.ResourceFromSpec(documentdb.Spec.Resource), monitoring, cfg) +} + // prodSplitConfig mirrors the documented production defaults (18.75%, cap 32Gi, // otel 48Mi/128Mi) without depending on environment variables. func prodSplitConfig() SplitConfig { @@ -47,7 +55,7 @@ func TestComputeResourceSplit_ProductionRows(t *testing.T) { cfg := prodSplitConfig() for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - s := ComputeResourceSplit(ddbWithMemory(tc.envelope, false), cfg) + s := computeSplit(ddbWithMemory(tc.envelope, false), cfg) if s.Gateway.MemoryLimit != tc.wantGW { t.Errorf("gateway memory = %q, want %q", s.Gateway.MemoryLimit, tc.wantGW) } @@ -69,7 +77,7 @@ func TestComputeResourceSplit_ProductionRows(t *testing.T) { func TestComputeResourceSplit_MonitoringCarvesOTel(t *testing.T) { cfg := prodSplitConfig() - s := ComputeResourceSplit(ddbWithMemory("16Gi", true), cfg) + s := computeSplit(ddbWithMemory("16Gi", true), cfg) if !s.MonitoringEnabled { t.Fatalf("monitoring should be enabled") @@ -98,7 +106,7 @@ func TestComputeResourceSplit_ExplicitOverridesWin(t *testing.T) { d.Spec.Resource.Database = &dbpreview.ComponentResources{Memory: "10Gi"} d.Spec.Resource.OTel = &dbpreview.ComponentResources{Memory: "256Mi", CPU: "150m"} - s := ComputeResourceSplit(d, cfg) + s := computeSplit(d, cfg) if s.Gateway.MemoryLimit != "2Gi" || s.Gateway.MemoryRequest != "2Gi" { t.Errorf("gateway override not applied: %+v", s.Gateway) @@ -122,7 +130,7 @@ func TestComputeResourceSplit_ExplicitOverridesWin(t *testing.T) { func TestComputeResourceSplit_UnsetMemoryNoCarveOut(t *testing.T) { cfg := prodSplitConfig() // No envelope memory set -> no automatic carve-out (legacy behavior). - s := ComputeResourceSplit(ddbWithMemory("", false), cfg) + s := computeSplit(ddbWithMemory("", false), cfg) if s.Gateway.MemoryLimit != "" || s.Postgres.MemoryLimit != "" { t.Errorf("expected no memory set, got gw=%q pg=%q", s.Gateway.MemoryLimit, s.Postgres.MemoryLimit) } @@ -135,7 +143,7 @@ func TestComputeResourceSplit_CPUFromEnvelope(t *testing.T) { cfg := prodSplitConfig() d := ddbWithMemory("8Gi", false) d.Spec.Resource.CPU = "4" - s := ComputeResourceSplit(d, cfg) + s := computeSplit(d, cfg) if s.Postgres.CPULimit != "4" || s.Postgres.CPURequest != "4" { t.Errorf("postgres cpu = %q/%q, want 4/4", s.Postgres.CPURequest, s.Postgres.CPULimit) } @@ -145,7 +153,7 @@ func TestComputeResourceSplit_GatewayCPULimitDefault(t *testing.T) { cfg := prodSplitConfig() cfg.GatewayCPULimit = "2" d := ddbWithMemory("8Gi", false) - s := ComputeResourceSplit(d, cfg) + s := computeSplit(d, cfg) if s.Gateway.CPULimit != "2" || s.Gateway.CPURequest != "2" { t.Errorf("gateway cpu = %q/%q, want 2/2", s.Gateway.CPURequest, s.Gateway.CPULimit) } @@ -159,7 +167,7 @@ func TestComputeResourceSplit_EnvelopeOmittedAllExplicit(t *testing.T) { d.Spec.Resource.Gateway = &dbpreview.ComponentResources{Memory: "512Mi", CPU: "500m"} d.Spec.Resource.Database = &dbpreview.ComponentResources{Memory: "4Gi", CPU: "3"} - s := ComputeResourceSplit(d, cfg) + s := computeSplit(d, cfg) if s.Gateway.MemoryLimit != "512Mi" || s.Gateway.CPULimit != "500m" { t.Errorf("gateway = %+v, want 512Mi/500m", s.Gateway) } @@ -175,7 +183,7 @@ func TestComputeResourceSplit_CPUCarvedWithMonitoring(t *testing.T) { cfg := prodSplitConfig() d := ddbWithMemory("8Gi", true) d.Spec.Resource.CPU = "4" - s := ComputeResourceSplit(d, cfg) + s := computeSplit(d, cfg) // otel cpu reservation defaults to 50m request / 200m limit (Burstable); // only the request is carved from the envelope, so postgres = 4 - 50m = 3950m. if s.OTel.CPURequest != "50m" { diff --git a/operator/src/internal/cnpg/testdata/render/custom-loglevel-and-stopdelay.golden.yaml b/operator/src/internal/cnpg/testdata/render/custom-loglevel-and-stopdelay.golden.yaml new file mode 100644 index 000000000..a0920dd2a --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/custom-loglevel-and-stopdelay.golden.yaml @@ -0,0 +1,92 @@ +metadata: + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: "" + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: "" + replica_type: primary + instances: 1 + logLevel: debug + managed: + roles: + - connectionLimit: -1 + ensure: absent + inherit: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 512MB + effective_io_concurrency: "200" + maintenance_work_mem: 128MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 256MB + wal_buffers: 16MB + work_mem: 16MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: {} + stopDelay: 120 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/gateway-tls-ready.golden.yaml b/operator/src/internal/cnpg/testdata/render/gateway-tls-ready.golden.yaml new file mode 100644 index 000000000..6141ee088 --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/gateway-tls-ready.golden.yaml @@ -0,0 +1,93 @@ +metadata: + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: "" + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: "" + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - connectionLimit: -1 + ensure: absent + inherit: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + gatewayTLSSecret: gw-tls-secret + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 512MB + effective_io_concurrency: "200" + maintenance_work_mem: 128MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 256MB + wal_buffers: 16MB + work_mem: 16MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: {} + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/iouring-and-changestreams.golden.yaml b/operator/src/internal/cnpg/testdata/render/iouring-and-changestreams.golden.yaml new file mode 100644 index 000000000..b3d5fcf17 --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/iouring-and-changestreams.golden.yaml @@ -0,0 +1,97 @@ +metadata: + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: "" + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: "" + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - connectionLimit: -1 + ensure: absent + inherit: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/wentingwu666666/documentdb-kubernetes-operator/documentdb-gateway:16-changestream + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/wentingwu666666/documentdb-kubernetes-operator/documentdb-oss:16-changestream + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 512MB + effective_io_concurrency: "200" + io_method: io_uring + maintenance_work_mem: 128MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 256MB + wal_buffers: 16MB + wal_level: logical + work_mem: 16MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: {} + seccompProfile: + localhostProfile: profiles/documentdb-iouring.json + type: Localhost + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/minimal.golden.yaml b/operator/src/internal/cnpg/testdata/render/minimal.golden.yaml new file mode 100644 index 000000000..a4636f95f --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/minimal.golden.yaml @@ -0,0 +1,92 @@ +metadata: + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: "" + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: "" + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - connectionLimit: -1 + ensure: absent + inherit: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 512MB + effective_io_concurrency: "200" + maintenance_work_mem: 128MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 256MB + wal_buffers: 16MB + work_mem: 16MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: {} + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/monitoring-otlp-and-prometheus.golden.yaml b/operator/src/internal/cnpg/testdata/render/monitoring-otlp-and-prometheus.golden.yaml new file mode 100644 index 000000000..13221835d --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/monitoring-otlp-and-prometheus.golden.yaml @@ -0,0 +1,104 @@ +metadata: + name: mon-both + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: mon-both + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: mon-both + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - comment: Dedicated role for the OTel Collector monitoring sidecar + connectionLimit: -1 + disablePassword: true + ensure: present + inherit: true + login: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + otelCollectorImage: otel/opentelemetry-collector-contrib:0.149.0 + otelConfigHash: 37f5d3890c5ab450 + otelConfigMapName: mon-both-otel-config + otelCpuLimit: 200m + otelCpuRequest: 50m + otelMemoryLimit: 128Mi + otelMemoryRequest: 48Mi + prometheusPort: "8888" + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 512MB + effective_io_concurrency: "200" + maintenance_work_mem: 128MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 256MB + wal_buffers: 16MB + work_mem: 16MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: {} + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/monitoring-prometheus.golden.yaml b/operator/src/internal/cnpg/testdata/render/monitoring-prometheus.golden.yaml new file mode 100644 index 000000000..329cda52c --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/monitoring-prometheus.golden.yaml @@ -0,0 +1,104 @@ +metadata: + name: mon-prom + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: mon-prom + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: mon-prom + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - comment: Dedicated role for the OTel Collector monitoring sidecar + connectionLimit: -1 + disablePassword: true + ensure: present + inherit: true + login: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + otelCollectorImage: otel/opentelemetry-collector-contrib:0.149.0 + otelConfigHash: e8ff8355aabfc3dd + otelConfigMapName: mon-prom-otel-config + otelCpuLimit: 200m + otelCpuRequest: 50m + otelMemoryLimit: 128Mi + otelMemoryRequest: 48Mi + prometheusPort: "9090" + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 512MB + effective_io_concurrency: "200" + maintenance_work_mem: 128MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 256MB + wal_buffers: 16MB + work_mem: 16MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: {} + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/postgres-tls.golden.yaml b/operator/src/internal/cnpg/testdata/render/postgres-tls.golden.yaml new file mode 100644 index 000000000..597da0bdf --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/postgres-tls.golden.yaml @@ -0,0 +1,95 @@ +metadata: + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: "" + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + certificates: + serverCASecret: pg-ca + serverTLSSecret: pg-tls + inheritedMetadata: + labels: + app: "" + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - connectionLimit: -1 + ensure: absent + inherit: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 512MB + effective_io_concurrency: "200" + maintenance_work_mem: 128MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 256MB + wal_buffers: 16MB + work_mem: 16MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: {} + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/process-identity.golden.yaml b/operator/src/internal/cnpg/testdata/render/process-identity.golden.yaml new file mode 100644 index 000000000..cbf77016f --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/process-identity.golden.yaml @@ -0,0 +1,94 @@ +metadata: + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: "" + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: "" + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - connectionLimit: -1 + ensure: absent + inherit: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + postgresGID: 26 + postgresUID: 26 + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 512MB + effective_io_concurrency: "200" + maintenance_work_mem: 128MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 256MB + wal_buffers: 16MB + work_mem: 16MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: {} + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/resource-envelope.golden.yaml b/operator/src/internal/cnpg/testdata/render/resource-envelope.golden.yaml new file mode 100644 index 000000000..05f1b4482 --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/resource-envelope.golden.yaml @@ -0,0 +1,100 @@ +metadata: + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: "" + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: "" + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - connectionLimit: -1 + ensure: absent + inherit: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + gatewayMemoryLimit: 1536Mi + gatewayMemoryRequest: 1536Mi + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 4992MB + effective_io_concurrency: "200" + maintenance_work_mem: 665MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 1664MB + wal_buffers: 16MB + work_mem: 5MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: + limits: + cpu: "4" + memory: 6656Mi + requests: + cpu: "4" + memory: 6656Mi + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/resource-overrides.golden.yaml b/operator/src/internal/cnpg/testdata/render/resource-overrides.golden.yaml new file mode 100644 index 000000000..83e2ecf69 --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/resource-overrides.golden.yaml @@ -0,0 +1,102 @@ +metadata: + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: "" + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: "" + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - connectionLimit: -1 + ensure: absent + inherit: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayCpuLimit: 500m + gatewayCpuRequest: 500m + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + gatewayMemoryLimit: 512Mi + gatewayMemoryRequest: 512Mi + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 3GB + effective_io_concurrency: "200" + maintenance_work_mem: 409MB + max_connections: "300" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 1GB + wal_buffers: 16MB + work_mem: 4MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/cnpg/testdata/render/user-params.golden.yaml b/operator/src/internal/cnpg/testdata/render/user-params.golden.yaml new file mode 100644 index 000000000..0b22d7292 --- /dev/null +++ b/operator/src/internal/cnpg/testdata/render/user-params.golden.yaml @@ -0,0 +1,92 @@ +metadata: + ownerReferences: + - apiVersion: "" + blockOwnerDeletion: true + controller: true + kind: "" + name: "" + uid: "" +spec: + affinity: {} + backup: + target: primary + volumeSnapshot: + onlineConfiguration: {} + snapshotOwnerReference: backup + bootstrap: + initdb: + postInitSQL: + - CREATE EXTENSION documentdb CASCADE + - CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100' + - ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS + inheritedMetadata: + labels: + app: "" + replica_type: primary + instances: 1 + logLevel: info + managed: + roles: + - connectionLimit: -1 + ensure: absent + inherit: true + name: otel_monitor + plugins: + - enabled: true + name: cnpg-i-sidecar-injector.documentdb.io + parameters: + documentDbCredentialSecret: documentdb-credentials + gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.113.0 + postgresql: + extensions: + - dynamic_library_path: + - lib + extension_control_path: + - share + image: + reference: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.113.0 + ld_library_path: + - lib + - system + name: documentdb + parameters: + autovacuum_analyze_scale_factor: "0.05" + autovacuum_max_workers: "4" + autovacuum_vacuum_cost_delay: 2ms + autovacuum_vacuum_scale_factor: "0.1" + checkpoint_completion_target: "0.9" + cron.database_name: postgres + effective_cache_size: 512MB + effective_io_concurrency: "200" + maintenance_work_mem: 128MB + max_connections: "200" + max_prepared_transactions: "100" + max_replication_slots: "10" + max_wal_senders: "10" + max_wal_size: 2GB + min_wal_size: 256MB + random_page_cost: "1.1" + shared_buffers: 256MB + wal_buffers: 16MB + work_mem: 64MB + pg_hba: + - host all all localhost trust + - hostssl replication streaming_replica all cert + shared_preload_libraries: + - pg_cron + - pg_documentdb_core + - pg_documentdb + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: switchover + resources: {} + stopDelay: 30 + storage: + size: 10Gi +status: + certificates: {} + configMapResourceVersion: {} + managedRolesStatus: {} + secretsResourceVersion: {} + switchReplicaClusterStatus: {} + topology: {} diff --git a/operator/src/internal/controller/documentdb_controller.go b/operator/src/internal/controller/documentdb_controller.go index 9dd8ed856..511a0bfdd 100644 --- a/operator/src/internal/controller/documentdb_controller.go +++ b/operator/src/internal/controller/documentdb_controller.go @@ -155,7 +155,7 @@ func (r *DocumentDBReconciler) Reconcile(ctx context.Context, req ctrl.Request) intent := product.DocumentDBAdapter{}.ToClusterIntent(documentdb) currentCnpgCluster := &cnpgv1.Cluster{} - desiredCnpgCluster := cnpg.GetCnpgClusterSpecFromIntent(req, documentdb, intent, documentdb.Name, replicationContext.StorageClass, replicationContext.IsPrimary(), logger) + desiredCnpgCluster := cnpg.GetCnpgClusterSpecFromIntent(intent, replicationContext.StorageClass, replicationContext.IsPrimary(), logger) if replicationContext.IsReplicating() { err = r.AddClusterReplicationToClusterSpec(ctx, documentdb, replicationContext, desiredCnpgCluster) @@ -1032,7 +1032,7 @@ func (r *DocumentDBReconciler) reconcileOtelConfigMap(ctx context.Context, docum return fmt.Errorf("failed to set owner reference: %w", err) } - configData, err := otelcfg.GenerateConfigMapData(documentdb.Name, namespace, documentdb.Spec.Monitoring) + configData, err := otelcfg.GenerateConfigMapData(documentdb.Name, namespace, product.MonitoringConfigFromSpec(documentdb.Spec.Monitoring)) if err != nil { return fmt.Errorf("failed to generate OTel config: %w", err) } diff --git a/operator/src/internal/otel/config.go b/operator/src/internal/otel/config.go index e87eb7e65..51e7afb34 100644 --- a/operator/src/internal/otel/config.go +++ b/operator/src/internal/otel/config.go @@ -10,8 +10,6 @@ import ( "sort" "gopkg.in/yaml.v3" - - dbpreview "github.com/documentdb/documentdb-operator/api/preview" ) //go:embed base_config.yaml @@ -19,6 +17,20 @@ var baseConfigYAML []byte const defaultPrometheusPort = 8888 +// MonitoringConfig is the product-neutral OTel collector configuration input. +// Product adapters map their custom resource's monitoring spec onto this struct +// so the collector config generation stays independent of any product CRD type. +type MonitoringConfig struct { + // Enabled reports whether the OTel Collector sidecar is requested. + Enabled bool + // OTLPEndpoint is the OTLP gRPC exporter endpoint. Empty disables the exporter. + OTLPEndpoint string + // Prometheus reports whether the Prometheus scrape exporter is configured. + Prometheus bool + // PrometheusPort is the Prometheus scrape port; 0 selects the default. + PrometheusPort int32 +} + // MonitorRoleName is the dedicated PostgreSQL identity the OTel Collector // sidecar uses for its health-check query. const MonitorRoleName = "otel_monitor" @@ -66,8 +78,8 @@ func ConfigMapName(clusterName string) string { // disabled, the operator deletes the ConfigMap and removes sidecar parameters, // then triggers a rolling restart (via restart annotation) so that CNPG // recreates pods without the sidecar. -func GenerateConfigMapData(clusterName, namespace string, spec *dbpreview.MonitoringSpec) (map[string]string, error) { - dynamicYAML, err := generateDynamicConfig(clusterName, namespace, spec) +func GenerateConfigMapData(clusterName, namespace string, cfg MonitoringConfig) (map[string]string, error) { + dynamicYAML, err := generateDynamicConfig(clusterName, namespace, cfg) if err != nil { return nil, err } @@ -81,7 +93,7 @@ func GenerateConfigMapData(clusterName, namespace string, spec *dbpreview.Monito // generateDynamicConfig builds the per-cluster dynamic config (resource // processor, exporters, pipeline wiring) that the collector deep-merges // with the embedded base_config.yaml. -func generateDynamicConfig(clusterName, namespace string, spec *dbpreview.MonitoringSpec) (string, error) { +func generateDynamicConfig(clusterName, namespace string, mon MonitoringConfig) (string, error) { cfg := collectorConfig{ Processors: map[string]any{ // `insert` (not `upsert`) so receivers that already emit their @@ -105,42 +117,40 @@ func generateDynamicConfig(clusterName, namespace string, spec *dbpreview.Monito exporterNames := []string{} - if spec.Exporter != nil { - if otlp := spec.Exporter.OTLP; otlp != nil && otlp.Endpoint != "" { - if cfg.Exporters == nil { - cfg.Exporters = map[string]any{} - } - cfg.Exporters["otlp"] = map[string]any{ - "endpoint": otlp.Endpoint, - "tls": map[string]any{ - // TODO: Support TLS for OTLP exporter. Currently hardcoded to - // insecure for in-cluster communication. When TLS is needed, - // add TLS config fields to OTLPExporterSpec (certSecret, etc.). - "insecure": true, - }, - } - exporterNames = append(exporterNames, "otlp") + if mon.OTLPEndpoint != "" { + if cfg.Exporters == nil { + cfg.Exporters = map[string]any{} + } + cfg.Exporters["otlp"] = map[string]any{ + "endpoint": mon.OTLPEndpoint, + "tls": map[string]any{ + // TODO: Support TLS for OTLP exporter. Currently hardcoded to + // insecure for in-cluster communication. When TLS is needed, + // add TLS config fields to the exporter model (certSecret, etc.). + "insecure": true, + }, } + exporterNames = append(exporterNames, "otlp") + } - if prom := spec.Exporter.Prometheus; prom != nil { - if cfg.Exporters == nil { - cfg.Exporters = map[string]any{} - } - port := prom.Port - if port == 0 { - port = defaultPrometheusPort - } - cfg.Exporters["prometheus"] = map[string]any{ - "endpoint": fmt.Sprintf("0.0.0.0:%d", port), - // Surface resource attributes as Prometheus labels (instead - // of burying them in target_info) so dashboards can filter - // by pod/container/cluster. - "resource_to_telemetry_conversion": map[string]any{ - "enabled": true, - }, - } - exporterNames = append(exporterNames, "prometheus") + if mon.Prometheus { + if cfg.Exporters == nil { + cfg.Exporters = map[string]any{} + } + port := mon.PrometheusPort + if port == 0 { + port = defaultPrometheusPort + } + cfg.Exporters["prometheus"] = map[string]any{ + "endpoint": fmt.Sprintf("0.0.0.0:%d", port), + // Surface resource attributes as Prometheus labels (instead + // of burying them in target_info) so dashboards can filter + // by pod/container/cluster. + "resource_to_telemetry_conversion": map[string]any{ + "enabled": true, + }, } + exporterNames = append(exporterNames, "prometheus") } // Wire pipeline: receivers + memory_limiter/batch from static.yaml, @@ -193,12 +203,12 @@ func HashConfigMapData(data map[string]string) string { // ResolvePrometheusPort returns the effective Prometheus port from the spec, // or 0 if Prometheus exporter is not configured. -func ResolvePrometheusPort(spec *dbpreview.MonitoringSpec) int32 { - if spec == nil || spec.Exporter == nil || spec.Exporter.Prometheus == nil { +func ResolvePrometheusPort(mon MonitoringConfig) int32 { + if !mon.Prometheus { return 0 } - if spec.Exporter.Prometheus.Port == 0 { + if mon.PrometheusPort == 0 { return defaultPrometheusPort } - return spec.Exporter.Prometheus.Port + return mon.PrometheusPort } diff --git a/operator/src/internal/otel/config_test.go b/operator/src/internal/otel/config_test.go index 0032c3312..d9aa6092d 100644 --- a/operator/src/internal/otel/config_test.go +++ b/operator/src/internal/otel/config_test.go @@ -10,8 +10,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v3" - - dbpreview "github.com/documentdb/documentdb-operator/api/preview" ) func TestOtel(t *testing.T) { @@ -82,13 +80,8 @@ var _ = Describe("base_config.yaml embed", func() { var _ = Describe("GenerateConfigMapData", func() { It("returns static.yaml from embedded base_config.yaml", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - Prometheus: &dbpreview.PrometheusExporterSpec{Port: 9090}, - }, - } - data, err := GenerateConfigMapData("cluster", "ns", spec) + cfg := MonitoringConfig{Enabled: true, Prometheus: true, PrometheusPort: 9090} + data, err := GenerateConfigMapData("cluster", "ns", cfg) Expect(err).NotTo(HaveOccurred()) // static.yaml should contain the embedded base config @@ -99,13 +92,8 @@ var _ = Describe("GenerateConfigMapData", func() { }) It("generates dynamic.yaml with resource processor and exporters", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - Prometheus: &dbpreview.PrometheusExporterSpec{Port: 9090}, - }, - } - data, err := GenerateConfigMapData("test-cluster", "test-ns", spec) + cfg := MonitoringConfig{Enabled: true, Prometheus: true, PrometheusPort: 9090} + data, err := GenerateConfigMapData("test-cluster", "test-ns", cfg) Expect(err).NotTo(HaveOccurred()) dynCfg := parseCfg(data["dynamic.yaml"]) @@ -128,15 +116,8 @@ var _ = Describe("GenerateConfigMapData", func() { }) It("includes OTLP exporter in dynamic.yaml when configured", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - OTLP: &dbpreview.OTLPExporterSpec{ - Endpoint: "otel-collector.monitoring:4317", - }, - }, - } - data, err := GenerateConfigMapData("cluster", "ns", spec) + cfg := MonitoringConfig{Enabled: true, OTLPEndpoint: "otel-collector.monitoring:4317"} + data, err := GenerateConfigMapData("cluster", "ns", cfg) Expect(err).NotTo(HaveOccurred()) dynCfg := parseCfg(data["dynamic.yaml"]) @@ -145,14 +126,8 @@ var _ = Describe("GenerateConfigMapData", func() { }) It("skips OTLP exporter when endpoint is empty", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - OTLP: &dbpreview.OTLPExporterSpec{Endpoint: ""}, - Prometheus: &dbpreview.PrometheusExporterSpec{Port: 9090}, - }, - } - data, err := GenerateConfigMapData("cluster", "ns", spec) + cfg := MonitoringConfig{Enabled: true, OTLPEndpoint: "", Prometheus: true, PrometheusPort: 9090} + data, err := GenerateConfigMapData("cluster", "ns", cfg) Expect(err).NotTo(HaveOccurred()) dynCfg := parseCfg(data["dynamic.yaml"]) @@ -160,13 +135,8 @@ var _ = Describe("GenerateConfigMapData", func() { }) It("includes Prometheus exporter with default port", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - Prometheus: &dbpreview.PrometheusExporterSpec{}, - }, - } - data, err := GenerateConfigMapData("cluster", "ns", spec) + cfg := MonitoringConfig{Enabled: true, Prometheus: true} + data, err := GenerateConfigMapData("cluster", "ns", cfg) Expect(err).NotTo(HaveOccurred()) dynCfg := parseCfg(data["dynamic.yaml"]) @@ -177,13 +147,8 @@ var _ = Describe("GenerateConfigMapData", func() { }) It("includes Prometheus exporter with custom port", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - Prometheus: &dbpreview.PrometheusExporterSpec{Port: 9090}, - }, - } - data, err := GenerateConfigMapData("cluster", "ns", spec) + cfg := MonitoringConfig{Enabled: true, Prometheus: true, PrometheusPort: 9090} + data, err := GenerateConfigMapData("cluster", "ns", cfg) Expect(err).NotTo(HaveOccurred()) dynCfg := parseCfg(data["dynamic.yaml"]) @@ -193,14 +158,8 @@ var _ = Describe("GenerateConfigMapData", func() { }) It("includes both OTLP and Prometheus exporters", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - OTLP: &dbpreview.OTLPExporterSpec{Endpoint: "otel-collector:4317"}, - Prometheus: &dbpreview.PrometheusExporterSpec{Port: 9090}, - }, - } - data, err := GenerateConfigMapData("cluster", "ns", spec) + cfg := MonitoringConfig{Enabled: true, OTLPEndpoint: "otel-collector:4317", Prometheus: true, PrometheusPort: 9090} + data, err := GenerateConfigMapData("cluster", "ns", cfg) Expect(err).NotTo(HaveOccurred()) dynCfg := parseCfg(data["dynamic.yaml"]) @@ -211,8 +170,8 @@ var _ = Describe("GenerateConfigMapData", func() { }) It("generates no pipeline when no exporters configured", func() { - spec := &dbpreview.MonitoringSpec{Enabled: true, Exporter: nil} - data, err := GenerateConfigMapData("cluster", "ns", spec) + cfg := MonitoringConfig{Enabled: true} + data, err := GenerateConfigMapData("cluster", "ns", cfg) Expect(err).NotTo(HaveOccurred()) dynCfg := parseCfg(data["dynamic.yaml"]) @@ -222,13 +181,8 @@ var _ = Describe("GenerateConfigMapData", func() { // Regression guards — see comments in config.go for the why behind each. It("uses 'insert' (not 'upsert') on the resource processor so per-datapoint k8s attrs survive", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - Prometheus: &dbpreview.PrometheusExporterSpec{Port: 9090}, - }, - } - data, err := GenerateConfigMapData("cluster", "ns", spec) + cfg := MonitoringConfig{Enabled: true, Prometheus: true, PrometheusPort: 9090} + data, err := GenerateConfigMapData("cluster", "ns", cfg) Expect(err).NotTo(HaveOccurred()) // Must not contain 'upsert' — that would clobber per-datapoint resource @@ -239,13 +193,8 @@ var _ = Describe("GenerateConfigMapData", func() { }) It("enables resource_to_telemetry_conversion on the prometheus exporter so resource attrs become labels", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - Prometheus: &dbpreview.PrometheusExporterSpec{Port: 9090}, - }, - } - data, err := GenerateConfigMapData("cluster", "ns", spec) + cfg := MonitoringConfig{Enabled: true, Prometheus: true, PrometheusPort: 9090} + data, err := GenerateConfigMapData("cluster", "ns", cfg) Expect(err).NotTo(HaveOccurred()) // Without this option the prometheus exporter writes resource attrs only // to target_info, hiding documentdb.cluster / k8s.* labels. @@ -281,32 +230,19 @@ var _ = Describe("HashConfigMapData", func() { }) var _ = Describe("ResolvePrometheusPort", func() { - It("returns 0 when spec is nil", func() { - Expect(ResolvePrometheusPort(nil)).To(Equal(int32(0))) + It("returns 0 when monitoring is empty", func() { + Expect(ResolvePrometheusPort(MonitoringConfig{})).To(Equal(int32(0))) }) It("returns 0 when Prometheus is not configured", func() { - spec := &dbpreview.MonitoringSpec{Enabled: true} - Expect(ResolvePrometheusPort(spec)).To(Equal(int32(0))) + Expect(ResolvePrometheusPort(MonitoringConfig{Enabled: true})).To(Equal(int32(0))) }) It("returns default port when Port is 0", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - Prometheus: &dbpreview.PrometheusExporterSpec{}, - }, - } - Expect(ResolvePrometheusPort(spec)).To(Equal(int32(8888))) + Expect(ResolvePrometheusPort(MonitoringConfig{Enabled: true, Prometheus: true})).To(Equal(int32(8888))) }) It("returns custom port when set", func() { - spec := &dbpreview.MonitoringSpec{ - Enabled: true, - Exporter: &dbpreview.ExporterSpec{ - Prometheus: &dbpreview.PrometheusExporterSpec{Port: 9090}, - }, - } - Expect(ResolvePrometheusPort(spec)).To(Equal(int32(9090))) + Expect(ResolvePrometheusPort(MonitoringConfig{Enabled: true, Prometheus: true, PrometheusPort: 9090})).To(Equal(int32(9090))) }) }) diff --git a/operator/src/internal/product/documentdb.go b/operator/src/internal/product/documentdb.go index b40c2c35a..d5b10e2e4 100644 --- a/operator/src/internal/product/documentdb.go +++ b/operator/src/internal/product/documentdb.go @@ -7,6 +7,7 @@ import ( "os" dbpreview "github.com/documentdb/documentdb-operator/api/preview" + otelcfg "github.com/documentdb/documentdb-operator/internal/otel" util "github.com/documentdb/documentdb-operator/internal/utils" ) @@ -26,8 +27,8 @@ func DocumentDBProfile() ProductProfile { } } -// DocumentDBAdapter is the first product adapter. It maps the DocumentDB custom -// resource onto the product-neutral model consumed by the reconciler. +// DocumentDBAdapter maps the DocumentDB custom resource onto the product-neutral +// model consumed by the reconciler. type DocumentDBAdapter struct{} // Profile returns the DocumentDB product profile. @@ -90,6 +91,11 @@ func (a DocumentDBAdapter) ToClusterIntent(db *dbpreview.DocumentDB) ClusterInte sidecarPlugin = db.Spec.Plugins.SidecarInjectorName } + walReplicaPlugin := p.WALReplicaPlugin + if db.Spec.Plugins != nil && db.Spec.Plugins.WalReplicaName != "" { + walReplicaPlugin = db.Spec.Plugins.WalReplicaName + } + var postgresImage string if db.Spec.Image != nil { postgresImage = db.Spec.Image.Postgres @@ -101,7 +107,20 @@ func (a DocumentDBAdapter) ToClusterIntent(db *dbpreview.DocumentDB) ClusterInte UID: db.Spec.Postgres.UID, GID: db.Spec.Postgres.GID, PostInitSQL: db.Spec.Postgres.PostInitSQL, + Parameters: db.Spec.Postgres.Parameters, + } + } + // Change streams is a DocumentDB-specific gate. Its only cluster effect is a + // PostgreSQL GUC, so the adapter contributes wal_level=logical as an ordinary + // resolved parameter rather than leaking a product concept into the neutral + // feature gates. Copy first so the user's spec map is never mutated. + if dbpreview.IsFeatureGateEnabled(db, dbpreview.FeatureGateChangeStreams) { + params := make(map[string]string, len(pg.Parameters)+1) + for k, v := range pg.Parameters { + params[k] = v } + params["wal_level"] = "logical" + pg.Parameters = params } var bootstrap Bootstrap @@ -114,6 +133,19 @@ func (a DocumentDBAdapter) ToClusterIntent(db *dbpreview.DocumentDB) ClusterInte bootstrap.Recovery = &r } + var tls TLS + if db.Status.TLS != nil && db.Status.TLS.Ready && db.Status.TLS.SecretName != "" { + tls.GatewaySecretName = db.Status.TLS.SecretName + } + if db.Spec.TLS != nil { + tls.PostgresCertificates = db.Spec.TLS.Postgres + } + + maxStopDelay := int32(util.CNPG_DEFAULT_STOP_DELAY) + if db.Spec.Timeouts.StopDelay != 0 { + maxStopDelay = db.Spec.Timeouts.StopDelay + } + return ClusterIntent{ Images: Images{ PostgresExtension: a.ExtensionImage(db), @@ -122,28 +154,77 @@ func (a DocumentDBAdapter) ToClusterIntent(db *dbpreview.DocumentDB) ClusterInte PullSecrets: db.Spec.ImagePullSecrets, }, Topology: Topology{ - Instances: db.Spec.InstancesPerNode, - Affinity: db.Spec.Affinity, + NodeCount: db.Spec.NodeCount, + InstancesPerNode: db.Spec.InstancesPerNode, + Affinity: db.Spec.Affinity, }, Storage: Storage{ PvcSize: db.Spec.Resource.Storage.PvcSize, }, Identity: Identity{ Name: db.Name, + Namespace: db.Namespace, UID: db.UID, APIVersion: db.APIVersion, Kind: db.Kind, }, - Postgres: pg, + Postgres: pg, + Resource: ResourceFromSpec(db.Spec.Resource), + TLS: tls, + Monitoring: MonitoringConfigFromSpec(db.Spec.Monitoring), + LogLevel: db.Spec.LogLevel, + Timeouts: Timeouts{StopDelay: maxStopDelay}, FeatureGates: FeatureGates{ IOUring: dbpreview.IsFeatureGateEnabled(db, dbpreview.FeatureGateIOUring), }, - Bootstrap: bootstrap, - CredentialSecret: credentialSecret, - SidecarInjectorPlugin: sidecarPlugin, - WALReplicaPlugin: p.WALReplicaPlugin, - Product: p, + Bootstrap: bootstrap, + CredentialSecret: credentialSecret, + Plugins: Plugins{ + SidecarInjectorName: sidecarPlugin, + WalReplicaName: walReplicaPlugin, + }, + Product: p, + } +} + +// ResourceFromSpec converts the DocumentDB resource spec into the product-neutral +// Resource model consumed by the CNPG resource-split logic. +func ResourceFromSpec(res dbpreview.Resource) Resource { + return Resource{ + Memory: res.Memory, + CPU: res.CPU, + Database: componentFromSpec(res.Database), + Gateway: componentFromSpec(res.Gateway), + OTel: componentFromSpec(res.OTel), + } +} + +// MonitoringConfigFromSpec maps the DocumentDB monitoring spec onto the +// product-neutral OTel collector config carried on the intent. It is the single +// CRD->neutral mapping shared by the builder and the controller's ConfigMap +// reconciliation, so generated collector config (and its hash) stays consistent. +func MonitoringConfigFromSpec(spec *dbpreview.MonitoringSpec) otelcfg.MonitoringConfig { + if spec == nil { + return otelcfg.MonitoringConfig{} + } + mc := otelcfg.MonitoringConfig{Enabled: spec.Enabled} + if spec.Exporter != nil { + if spec.Exporter.OTLP != nil { + mc.OTLPEndpoint = spec.Exporter.OTLP.Endpoint + } + if spec.Exporter.Prometheus != nil { + mc.Prometheus = true + mc.PrometheusPort = spec.Exporter.Prometheus.Port + } + } + return mc +} + +func componentFromSpec(c *dbpreview.ComponentResources) *ComponentResource { + if c == nil { + return nil } + return &ComponentResource{Memory: c.Memory, CPU: c.CPU} } // compile-time assertion that DocumentDBAdapter satisfies the Adapter seam. diff --git a/operator/src/internal/product/intent.go b/operator/src/internal/product/intent.go index 06ad7c5b4..892fb8470 100644 --- a/operator/src/internal/product/intent.go +++ b/operator/src/internal/product/intent.go @@ -7,6 +7,8 @@ import ( cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + + otelcfg "github.com/documentdb/documentdb-operator/internal/otel" ) // Images holds the fully-resolved container images for a cluster. @@ -23,15 +25,17 @@ type Images struct { // Topology describes the cluster shape and scheduling. type Topology struct { - // Instances is the number of PostgreSQL instances in the cluster. - Instances int + // NodeCount is the number of nodes (shards) in the cluster. + NodeCount int + // InstancesPerNode is the number of PostgreSQL instances per node. + InstancesPerNode int // Affinity is the CNPG affinity/anti-affinity passthrough. Affinity cnpgv1.AffinityConfiguration } // Storage describes the persistent volume request. StorageClass is resolved by -// the controller from replication context and passed to the builder separately, -// so it is not part of this adapter-derived model yet. +// the controller from replication context and passed to the builder separately +// (as a render parameter), so it is not part of this adapter-derived model. type Storage struct { // PvcSize is the persistent volume claim size (for example "10Gi"). PvcSize string @@ -41,27 +45,70 @@ type Storage struct { // and resource labels. type Identity struct { Name string + Namespace string UID types.UID APIVersion string Kind string } // Postgres carries the operator-managed PostgreSQL process and init tuning taken -// from the custom resource. Parameter/GUC assembly stays product-specific in the -// builder and is not represented here yet. +// from the custom resource. type Postgres struct { // UID and GID are the process identity overrides; nil leaves the CNPG default. UID *int64 GID *int64 // PostInitSQL is appended to the mandatory bootstrap SQL. PostInitSQL []string + // Parameters are the resolved PostgreSQL GUC overrides. They start from the + // user-supplied values and may include product-mandated defaults contributed + // by the adapter (for example DocumentDB change streams adds + // wal_level=logical). They are merged on top of the operator's static and + // memory-aware defaults and below the neutral protected parameters when the + // builder assembles the final GUC set. + Parameters map[string]string } -// FeatureGates carries the resolved feature-gate flags the builder acts on. +// FeatureGates carries the resolved, product-neutral feature-gate flags the +// builder acts on. Only genuinely cross-product (infrastructure) gates belong +// here; product-specific gates are expressed through their concrete effect (for +// example DocumentDB change streams contributing Postgres.Parameters +// wal_level=logical) instead of leaking into this struct. type FeatureGates struct { + // IOUring relaxes the postgres seccomp profile and enables io_method=io_uring. + // It is an infrastructure concern shared across products. IOUring bool } +// ComponentResource is a per-container resource override (request==limit when +// set). Empty strings mean "unset". +type ComponentResource struct { + Memory string + CPU string +} + +// Resource is the product-neutral pod resource envelope plus optional +// per-container overrides. It mirrors the shape the builder carves across the +// PostgreSQL, gateway, and OTel collector containers. +type Resource struct { + // Memory and CPU are the total pod envelope (may be empty/unset). + Memory string + CPU string + // Database, Gateway, and OTel optionally override individual containers. + Database *ComponentResource + Gateway *ComponentResource + OTel *ComponentResource +} + +// TLS carries the resolved TLS inputs the builder renders onto the Cluster. +type TLS struct { + // GatewaySecretName is the ready gateway TLS secret surfaced to the plugin. + // Empty when TLS is not yet provisioned. + GatewaySecretName string + // PostgresCertificates is the CNPG certificates passthrough for the Postgres + // server (nil when TLS is not configured). + PostgresCertificates *cnpgv1.CertificatesConfiguration +} + // Recovery describes a bootstrap-from-source request. A nil Recovery on Bootstrap // means default initialization. type Recovery struct { @@ -76,8 +123,7 @@ type Bootstrap struct { // ClusterIntent is the product-neutral desired state the reconciler renders into // a CNPG Cluster. Product adapters populate it from their custom resource; the -// reconciler consumes it without product-branding logic. Fields are added to -// this struct as the builder is progressively rewired onto the seam. +// reconciler consumes it without product-branding logic. type ClusterIntent struct { // Images are the resolved extension, gateway, and postgres images. Images Images @@ -94,6 +140,23 @@ type ClusterIntent struct { // Postgres is the operator-managed PostgreSQL process and init tuning. Postgres Postgres + // Resource is the product-neutral pod resource envelope and per-container + // overrides the builder carves across containers. + Resource Resource + + // TLS carries the resolved TLS inputs (gateway secret + Postgres certificates). + TLS TLS + + // Monitoring carries the resolved, product-neutral OTel collector config the + // builder renders (config map name/hash + Prometheus port) on the fly. + Monitoring otelcfg.MonitoringConfig + + // LogLevel is the desired CNPG log level (empty means the builder default). + LogLevel string + + // Timeouts carries the resolved CNPG process timeouts (defaults applied). + Timeouts Timeouts + // FeatureGates are the resolved feature-gate flags. FeatureGates FeatureGates @@ -103,11 +166,26 @@ type ClusterIntent struct { // CredentialSecret is the resolved credential secret name. CredentialSecret string - // SidecarInjectorPlugin and WALReplicaPlugin are the resolved CNPG plugin - // names to wire onto the Cluster. - SidecarInjectorPlugin string - WALReplicaPlugin string + // Plugins carries the resolved CNPG plugin names to wire onto the Cluster. + Plugins Plugins // Product is the profile this intent was produced from. Product ProductProfile } + +// Timeouts carries the resolved CNPG process timeouts. It mirrors the CRD's +// Timeouts grouping; values here are resolved (CNPG defaults applied). +type Timeouts struct { + // StopDelay is the resolved CNPG max stop delay in seconds. + StopDelay int32 +} + +// Plugins carries the resolved CNPG plugin names. It mirrors the CRD's Plugins +// grouping; values here are resolved (profile defaults applied, spec honored). +type Plugins struct { + // SidecarInjectorName is the CNPG sidecar injector plugin name. + SidecarInjectorName string + // WalReplicaName is the CNPG WAL replica plugin name used for cross-cluster + // replication. + WalReplicaName string +} diff --git a/operator/src/internal/product/intent_test.go b/operator/src/internal/product/intent_test.go index 77cc91cc7..50f3ea060 100644 --- a/operator/src/internal/product/intent_test.go +++ b/operator/src/internal/product/intent_test.go @@ -32,11 +32,11 @@ func TestToClusterIntentDefaults(t *testing.T) { if intent.CredentialSecret != util.DEFAULT_DOCUMENTDB_CREDENTIALS_SECRET { t.Errorf("CredentialSecret = %q, want %q", intent.CredentialSecret, util.DEFAULT_DOCUMENTDB_CREDENTIALS_SECRET) } - if intent.SidecarInjectorPlugin != util.DEFAULT_SIDECAR_INJECTOR_PLUGIN { - t.Errorf("SidecarInjectorPlugin = %q, want %q", intent.SidecarInjectorPlugin, util.DEFAULT_SIDECAR_INJECTOR_PLUGIN) + if intent.Plugins.SidecarInjectorName != util.DEFAULT_SIDECAR_INJECTOR_PLUGIN { + t.Errorf("Plugins.SidecarInjectorName = %q, want %q", intent.Plugins.SidecarInjectorName, util.DEFAULT_SIDECAR_INJECTOR_PLUGIN) } - if intent.WALReplicaPlugin != util.DEFAULT_WAL_REPLICA_PLUGIN { - t.Errorf("WALReplicaPlugin = %q, want %q", intent.WALReplicaPlugin, util.DEFAULT_WAL_REPLICA_PLUGIN) + if intent.Plugins.WalReplicaName != util.DEFAULT_WAL_REPLICA_PLUGIN { + t.Errorf("Plugins.WalReplicaName = %q, want %q", intent.Plugins.WalReplicaName, util.DEFAULT_WAL_REPLICA_PLUGIN) } if intent.Product.Name != "DocumentDB" { t.Errorf("Product.Name = %q, want DocumentDB", intent.Product.Name) @@ -69,8 +69,8 @@ func TestToClusterIntentOverrides(t *testing.T) { if intent.CredentialSecret != "my-secret" { t.Errorf("CredentialSecret = %q, want my-secret", intent.CredentialSecret) } - if intent.SidecarInjectorPlugin != "custom-injector.example.io" { - t.Errorf("SidecarInjectorPlugin = %q, want custom-injector.example.io", intent.SidecarInjectorPlugin) + if intent.Plugins.SidecarInjectorName != "custom-injector.example.io" { + t.Errorf("Plugins.SidecarInjectorName = %q, want custom-injector.example.io", intent.Plugins.SidecarInjectorName) } } @@ -90,8 +90,8 @@ func TestToClusterIntentTopologyStorageIdentity(t *testing.T) { intent := a.ToClusterIntent(db) - if intent.Topology.Instances != 3 { - t.Errorf("Topology.Instances = %d, want 3", intent.Topology.Instances) + if intent.Topology.InstancesPerNode != 3 { + t.Errorf("Topology.InstancesPerNode = %d, want 3", intent.Topology.InstancesPerNode) } if intent.Storage.PvcSize != "20Gi" { t.Errorf("Storage.PvcSize = %q, want 20Gi", intent.Storage.PvcSize)