Skip to content

ERA-60185 ERA-57641 : Add ConfigMap defaults support for Database CR and LCMConfig data Bug#208

Merged
manavrajvanshi merged 16 commits intomainfrom
configmap-defaults
Mar 31, 2026
Merged

ERA-60185 ERA-57641 : Add ConfigMap defaults support for Database CR and LCMConfig data Bug#208
manavrajvanshi merged 16 commits intomainfrom
configmap-defaults

Conversation

@sasikanthmasini
Copy link
Copy Markdown
Contributor

@sasikanthmasini sasikanthmasini commented Feb 25, 2026

Overview

Operators can define fleet defaults in a Kubernetes ConfigMap. Developers set defaultsConfigMapRef on the Database spec to point at that ConfigMap. The mutating webhook fills missing fields first; then the validating webhook runs. The API server stores a fully specified custom resource, and the controller does not load the ConfigMap again for defaulting.

Key changes

API (database_types.go under api/v1alpha1)

  • defaultsConfigMapRef on DatabaseSpec — the ConfigMap must live in the same namespace as the Database.

ConfigMap application (configmap_defaults.go and database_webhook.go)

  • FetchConfigMapDefaults uses an uncached API reader (GetAPIReader) so admission does not block on a missing ConfigMap informer.
  • DatabaseCustomDefaulter (mutating webhook): optionally fetch the ConfigMap, apply ApplyDefaultsFromConfigMap, run the existing type defaulter, then validation sees a populated spec.
  • Supports generic vs type-specific keys (for example size vs postgres.size) and clone-specific keys (for example clone.clusterName and clone.profiles.*) with the documented precedence.
  • Controller: duplicate ConfigMap defaulting was removed (including controller_adapters configmap defaults); the controller uses the CR after webhooks.

Clone: latest snapshot (name_resolution.go under controller_adapters, snapshots.go under ndb_api)

  • If snapshotId and snapshotName are both omitted, the operator can pick the latest snapshot for the source database time machine. Other clone fields are still required and validated after defaulting.

Webhook validation (webhook_helpers.go under api/v1alpha1)

  • Validation stays strict after defaulting; there is no relaxed path based on whether a ConfigMap is referenced.
  • Cluster: either clusterId or clusterName must be set after defaulting (the ConfigMap may supply clusterName).
  • Clone: snapshot id/name may be omitted when using latest-snapshot behavior.

RBAC (config/rbac)

  • ConfigMap read access for the operator where needed; ClusterRoleBinding service account reference fixed if applicable.

Architecture

Create or update a Database custom resource in this order:

  1. Mutating webhook — If defaultsConfigMapRef is set, load the ConfigMap and apply defaults, then run the existing type defaulter (description, database names, time machine baselines, and so on).
  2. Validating webhook — Runs on the fully defaulted spec (strict rules; no special “ConfigMap present” shortcuts).
  3. API server — Persists the resource; the controller reconciles what is already complete and does not re-apply ConfigMap defaults.

ConfigMap behaviour

Rule What happens
CR wins Values already set on the Database spec are not overwritten by the ConfigMap.
Precedence Where both exist, type-specific keys (for example postgres.size) win over generic keys (size). For clones, clone.* keys (for example clone.clusterName) win over the matching generic keys when both are present.
Profiles For each profile slot (software, compute, network, db param, db param instance), if either id or name is already set, defaults for that slot are not applied.
Missing ConfigMap If defaultsConfigMapRef is set but the ConfigMap cannot be read, defaulting continues without those values (no hard failure in the mutating webhook for that case).
Limitation There is no supported way to say “leave this field empty and never take a ConfigMap value”; an empty field can still receive a default from the ConfigMap.

Unit Tests

ConfigMap defaulting is covered in api/v1alpha1/configmap_defaults_test.go; the mutating defaulter (DatabaseCustomDefaulter) in api/v1alpha1/database_webhook_test.go. Validating and mutating admission paths are covered by the existing Ginkgo suite in api/v1alpha1/webhook_suite_test.go (envtest). Clone request generation is covered in ndb_api/clone_helpers_test.go.

image

Per-field defaults (defaultsConfigMap → empty CR fields only)


ConfigMap key Provision / Clone What it sets Applied when
clusterName Both NDB cluster name clusterId and clusterName are both empty
timezone Both DB timezone timezone is empty
size Provision Size (GB) size is 0
profiles.compute.name Both Compute profile Compute id and name both empty
profiles.network.name Both Network profile Same (id + name empty)
profiles.software.name Both Software / engine version profile Same
profiles.dbParam.name Both DB parameter profile Same
profiles.dbParamInstance.name Both DB param instance profile (e.g. MSSQL) Same
timeMachine.sla Provision Snapshot SLA (e.g. BRASS, NONE) TM SLA field empty
timeMachine.dailySnapshotTime Provision Daily snapshot time (HH:MM:SS) Empty
timeMachine.snapshotsPerDay Provision Snapshots per day 0
timeMachine.logCatchUpFrequency Provision Log catch-up (minutes) 0
timeMachine.weeklySnapshotDay Provision Weekly snapshot day Empty
timeMachine.monthlySnapshotDay Provision Monthly snapshot day 0
timeMachine.quarterlySnapshotMonth Provision Quarterly snapshot month Empty
clone.clusterName Clone Cluster default for clones Clone: checked before clusterName
clone.timezone Clone Timezone for clones Clone: checked before timezone
clone.profiles.* Clone Same profile slots as above Clone: checked before generic profiles.*

