Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
documentDbCredentialSecretParameter = "documentDbCredentialSecret"
otelCollectorImageParameter = "otelCollectorImage"
otelConfigMapNameParameter = "otelConfigMapName"
otelConfigHashParameter = "otelConfigHash"
otelMemoryRequestParameter = "otelMemoryRequest"
otelMemoryLimitParameter = "otelMemoryLimit"
otelCPURequestParameter = "otelCpuRequest"
Expand All @@ -47,6 +48,7 @@ type Configuration struct {
DocumentDbCredentialSecret string
OtelCollectorImage string
OtelConfigMapName string
OtelConfigHash string
OTelMemoryRequest string
OTelMemoryLimit string
OTelCPURequest string
Expand Down Expand Up @@ -84,6 +86,8 @@ func FromParameters(
gatewayImage := helper.Parameters[gatewayImageParameter]
credentialSecret := helper.Parameters[documentDbCredentialSecretParameter]
pullPolicy := parsePullPolicy(helper.Parameters[gatewayImagePullPolicyParameter])
otelCollectorImage := helper.Parameters[otelCollectorImageParameter]
otelConfigMapName := helper.Parameters[otelConfigMapNameParameter]
validateQuantityParameters(helper, &validationErrors,
gatewayMemoryRequestParameter,
gatewayMemoryLimitParameter,
Expand All @@ -108,6 +112,37 @@ func FromParameters(
}
}

requiredOtelParameters := []string{
otelCollectorImageParameter,
otelConfigMapNameParameter,
}
otelParameters := append([]string{
prometheusPortParameter,
otelConfigHashParameter,
otelMemoryRequestParameter,
otelMemoryLimitParameter,
otelCPURequestParameter,
otelCPULimitParameter,
}, requiredOtelParameters...)
otelConfigured := false
for _, parameter := range otelParameters {
otelConfigured = otelConfigured || helper.Parameters[parameter] != ""
}
if otelConfigured {
for _, parameter := range requiredOtelParameters {
if helper.Parameters[parameter] == "" {
validationErrors = append(
validationErrors,
validation.BuildErrorForParameter(
helper,
parameter,
"required when any OTel sidecar parameter is configured",
),
)
}
}
}

configuration := &Configuration{
Labels: labels,
Annotations: annotations,
Expand All @@ -118,8 +153,9 @@ func FromParameters(
GatewayCPURequest: helper.Parameters[gatewayCPURequestParameter],
GatewayCPULimit: helper.Parameters[gatewayCPULimitParameter],
DocumentDbCredentialSecret: credentialSecret,
OtelCollectorImage: helper.Parameters[otelCollectorImageParameter],
OtelConfigMapName: helper.Parameters[otelConfigMapNameParameter],
OtelCollectorImage: otelCollectorImage,
OtelConfigMapName: otelConfigMapName,
OtelConfigHash: helper.Parameters[otelConfigHashParameter],
OTelMemoryRequest: helper.Parameters[otelMemoryRequestParameter],
OTelMemoryLimit: helper.Parameters[otelMemoryLimitParameter],
OTelCPURequest: helper.Parameters[otelCPURequestParameter],
Expand Down Expand Up @@ -231,10 +267,16 @@ func (config *Configuration) ToParameters() (map[string]string, error) {
setIfNotEmpty(gatewayCPURequestParameter, config.GatewayCPURequest)
setIfNotEmpty(gatewayCPULimitParameter, config.GatewayCPULimit)
result[documentDbCredentialSecretParameter] = config.DocumentDbCredentialSecret
setIfNotEmpty(otelCollectorImageParameter, config.OtelCollectorImage)
setIfNotEmpty(otelConfigMapNameParameter, config.OtelConfigMapName)
setIfNotEmpty(otelConfigHashParameter, config.OtelConfigHash)
setIfNotEmpty(otelMemoryRequestParameter, config.OTelMemoryRequest)
setIfNotEmpty(otelMemoryLimitParameter, config.OTelMemoryLimit)
setIfNotEmpty(otelCPURequestParameter, config.OTelCPURequest)
setIfNotEmpty(otelCPULimitParameter, config.OTelCPULimit)
if config.PrometheusPort > 0 {
result[prometheusPortParameter] = strconv.FormatInt(int64(config.PrometheusPort), 10)
}

return result, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,105 @@ func TestFromParameters(t *testing.T) {
}
})

t.Run("parses OTel monitoring parameters", func(t *testing.T) {
helper := &common.Plugin{Parameters: map[string]string{
"otelCollectorImage": "otel/opentelemetry-collector-contrib:test",
"otelConfigMapName": "demo-otel-config",
"otelConfigHash": "abc123",
}}
config, errs := FromParameters(helper)
if len(errs) != 0 {
t.Fatalf("unexpected validation errors: %v", errs)
}
if config.OtelCollectorImage != "otel/opentelemetry-collector-contrib:test" {
t.Errorf("OtelCollectorImage = %q", config.OtelCollectorImage)
}
if config.OtelConfigMapName != "demo-otel-config" {
t.Errorf("OtelConfigMapName = %q", config.OtelConfigMapName)
}
if config.OtelConfigHash != "abc123" {
t.Errorf("OtelConfigHash = %q", config.OtelConfigHash)
}
})

for _, tt := range []struct {
name string
parameters map[string]string
wantErrors int
}{
{
name: "rejects collector image without config map",
parameters: map[string]string{
"otelCollectorImage": "otel/opentelemetry-collector-contrib:test",
},
wantErrors: 1,
},
{
name: "rejects config map without collector image",
parameters: map[string]string{
"otelConfigMapName": "demo-otel-config",
},
wantErrors: 1,
},
{
name: "rejects optional OTel parameter without required parameters",
parameters: map[string]string{
"prometheusPort": "8888",
},
wantErrors: 2,
},
{
name: "rejects config hash without required parameters",
parameters: map[string]string{
"otelConfigHash": "abc123",
},
wantErrors: 2,
},
{
name: "rejects memory request without required parameters",
parameters: map[string]string{
"otelMemoryRequest": "64Mi",
},
wantErrors: 2,
},
{
name: "rejects memory limit without required parameters",
parameters: map[string]string{
"otelMemoryLimit": "128Mi",
},
wantErrors: 2,
},
{
name: "rejects CPU request without required parameters",
parameters: map[string]string{
"otelCpuRequest": "100m",
},
wantErrors: 2,
},
{
name: "rejects CPU limit without required parameters",
parameters: map[string]string{
"otelCpuLimit": "300m",
},
wantErrors: 2,
},
} {
t.Run(tt.name, func(t *testing.T) {
_, errs := FromParameters(&common.Plugin{Parameters: tt.parameters})
if len(errs) != tt.wantErrors {
t.Fatalf("validation errors = %d, want %d: %v", len(errs), tt.wantErrors, errs)
}
})
}

t.Run("resource parameters from parameters", func(t *testing.T) {
helper := &common.Plugin{Parameters: map[string]string{
"gatewayMemoryRequest": "768Mi",
"gatewayMemoryLimit": "3Gi",
"gatewayCpuRequest": "500m",
"gatewayCpuLimit": "2",
"otelCollectorImage": "otel:latest",
"otelConfigMapName": "otel-config",
"otelMemoryRequest": "64Mi",
"otelMemoryLimit": "128Mi",
"otelCpuRequest": "100m",
Expand Down Expand Up @@ -130,6 +223,9 @@ func TestToParametersRoundTrip(t *testing.T) {
GatewayMemoryLimit: "3Gi",
GatewayCPURequest: "500m",
GatewayCPULimit: "2",
OtelCollectorImage: "otel:latest",
OtelConfigMapName: "otel-config",
OtelConfigHash: "abc123",
OTelMemoryRequest: "64Mi",
OTelMemoryLimit: "128Mi",
OTelCPURequest: "100m",
Expand Down Expand Up @@ -164,6 +260,15 @@ func TestToParametersRoundTrip(t *testing.T) {
if restored.GatewayCPULimit != original.GatewayCPULimit {
t.Errorf("round-trip gateway cpu limit = %q, want %q", restored.GatewayCPULimit, original.GatewayCPULimit)
}
if restored.OtelCollectorImage != original.OtelCollectorImage {
t.Errorf("round-trip OTel collector image = %q, want %q", restored.OtelCollectorImage, original.OtelCollectorImage)
}
if restored.OtelConfigMapName != original.OtelConfigMapName {
t.Errorf("round-trip OTel config map = %q, want %q", restored.OtelConfigMapName, original.OtelConfigMapName)
}
if restored.OtelConfigHash != original.OtelConfigHash {
t.Errorf("round-trip OTel config hash = %q, want %q", restored.OtelConfigHash, original.OtelConfigHash)
}
if restored.OTelMemoryRequest != original.OTelMemoryRequest {
t.Errorf("round-trip otel memory request = %q, want %q", restored.OTelMemoryRequest, original.OTelMemoryRequest)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func (impl Implementation) reconcileMetadata(
})
}

otelSidecar := newOtelCollectorSidecar(configuration.OtelCollectorImage, cluster.Name)
otelSidecar := newOtelCollectorSidecar(configuration.OtelCollectorImage)
if resources := buildResources(
configuration.OTelCPURequest,
configuration.OTelCPULimit,
Expand Down Expand Up @@ -490,6 +490,7 @@ func injectGatewayOTelEnv(pod *corev1.Pod) {
// otelCollectorContainerName is the name of the injected OpenTelemetry
// Collector sidecar.
const otelCollectorContainerName = "otel-collector"
const otelMonitorRoleName = "otel_monitor"

// gatewaySecurityContext returns the SecurityContext for the documentdb-gateway
// sidecar: the shared PSA-restricted hardening plus an explicit UID/GID of
Expand All @@ -506,19 +507,18 @@ func gatewaySecurityContext() *corev1.SecurityContext {
// the caller). It carries the shared PSA-restricted SecurityContext without an
// explicit UID so the upstream collector image keeps its own baked-in non-root
// user (UID 10001); PSA "restricted" only requires runAsNonRoot, not a fixed
// UID. clusterName selects the CNPG-managed "<cluster>-app" credential secret.
func newOtelCollectorSidecar(image, clusterName string) *corev1.Container {
// UID.
func newOtelCollectorSidecar(image string) *corev1.Container {
return &corev1.Container{
Name: otelCollectorContainerName,
Image: image,
Args: []string{
"--config=file:/config/static.yaml",
"--config=file:/config/dynamic.yaml",
},
// PGUSER and PGPASSWORD are sourced from the CNPG-managed application secret
// ("<cluster>-app"). CNPG auto-creates this secret with "username" and "password"
// keys for the application database user. The OTel Collector's sqlquery receiver
// uses these credentials to connect to PostgreSQL and collect health metrics.
// PostgreSQL currently uses trust authentication, so injecting a password
// would not enforce access control. Use the dedicated password-disabled
// identity directly until database authentication is tightened.
Env: []corev1.EnvVar{
{
Name: "POD_NAME",
Expand All @@ -529,26 +529,8 @@ func newOtelCollectorSidecar(image, clusterName string) *corev1.Container {
},
},
{
Name: "PGUSER",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: clusterName + "-app",
},
Key: "username",
},
},
},
{
Name: "PGPASSWORD",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: clusterName + "-app",
},
Key: "password",
},
},
Name: "PGUSER",
Value: otelMonitorRoleName,
},
},
VolumeMounts: []corev1.VolumeMount{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ func TestGatewaySecurityContext_PSARestrictedAsUID1000(t *testing.T) {
// restricted and, unlike the gateway, does not force a UID so the collector
// image keeps its own non-root user (UID 10001).
func TestNewOtelCollectorSidecar_Hardened(t *testing.T) {
c := newOtelCollectorSidecar("otel/opentelemetry-collector-contrib:test", "demo")
c := newOtelCollectorSidecar("otel/opentelemetry-collector-contrib:test")

if c.Name != otelCollectorContainerName {
t.Fatalf("container name = %q, want %q", c.Name, otelCollectorContainerName)
Expand All @@ -365,3 +365,22 @@ func TestNewOtelCollectorSidecar_Hardened(t *testing.T) {
t.Errorf("otel-collector must not force a GID, got %d", *c.SecurityContext.RunAsGroup)
}
}

// TestNewOtelCollectorSidecar_MonitorUser asserts the sidecar connects using
// the dedicated monitoring identity without injecting an unused password.
func TestNewOtelCollectorSidecar_MonitorUser(t *testing.T) {
c := newOtelCollectorSidecar("otel/opentelemetry-collector-contrib:test")

for _, env := range c.Env {
if env.Name == "PGPASSWORD" {
t.Fatal("PGPASSWORD must not be injected while PostgreSQL uses trust authentication")
}
if env.Name == "PGUSER" {
if env.Value != otelMonitorRoleName {
t.Fatalf("PGUSER = %q, want %q", env.Value, otelMonitorRoleName)
}
return
}
}
t.Fatal("missing PGUSER env var")
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ func (Implementation) ValidateClusterChange(
var newConfiguration *config.Configuration
newConfiguration, result.ValidationErrors = config.FromParameters(newClusterHelper)
oldConfiguration, _ := config.FromParameters(oldClusterHelper)
result.ValidationErrors = config.ValidateChanges(oldConfiguration, newConfiguration, newClusterHelper)
result.ValidationErrors = append(
result.ValidationErrors,
config.ValidateChanges(oldConfiguration, newConfiguration, newClusterHelper)...,
)

return result, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package operator

import (
"context"
"encoding/json"
"testing"

cnpgv1 "github.com/cloudnative-pg/api/pkg/api/v1"
cnpgoperator "github.com/cloudnative-pg/cnpg-i/pkg/operator"

"github.com/documentdb/cnpg-i-sidecar-injector/pkg/metadata"
)

func TestValidateClusterChangeRetainsParameterErrors(t *testing.T) {
oldCluster := &cnpgv1.Cluster{}
newCluster := &cnpgv1.Cluster{
Spec: cnpgv1.ClusterSpec{
Plugins: []cnpgv1.PluginConfiguration{
{
Name: metadata.PluginName,
Parameters: map[string]string{
"otelCollectorImage": "otel/opentelemetry-collector-contrib:test",
},
},
},
},
}

oldJSON, err := json.Marshal(oldCluster)
if err != nil {
t.Fatalf("marshal old cluster: %v", err)
}
newJSON, err := json.Marshal(newCluster)
if err != nil {
t.Fatalf("marshal new cluster: %v", err)
}

result, err := (Implementation{}).ValidateClusterChange(
context.Background(),
&cnpgoperator.OperatorValidateClusterChangeRequest{
OldCluster: oldJSON,
NewCluster: newJSON,
},
)
if err != nil {
t.Fatalf("ValidateClusterChange() error: %v", err)
}
if got, want := len(result.ValidationErrors), 1; got != want {
t.Fatalf("validation errors = %d, want %d: %v", got, want, result.ValidationErrors)
}
}
Loading
Loading