ConfigMap Structure Used

# Default values for Database CRs: the mutating webhook reads this ConfigMap (via
# spec.defaultsConfigMapRef) and fills empty instance fields before validation.
apiVersion: v1
kind: ConfigMap
metadata:
  name: ndb-provisioning-defaults
  namespace: default
data:
  timezone: "UTC"
  # REQUIRED: exact Nutanix cluster name as registered in NDB (NDB UI → Operations “Nutanix Cluster”, or copy from a working Database CR).
  # Wrong value => controller events: "cluster with name '...' not found".
  clusterName: "REPLACE_NDB_CLUSTER_NAME"

  # PostgreSQL
  postgres.profiles.software.name: "POSTGRES_15.6_ROCKY_LINUX_8_OOB"
  postgres.profiles.network.name: "DEFAULT_OOB_POSTGRESQL_NETWORK"
  postgres.profiles.dbParam.name: "DEFAULT_POSTGRES_PARAMS"
  postgres.profiles.compute.name: "DEFAULT_OOB_COMPUTE"
  postgres.size: "10"

  # MySQL
  mysql.profiles.software.name: "MYSQL_8.0_ROCKY_LINUX_8_OOB"
  mysql.profiles.network.name: "DEFAULT_OOB_MYSQL_NETWORK"
  mysql.profiles.dbParam.name: "DEFAULT_MYSQL_PARAMS"
  mysql.profiles.compute.name: "DEFAULT_OOB_COMPUTE"
  mysql.size: "10"

  # MongoDB
  mongodb.profiles.software.name: "MONGODB_1.2debian_NDBOperator"
  mongodb.profiles.network.name: "DEFAULT_OOB_MONGODB_NETWORK"
  mongodb.profiles.compute.name: "DEFAULT_OOB_COMPUTE"
  mongodb.size: "10"

  # Microsoft SQL Server
  mssql.profiles.software.name: "MSSQL-4-NDBOperator"
  mssql.profiles.network.name: "DEFAULT_OOB_SQLSERVER_NETWORK"
  mssql.profiles.dbParam.name: "DEFAULT_SQLSERVER_DATABASE_PARAMS"
  mssql.profiles.dbParamInstance.name: "DEFAULT_SQLSERVER_INSTANCE_PARAMS"
  mssql.profiles.compute.name: "DEFAULT_OOB_COMPUTE"
  mssql.size: "10"

  # Time Machine (shared; per-type overrides possible, e.g. postgres.timeMachine.sla)
  timeMachine.sla: "DEFAULT_OOB_BRASS_SLA"
  timeMachine.dailySnapshotTime: "12:00:00"
  timeMachine.snapshotsPerDay: "1"
  timeMachine.logCatchUpFrequency: "30"
  timeMachine.weeklySnapshotDay: "MONDAY"
  timeMachine.monthlySnapshotDay: "1"

Example Usage

ConfigMap with Defaults

apiVersion: v1
kind: ConfigMap
metadata:
  name: ndb-defaults
data:
  timezone: "UTC"
  clusterName: "prod-cluster"
  postgres.profiles.software.name: "POSTGRES_14.13_OOB"
  postgres.profiles.network.name: "DEFAULT_OOB_POSTGRESQL_NETWORK"
  postgres.profiles.dbParam.name: "DEFAULT_POSTGRES_PARAMS"
  postgres.profiles.compute.name: "DEFAULT_OOB_COMPUTE"
  postgres.size: "10"
  timeMachine.sla: "DEFAULT_OOB_BRASS_SLA"
  timeMachine.dailySnapshotTime: "12:00:00"
  timeMachine.snapshotsPerDay: "1"
  # ... additional settings

Minimal Database CR (Provisioning)

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: pgsi-provision-dbcr
  namespace: default
spec:
  ndbRef: ndb-191
  defaultsConfigMapRef: ndb-provisioning-defaults
  databaseInstance:
    name: pgsi-provision-dbcr
    credentialSecret: db-secret-pg-si
    type: postgres
    profiles: {}
    additionalArguments: {}

Minimal Clone CR (Auto-snapshot)

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: pgsi-clone-dbcr
  namespace: default
spec:
  ndbRef: ndb-191
  defaultsConfigMapRef: ndb-provisioning-defaults
  isClone: true
  clone:
    name: pgsi-clone-dbcr
    type: postgres
    credentialSecret: db-secret-pg-si
    sourceDatabaseName: pgsi-provision-dbcr
    profiles: {}
    additionalArguments: {}

Tested Provisioning All for DBs and cloning of PGSI

image

Traditional CR PGSI Provisioning Test :

image

Negative test : Bad software profile

Shows clear controller events with :

Type Reason Message
Warning RequestGenerationFailed Could not generate database provisioning request. could not resolve profile using the provided name=THIS_SOFTWARE_PROFILE_DOES_NOT_EXIST
Warning NDBRequestFailed Failed to create database on NDB. could not resolve profile using the provided name=THIS_SOFTWARE_PROFILE_DOES_NOT_EXIST

Scenarios:

Wrong profile name in the CR
ConfigMap only fills profiles when id and name are empty. If the user sets a name (even wrong), that value wins; ConfigMap does not fix it. Reconcile fails with “could not resolve profile … name=…”. Fix the name/id in the CR, or clear both to let defaults apply again.

Traditional CR, empty profiles (open-source) :
Empty id/name → operator picks NDB OOB profiles via its built-in filters (not arbitrary). MSSQL still needs software in the CR.

ConfigMap has a custom profile; user wanted “NDB default” so left CR empty

Empty CR + key in ConfigMap → ConfigMap value is applied, not NDB OOB.

Handle it: Omit profile keys from the org ConfigMap if you want OOB; use a slimmer ConfigMap for those teams; or put the real NDB profile name in the CR once known. There is no separate “use NDB OOB” flag.

Test Coverage: 20/20 Tests Passed

Category Tests Status
Functionality PostgreSQL & MySQL provisioning with ConfigMap defaults Cloning with auto-snapshot, explicit name, and explicit ID 5/5
Backward Compatibility Provisioning & cloning without ConfigMap (traditional flow) Webhook validation for non-ConfigMap CRs 4/4
Override Behavior Explicit CR values override ConfigMap defaults Verified with size, timezone, and profile overrides 4/4
Error Handling Missing ConfigMap, empty ConfigMap, invalid profiles Invalid source DB, invalid snapshot, Time Machine not ready 7/7

Key Validations

  • ConfigMap integration working correctly
  • Database-specific profiles (postgres vs mysql) applied correctly
  • Name-based resolution for cluster, database, and snapshot
  • Auto-snapshot selection for latest snapshot
  • Explicit CR values override ConfigMap defaults
  • Backward compatibility: traditional flows unchanged
  • Error handling: all errors reported via Kubernetes events with clear messages
  • RBAC permissions configured correctly

Documentation

  • Updated README.md with ConfigMap usage, supported keys table, key precedence, and "How it works" (namespace, missing ConfigMap behavior)

Testing Summary and Logs

1. Functionality Testing

1.1 PostgreSQL Provisioning with ConfigMap Defaults

Test File: test-pgsi-1.yaml

Minimal CR:

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: pgsi-1
spec:
  ndbRef: ndb-test-163
  defaultsConfigMapRef: ndb-163-defaults  # Reference to ConfigMap
  isClone: false
  databaseInstance:
    type: postgres
    name: pgsi-1
    databaseNames: ["testdb"]
    credentialSecret: pgsi-1-secret

ConfigMap Defaults Applied:

  • Cluster: auto_cluster_nested_697cec1c92fce9da8fbb8668
  • Timezone: UTC
  • Software Profile: POSTGRES_14.13_OOB
  • Network Profile: DEFAULT_OOB_POSTGRESQL_NETWORK
  • DbParam Profile: DEFAULT_POSTGRES_PARAMS
  • Compute Profile: DEFAULT_OOB_COMPUTE
  • Size: 10 GB
  • Time Machine SLA: DEFAULT_OOB_BRASS_SLA

Logs (from defaulter webhook in operator pod):

INFO    Fetching defaults ConfigMap    {"namespace": "default", "configMapName": "ndb-163-defaults"}
INFO    Successfully fetched defaults ConfigMap    {"configMapName": "ndb-163-defaults", "keysCount": 12}
INFO    Applying defaults from ConfigMap    {"databaseName": "pgsi-1"}
INFO    Applied default clusterName    {"value": "auto_cluster_nested_697cec1c92fce9da8fbb8668"}
INFO    Applied default profiles.software.name    {"value": "POSTGRES_14.13_OOB"}
INFO    Applied default profiles.network.name    {"value": "DEFAULT_OOB_POSTGRESQL_NETWORK"}
INFO    Applied default profiles.dbParam.name    {"value": "DEFAULT_POSTGRES_PARAMS"}
INFO    Defaults applied successfully    {"databaseName": "pgsi-1", "dbType": "postgres"}

Result: Database pgsi-1 successfully created on NDB with all defaults properly applied.


1.2 MySQL Provisioning with ConfigMap Defaults

Test File: test-mysql-1.yaml

Minimal CR:

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: mysql-1
spec:
  ndbRef: ndb-test-163
  defaultsConfigMapRef: ndb-163-defaults
  isClone: false
  databaseInstance:
    type: mysql
    name: mysql-1
    databaseNames: ["testdb"]
    credentialSecret: mysql-1-secret

ConfigMap Defaults Applied:

  • Software Profile: MYSQL_8.0.37_OOB
  • Network Profile: DEFAULT_OOB_MYSQL_NETWORK
  • DbParam Profile: DEFAULT_MYSQL_PARAMS
  • All other defaults same as PostgreSQL

Logs (from defaulter webhook in operator pod):

INFO    Applied default profiles.software.name    {"value": "MYSQL_8.0.37_OOB"}
INFO    Applied default profiles.network.name    {"value": "DEFAULT_OOB_MYSQL_NETWORK"}
INFO    Applied default profiles.dbParam.name    {"value": "DEFAULT_MYSQL_PARAMS"}
INFO    Defaults applied successfully    {"databaseName": "mysql-1", "dbType": "mysql"}

Result: Database mysql-1 successfully created with database-specific MySQL profiles.


1.3 Cloning with Auto-Snapshot Selection

Test File: test-clone-pgsi-auto-snapshot.yaml

Minimal Clone CR (no snapshot specified):

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: clone-pgsi-auto
spec:
  ndbRef: ndb-test-163
  defaultsConfigMapRef: ndb-163-defaults
  isClone: true
  clone:
    type: postgres
    name: clone-pgsi-auto
    sourceDatabaseName: "pgsi-1"
    # No snapshotName or snapshotId - operator auto-selects latest
    credentialSecret: clone-pgsi-auto-secret

Operator Logs:

INFO    Both snapshotId and snapshotName are empty, auto-selecting latest snapshot
INFO    Fetching source database to get timeMachineId
INFO    Source database found    {"databaseId": "xxx", "timeMachineId": "e9e70cbf-8a42-4059-8298-be9126004e69"}
INFO    Fetching all snapshots for time machine
INFO    Found 3 snapshots, selecting latest by timestamp
INFO    Auto-selected latest snapshot    {"snapshotId": "db842c71-1ca3-4a5f-9ab5-c66c3644436b", "snapshotName": "snapthot1"}

Result: Clone successfully created using auto-selected latest snapshot.


1.4 Cloning with Explicit Snapshot Name

Test File: test-clone-pgsi-with-name.yaml

spec:
  clone:
    sourceDatabaseName: "pgsi-1"
    snapshotName: "snapthot1"  # Explicit snapshot name

Operator Logs:

INFO    Resolving snapshot name 'snapthot1' to UUID
INFO    Snapshot resolved successfully    {"snapshotName": "snapthot1", "snapshotId": "db842c71-1ca3-4a5f-9ab5-c66c3644436b"}

Result: Clone created successfully using explicit snapshot name.


1.5 Cloning with Explicit Snapshot ID

Test File: test-clone-pgsi-with-id.yaml

spec:
  clone:
    sourceDatabaseName: "pgsi-1"
    snapshotId: "db842c71-1ca3-4a5f-9ab5-c66c3644436b"  # Explicit UUID

Result: Clone created successfully using explicit snapshot UUID (no name resolution needed).


2. Backward Compatibility Testing

2.1 Provisioning WITHOUT ConfigMap (Traditional Flow)

Test File: test-pgsi-2-backward-compat.yaml

Traditional CR (no ConfigMap reference):

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: pgsi-2
spec:
  ndbRef: ndb-test-163
  # No defaultsConfigMapRef
  isClone: false
  databaseInstance:
    type: postgres
    name: pgsi-2
    databaseNames: ["testdb"]
    credentialSecret: pgsi-2-secret
    clusterId: "697cec1c-92fc-e9da-8fbb-8668b54a1639"
    timezone: "UTC"
    profiles:
      software:
        id: "e0038090-fd0b-4b0f-87b4-8c992a9620d9"
      network:
        id: "3e8d0bd0-da1b-49ef-b1e5-ed72cf8d53ee"
      dbParam:
        id: "24fb5e63-b3d3-418e-b27d-d2bc4a572be9"
      compute:
        id: "d3b16b1c-eaea-4a74-91b9-6865a42c3e48"
    size: 10
    timeMachine:
      sla: "DEFAULT_OOB_BRASS_SLA"
      # ... all other fields explicitly provided

Result: Database pgsi-2 successfully created. Traditional provisioning flow works unchanged.

Verification:

$ kubectl get database pgsi-2
NAME     STATUS    DATABASE-ID
pgsi-2   READY     <uuid>

2.2 Cloning WITHOUT ConfigMap (Traditional Flow)

Test File: test-clone-pgsi-1-backward-compat.yaml

Traditional Clone CR:

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: clone-pgsi-1-noconfig
spec:
  ndbRef: ndb-test-163
  # No defaultsConfigMapRef
  isClone: true
  clone:
    type: postgres
    name: clone-pgsi-1-noconfig
    sourceDatabaseId: "<uuid>"
    snapshotId: "<uuid>"
    clusterId: "<uuid>"
    timezone: "UTC"
    # ... all fields explicitly provided
    credentialSecret: clone-pgsi-1-noconfig-secret

Result: Clone clone-pgsi-1-noconfig successfully created. Traditional cloning flow works unchanged.

2.3. Webhook validation (traditional flow, no ConfigMap)

Test: Provision CR with no defaultsConfigMapRef, no cluster, no ConfigMap, no size (minimal invalid body).

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: bc-webhook-invalid-20260326
  namespace: default
spec:
  ndbRef: ndb-191
  isClone: false
  databaseInstance:
    name: bc-webhook-invalid-20260326
    credentialSecret: db-secret-pg-si
    type: postgres

Actual admission response (this run):

Error from server (Forbidden): admission webhook "vdatabase.kb.io" denied the request:
spec.Instance.clusterId: Invalid value: "": Either clusterId or clusterName must be provided;
spec.Instance.size: Invalid value: 0: Initial Database size must be specified with a value 10 GBs or more

Result: Webhook correctly enforces validation when ConfigMap is not used.

3. Override Behavior Testing

3.1 Provisioning with overrides

ConfigMap: timezone: UTC, postgres.size: "10"CR overrides with size: 20, timezone: Asia/Kolkata.

CR (override-demo-provision):

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: override-demo-provision
  namespace: default
spec:
  ndbRef: ndb-191
  defaultsConfigMapRef: ndb-provisioning-defaults
  isClone: false
  databaseInstance:
    type: postgres
    name: override-demo-provision
    databaseNames: ["testdb"]
    credentialSecret: db-secret-pg-si
    size: 20
    timezone: "Asia/Kolkata"

Admission (mutating webhook) — same requestID on each line; ConfigMap fetched (keysCount: 28); defaults applied for cluster/profiles/Time Machineno Applied default size / Applied default timezone (CR wins for those).

2026-03-26T11:13:02Z	INFO	admission	Entering Default()...	{"Database": {"name":"override-demo-provision","namespace":"default"}, ... "requestID": "90a2f9ea-c229-49f2-8d6f-ecef24c03359"}
2026-03-26T11:13:02Z	INFO	admission	Fetching defaults ConfigMap	{...,"configMapName": "ndb-provisioning-defaults"}
2026-03-26T11:13:02Z	INFO	admission	Successfully fetched defaults ConfigMap	{...,"configMapName": "ndb-provisioning-defaults", "keysCount": 28}
2026-03-26T11:13:02Z	INFO	admission	Applying defaults from ConfigMap	{...,"databaseName": "override-demo-provision"}
2026-03-26T11:13:02Z	INFO	admission	Applied default clusterName	{...,"value": "auto_cluster_nested_699fdfa37298f635e3071a95"}
2026-03-26T11:13:02Z	INFO	admission	Applied default profiles.software.name	{...,"value": "POSTGRES_15.6_ROCKY_LINUX_8_OOB"}
2026-03-26T11:13:02Z	INFO	admission	Applied default profiles.compute.name	{...,"value": "DEFAULT_OOB_COMPUTE"}
2026-03-26T11:13:02Z	INFO	admission	Applied default profiles.network.name	{...,"value": "DEFAULT_OOB_POSTGRESQL_NETWORK"}
2026-03-26T11:13:02Z	INFO	admission	Applied default profiles.dbParam.name	{...,"value": "DEFAULT_POSTGRES_PARAMS"}
2026-03-26T11:13:02Z	INFO	admission	Applied default timeMachine.sla	{...,"value": "DEFAULT_OOB_BRASS_SLA"}
2026-03-26T11:13:02Z	INFO	admission	Applied default timeMachine.dailySnapshotTime	{...,"value": "12:00:00"}
2026-03-26T11:13:02Z	INFO	admission	Applied default timeMachine.snapshotsPerDay	{...,"value": 1}
2026-03-26T11:13:02Z	INFO	admission	Applied default timeMachine.logCatchUpFrequency	{...,"value": 30}
2026-03-26T11:13:02Z	INFO	admission	Applied default timeMachine.weeklySnapshotDay	{...,"value": "MONDAY"}
2026-03-26T11:13:02Z	INFO	admission	Applied default timeMachine.monthlySnapshotDay	{...,"value": 1}
2026-03-26T11:13:02Z	INFO	admission	Defaults applied successfully	{...,"databaseName": "override-demo-provision", "dbType": "postgres"}
2026-03-26T11:13:02Z	INFO	admission	Exiting Default()!	{...}
2026-03-26T11:13:02Z	INFO	admission	Entering ValidateCreate...	{...}
2026-03-26T11:13:02Z	INFO	admission	ValidateCreate webhook response...	{...,"combined_err": null}
2026-03-26T11:13:02Z	INFO	admission	Exiting ValidateCreate!	{...}

Operator log — Database Provisioning (requestBody sanitized):

{
  "name": "override-demo-provision",
  "newDbServerTimeZone": "Asia/Kolkata",
  "timeMachineInfo": {
    "name": "override-demo-provision_TM"
  },
  "actionArguments": [
    { "name": "database_size", "value": "20" }
  ]
}

Result: size and timezone on the CR override ConfigMap postgres.size / timezone.


3.2 Cloning with overrides

ConfigMap: timezone: UTCCR overrides clone timezone: Europe/London.

CR (override-demo-clone):

apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: override-demo-clone
  namespace: default
spec:
  ndbRef: ndb-191
  defaultsConfigMapRef: ndb-provisioning-defaults
  isClone: true
  clone:
    name: override-demo-clone
    type: postgres
    credentialSecret: db-secret-pg-si
    sourceDatabaseName: pgsi-provision-dbcr
    timezone: "Europe/London"
    profiles: {}
    additionalArguments: {}

Admission (mutating webhook) — ConfigMap applied for cluster/profiles; no Applied default timezone (timezone set on CR).

2026-03-26T11:13:05Z	INFO	admission	Entering Default()...	{"Database": {"name":"override-demo-clone","namespace":"default"}, ... "requestID": "a83553e5-c811-4a56-be0d-925d764740eb"}
2026-03-26T11:13:05Z	INFO	admission	Fetching defaults ConfigMap	{...,"configMapName": "ndb-provisioning-defaults"}
2026-03-26T11:13:05Z	INFO	admission	Successfully fetched defaults ConfigMap	{...,"keysCount": 28}
2026-03-26T11:13:05Z	INFO	admission	Applying defaults from ConfigMap	{...,"databaseName": "override-demo-clone"}
2026-03-26T11:13:05Z	INFO	admission	Applied default clusterName	{...,"value": "auto_cluster_nested_699fdfa37298f635e3071a95"}
2026-03-26T11:13:05Z	INFO	admission	Applied default profiles.software.name	{...,"value": "POSTGRES_15.6_ROCKY_LINUX_8_OOB"}
2026-03-26T11:13:05Z	INFO	admission	Applied default profiles.compute.name	{...,"value": "DEFAULT_OOB_COMPUTE"}
2026-03-26T11:13:05Z	INFO	admission	Applied default profiles.network.name	{...,"value": "DEFAULT_OOB_POSTGRESQL_NETWORK"}
2026-03-26T11:13:05Z	INFO	admission	Applied default profiles.dbParam.name	{...,"value": "DEFAULT_POSTGRES_PARAMS"}
2026-03-26T11:13:05Z	INFO	admission	Defaults applied successfully	{...,"databaseName": "override-demo-clone", "dbType": "postgres"}
2026-03-26T11:13:05Z	INFO	admission	Exiting Default()!	{...}
2026-03-26T11:13:05Z	INFO	admission	ValidateCreate webhook response...	{...,"combined_err": null}

Operator log — Database Cloning (requestBody sanitized):

{
  "name": "override-demo-clone",
  "timeZone": "Europe/London",
  "timeMachineId": "2715137d-079d-46a7-a828-66a3ba6d74ab",
  "snapshotId": "b1a5a9df-2872-4e4d-8545-be866f9edb9a",
  "nxClusterId": "1b243c01-56f4-4c8c-af00-f50ae117e04e"
}

Result: Clone timezone on the CR overrides ConfigMap UTC.


4. Edge Cases and Error Handling

4.1 Non-existent ConfigMap — handled

Test: docs/configmap-defaults-pr/14-neg-missing-configmap.yaml (defaultsConfigMapRef → missing ConfigMap).

Admission: Request denied (resource not created). Example:

Error from server (Forbidden): ... admission webhook "vdatabase.kb.io" denied the request:
spec.Instance.clusterId: Invalid value: "": Either clusterId or clusterName must be provided;
spec.Instance.size: Invalid value: 0: Initial Database size must be specified with a value 10 GBs or more

Operator / admission logs (excerpt):

2026-03-26T10:54:56Z	INFO	admission	Fetching defaults ConfigMap	{...,"configMapName": "ndb-provisioning-defaults-this-name-does-not-exist"}
2026-03-26T10:54:56Z	INFO	admission	ConfigMap not found or error fetching	{...,"error": "configmaps \"ndb-provisioning-defaults-this-name-does-not-exist\" not found"}
2026-03-26T10:54:56Z	INFO	admission	Could not fetch ConfigMap, proceeding without ConfigMap defaults	{...}
2026-03-26T10:54:56Z	INFO	admission	ValidateCreate webhook response...	{...,"combined_err": "spec.Instance.clusterId: Invalid value: \"\": Either clusterId or clusterName must be provided; spec.Instance.size: Invalid value: 0: ..."}

4.2 Empty ConfigMap (data: {}) — handled

Test: ConfigMap ndb-empty-defaults-test with data: {} + Database neg-empty-cm-test referencing it (minimal spec with size: 10 and clusterName so admission passes).

Admission logs (excerpt):

2026-03-26T10:56:02Z	INFO	admission	Fetching defaults ConfigMap	{...,"configMapName": "ndb-empty-defaults-test"}
2026-03-26T10:56:02Z	INFO	admission	ConfigMap is empty	{...,"configMapName": "ndb-empty-defaults-test"}
2026-03-26T10:56:02Z	INFO	admission	ValidateCreate webhook response...	{...,"combined_err": null}

No ConfigMap keys are applied; standard defaulter still runs; reconcile proceeds if the rest of the spec is valid.


4.3 Invalid profile name — handled

Test: 15-neg-bad-profile-configmap.yaml + 16-neg-bad-profile-database.yaml (postgres.profiles.software.name: THIS_SOFTWARE_PROFILE_DOES_NOT_EXIST).

Admission (excerpt): ConfigMap fetched, bad software name applied from map, validation passes.

2026-03-26T10:55:41Z	INFO	admission	Applied default profiles.software.name	{...,"value": "THIS_SOFTWARE_PROFILE_DOES_NOT_EXIST"}

Reconcile / controller (excerpt):

2026-03-26T10:55:42Z	ERROR	Software Profile could not be resolved or is not in READY state	{...,"error": "could not resolve profile using the provided name=THIS_SOFTWARE_PROFILE_DOES_NOT_EXIST"}
2026-03-26T10:55:42Z	ERROR	Could not generate database provisioning request	{...,"error": "could not resolve profile using the provided name=THIS_SOFTWARE_PROFILE_DOES_NOT_EXIST"}

Events (kubectl describe database neg-bad-profile):

Warning  RequestGenerationFailed  ...  Error: Could not generate database provisioning request. could not resolve profile using the provided name=THIS_SOFTWARE_PROFILE_DOES_NOT_EXIST
Warning  NDBRequestFailed         ...  Error: Failed to create database on NDB. could not resolve profile using the provided name=THIS_SOFTWARE_PROFILE_DOES_NOT_EXIST

4.4 Invalid source database name — handled

Test: Clone neg-invalid-src with sourceDatabaseName: this-database-does-not-exist-xyz (no ConfigMap).

Events:

Warning  RequestGenerationFailed  ...  Error: Failed to resolve names to UUIDs. failed to resolve source database name 'this-database-does-not-exist-xyz': database with name 'this-database-does-not-exist-xyz' not found
Warning  NDBRequestFailed         ...  Error: Failed to create database on NDB. failed to resolve source database name 'this-database-does-not-exist-xyz': ...

4.5 Invalid snapshot name — handled

Test: Clone neg-invalid-snap with valid sourceDatabaseName: pgsi-provision-dbcr and snapshotName: this-snapshot-does-not-exist-xyz.

Events (NDB may include time machine id in the message):

Warning  RequestGenerationFailed  ...  Error: Failed to resolve names to UUIDs. failed to resolve snapshot name 'this-snapshot-does-not-exist-xyz': snapshot with name 'this-snapshot-does-not-exist-xyz' not found in time machine '2715137d-079d-46a7-a828-66a3ba6d74ab'

4.6 Webhook + ConfigMap (defaulter → validation)

Behavior (verified across tests above): Mutating webhook loads ConfigMap when referenced, applies defaults only to empty fields, then runs the standard defaulter. Validating webhook runs on the merged object (ValidateCreate / combined_err in logs).

Key validations

  • ConfigMap defaults: mutating webhook before validation; explicit CR values win over ConfigMap for non-empty fields.
  • Engine-specific ConfigMap keys (e.g. postgres.*) override generic keys when set and non-empty.
  • Name resolution (cluster / DB / snapshot) and profile resolution errors surface as RequestGenerationFailed / NDBRequestFailed Events with operator/NDB text.
  • Auto-snapshot: omit both snapshot id and name on clone → operator selects latest (not exercised in the snippets above; covered by clone flow + code).
  • Traditional CRs without defaultsConfigMapRef unchanged.
  • RBAC: operator must be able to get ConfigMaps in the Database namespace

LCMConfig Jira test (ERA-57641)

Goal: Clone Postgres with LCM additionalArguments (expireInDays, expiryDateTimezone, deleteDatabase) and confirm the controller builds a clone request with lcmConfig and does not panic on nil LcmConfig.

YAML exercised

# ERA-57641 style: Postgres clone with LCM additionalArguments.
# Adjust: metadata.name, namespace, ndbRef, credentialSecret, sourceDatabaseName, defaultsConfigMapRef.
apiVersion: ndb.nutanix.com/v1alpha1
kind: Database
metadata:
  name: era57641-lcm-clone-test
  namespace: default
spec:
  isClone: true
  ndbRef: ndb-191
  defaultsConfigMapRef: ndb-provisioning-defaults
  clone:
    type: postgres
    name: era57641-lcm-clone-test
    credentialSecret: db-secret-pg-si
    sourceDatabaseName: pgsi-provision-dbcr
    additionalArguments:
      expireInDays: "7"
      expiryDateTimezone: "UTC"
      deleteDatabase: "true"

Repo path: docs/configmap-defaults-pr/era57641-lcm-clone-test.yaml

operator logs (verification)

  • No panic / stack trace in controller logs for this reconcile.
  • Log lines include Entered ndb_api.GenerateCloningRequest and Returning from ndb_api.GenerateCloningRequest for the Database name used above.
  • Log line Database Cloning with requestBody containing lcmConfig and nested databaseLCMConfig / expiryDetails with the three values (expireInDays, expiryDateTimezone, deleteDatabase).

image

Copy link
Copy Markdown
Contributor

@shivaprasadmb shivaprasadmb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few suggesions:

  1. Document all supported ConfigMap keys
    README already has a good example. Adding a short “Supported keys” subsection (e.g. clusterName, size, timezone, profiles., timeMachine., type-specific keys like postgres.size) would help admins.

  2. Validation after applying defaults
    Optional: after ApplyDefaultsToDatabase, run a quick check (e.g. for provisioning: cluster set, size >= 10, required TM fields) and emit a clear event or set a condition if something required is still missing, so “ConfigMap missing required key” is easier to diagnose.

  3. ConfigMap namespace
    Restricting the ConfigMap to the Database’s namespace is simple and consistent with RBAC. If you ever need cross-namespace defaults, you could add an optional defaultsConfigMapNamespace (or a small struct ref) later; not necessary for the current design.

Allow developers to specify minimal inputs in Database CR with admin-provided defaults via ConfigMap.
Values in CR take precedence over ConfigMap defaults.
…ecified

This commit adds the critical logic to skip validation for fields that
can be provided via ConfigMap defaults (cluster, size, profiles, timeMachine).

Key changes:
- Webhook detects when defaultsConfigMapRef is set
- Passes hasDefaultsConfigMap flag to validation handlers
- Validation handlers skip cluster/size/TM checks when ConfigMap ref is present
- Allows minimal Database CRs to pass validation successfully

Tested: Successfully provisioned databases using minimal CRs with ConfigMap defaults.
…iles

- Add postgres.profiles.dbParam.name for PostgreSQL-specific defaults
- Add mysql.profiles.dbParam.name for MySQL-specific defaults
- Add MySQL-specific example section in ConfigMap
- Clarify that dbParam profiles should be database-type-specific
- Updated webhook validation to allow omitting both snapshotId and snapshotName when defaultsConfigMapRef is set
- Added automatic selection of latest snapshot when both snapshot fields are empty
- Implemented GetLatestSnapshotForTM() in ndb_api to fetch most recent snapshot by timestamp
- Updated README with documentation on auto-snapshot feature

Testing: Verified with PostgreSQL and MySQL clones, backward compatibility maintained
Changes:
- Removed ConfigMap requirement for auto-snapshot feature
- Auto-snapshot now works when both snapshotId and snapshotName are omitted
- Updated webhook validation: removed hasDefaultsConfigMap check
- Updated test: changed from expecting error to expecting success
- Updated documentation: clarified auto-snapshot works in any scenario

This makes features orthogonal:
- ConfigMap defaults = provide default configuration values
- Auto-snapshot = automatically select latest snapshot

Benefits:
- Better UX: no need for ConfigMap just to get auto-snapshot
- More flexible: supports common "clone latest" use case
- Simpler mental model: omit snapshot = latest

Testing:
-  All 7 webhook validation scenarios pass
-  Auto-snapshot works without ConfigMap (new behavior)
-  Backward compatibility maintained
-  ConfigMap + auto-snapshot still works
…nts (ERA-57641)

- Initialize req.LcmConfig before dereferencing to avoid panic
- Fix refreshDetails error condition: use refreshDetailsCount instead of databaseLcmConfigCount
- Fix typo in error message: refreshInDay -> refreshInDays
Copy link
Copy Markdown
Contributor

@shivaprasadmb shivaprasadmb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread api/v1alpha1/webhook_helpers.go Outdated
Copy link
Copy Markdown
Contributor

@manavrajvanshi manavrajvanshi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most changes look good to me, can you please do the following -

  1. Add unit tests for the changes made and new functions added.
  2. Attach screenshots for how you tested the configmap defaults.

- Document empty ConfigMap data, RBAC (get/list ConfigMaps), and precedence
- Split engine size vs profile keys; size is provision-only
- Note OOB vs ConfigMap for profile slots; fix typos (Default, Development)

Made-with: Cursor
@sasikanthmasini
Copy link
Copy Markdown
Contributor Author

Most changes look good to me, can you please do the following -

  1. Add unit tests for the changes made and new functions added.
  2. Attach screenshots for how you tested the configmap defaults.

Added unit tests and attached screenshots of testing.

@sasikanthmasini sasikanthmasini changed the title ERA-60185 : Add ConfigMap defaults support for Database CR ERA-60185 ERA-57641 : Add ConfigMap defaults support for Database CR and LCMConfig data Bug Mar 27, 2026
@sasikanthmasini sasikanthmasini changed the title ERA-60185 ERA-57641 : Add ConfigMap defaults support for Database CR and LCMConfig data Bug ERA-60185 : Add ConfigMap defaults support for Database CR and LCMConfig data Bug Mar 27, 2026
@sasikanthmasini sasikanthmasini changed the title ERA-60185 : Add ConfigMap defaults support for Database CR and LCMConfig data Bug ERA-60185 ERA-57641 : Add ConfigMap defaults support for Database CR and LCMConfig data Bug Mar 27, 2026
Copy link
Copy Markdown
Contributor

@shivaprasadmb shivaprasadmb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the review comments and also run make fmt to format all the files

Comment thread api/v1alpha1/configmap_defaults.go Outdated
Comment thread README.md
Copy link
Copy Markdown
Contributor

@shivaprasadmb shivaprasadmb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread ndb_api/clone_helpers_test.go
Copy link
Copy Markdown
Contributor

@manavrajvanshi manavrajvanshi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a comment about the nil bug.

@manavrajvanshi manavrajvanshi merged commit 7fcc8e5 into main Mar 31, 2026
9 checks passed
@manavrajvanshi manavrajvanshi deleted the configmap-defaults branch March 31, 2026 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants