Skip to content

Commit ab7f542

Browse files
authored
adds ability to configure database details for registry server (#2800)
* adds ability to configure database details for registry server Signed-off-by: Chris Burns <[email protected]> * bunmps crds chart Signed-off-by: Chris Burns <[email protected]> --------- Signed-off-by: Chris Burns <[email protected]>
1 parent 59e4399 commit ab7f542

File tree

8 files changed

+433
-2
lines changed

8 files changed

+433
-2
lines changed

cmd/thv-operator/api/v1alpha1/mcpregistry_types.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ type MCPRegistrySpec struct {
5454
// +kubebuilder:pruning:PreserveUnknownFields
5555
// +kubebuilder:validation:Type=object
5656
PodTemplateSpec *runtime.RawExtension `json:"podTemplateSpec,omitempty"`
57+
58+
// DatabaseConfig defines the PostgreSQL database configuration for the registry API server.
59+
// If not specified, defaults will be used:
60+
// - Host: "postgres"
61+
// - Port: 5432
62+
// - User: "db_app"
63+
// - MigrationUser: "db_migrator"
64+
// - Database: "registry"
65+
// - SSLMode: "prefer"
66+
// - MaxOpenConns: 10
67+
// - MaxIdleConns: 2
68+
// - ConnMaxLifetime: "30m"
69+
// +optional
70+
DatabaseConfig *MCPRegistryDatabaseConfig `json:"databaseConfig,omitempty"`
5771
}
5872

5973
// MCPRegistryConfig defines the configuration for a registry data source
@@ -187,6 +201,66 @@ type TagFilter struct {
187201
Exclude []string `json:"exclude,omitempty"`
188202
}
189203

204+
// MCPRegistryDatabaseConfig defines PostgreSQL database configuration for the registry API server.
205+
// Uses a two-user security model: separate users for operations and migrations.
206+
type MCPRegistryDatabaseConfig struct {
207+
// Host is the database server hostname
208+
// +kubebuilder:default="postgres"
209+
// +optional
210+
Host string `json:"host,omitempty"`
211+
212+
// Port is the database server port
213+
// +kubebuilder:default=5432
214+
// +kubebuilder:validation:Minimum=1
215+
// +kubebuilder:validation:Maximum=65535
216+
// +optional
217+
Port int `json:"port,omitempty"`
218+
219+
// User is the application user (limited privileges: SELECT, INSERT, UPDATE, DELETE)
220+
// Credentials should be provided via pgpass file or environment variables
221+
// +kubebuilder:default="db_app"
222+
// +optional
223+
User string `json:"user,omitempty"`
224+
225+
// MigrationUser is the migration user (elevated privileges: CREATE, ALTER, DROP)
226+
// Used for running database schema migrations
227+
// Credentials should be provided via pgpass file or environment variables
228+
// +kubebuilder:default="db_migrator"
229+
// +optional
230+
MigrationUser string `json:"migrationUser,omitempty"`
231+
232+
// Database is the database name
233+
// +kubebuilder:default="registry"
234+
// +optional
235+
Database string `json:"database,omitempty"`
236+
237+
// SSLMode is the SSL mode for the connection
238+
// Valid values: disable, allow, prefer, require, verify-ca, verify-full
239+
// +kubebuilder:validation:Enum=disable;allow;prefer;require;verify-ca;verify-full
240+
// +kubebuilder:default="prefer"
241+
// +optional
242+
SSLMode string `json:"sslMode,omitempty"`
243+
244+
// MaxOpenConns is the maximum number of open connections to the database
245+
// +kubebuilder:default=10
246+
// +kubebuilder:validation:Minimum=1
247+
// +optional
248+
MaxOpenConns int `json:"maxOpenConns,omitempty"`
249+
250+
// MaxIdleConns is the maximum number of idle connections in the pool
251+
// +kubebuilder:default=2
252+
// +kubebuilder:validation:Minimum=0
253+
// +optional
254+
MaxIdleConns int `json:"maxIdleConns,omitempty"`
255+
256+
// ConnMaxLifetime is the maximum amount of time a connection may be reused (Go duration format)
257+
// Examples: "30m", "1h", "24h"
258+
// +kubebuilder:validation:Pattern=^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$
259+
// +kubebuilder:default="30m"
260+
// +optional
261+
ConnMaxLifetime string `json:"connMaxLifetime,omitempty"`
262+
}
263+
190264
// MCPRegistryStatus defines the observed state of MCPRegistry
191265
type MCPRegistryStatus struct {
192266
// Phase represents the current overall phase of the MCPRegistry

cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/thv-operator/pkg/registryapi/config/config.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,41 @@ type Config struct {
103103
// Defaults to "default" if not specified
104104
RegistryName string `yaml:"registryName,omitempty"`
105105
Registries []RegistryConfig `yaml:"registries"`
106+
Database *DatabaseConfig `yaml:"database,omitempty"`
107+
}
108+
109+
// DatabaseConfig defines PostgreSQL database configuration
110+
// Uses two-user security model: separate users for operations and migrations
111+
type DatabaseConfig struct {
112+
// Host is the database server hostname
113+
Host string `yaml:"host"`
114+
115+
// Port is the database server port
116+
Port int `yaml:"port"`
117+
118+
// User is the application user (limited privileges: SELECT, INSERT, UPDATE, DELETE)
119+
// Credentials provided via pgpass file
120+
User string `yaml:"user"`
121+
122+
// MigrationUser is the migration user (elevated privileges: CREATE, ALTER, DROP)
123+
// Used for running database schema migrations
124+
// Credentials provided via pgpass file
125+
MigrationUser string `yaml:"migrationUser"`
126+
127+
// Database is the database name
128+
Database string `yaml:"database"`
129+
130+
// SSLMode is the SSL mode for the connection
131+
SSLMode string `yaml:"sslMode"`
132+
133+
// MaxOpenConns is the maximum number of open connections to the database
134+
MaxOpenConns int `yaml:"maxOpenConns"`
135+
136+
// MaxIdleConns is the maximum number of idle connections in the pool
137+
MaxIdleConns int `yaml:"maxIdleConns"`
138+
139+
// ConnMaxLifetime is the maximum amount of time a connection may be reused
140+
ConnMaxLifetime string `yaml:"connMaxLifetime"`
106141
}
107142

108143
// RegistryConfig defines the configuration for a registry data source
@@ -228,6 +263,9 @@ func (cm *configManager) BuildConfig() (*Config, error) {
228263
config.Registries = append(config.Registries, *registryConfig)
229264
}
230265

266+
// Build database configuration from CRD spec or use defaults
267+
config.Database = buildDatabaseConfig(mcpRegistry.Spec.DatabaseConfig)
268+
231269
return &config, nil
232270
}
233271

@@ -371,3 +409,56 @@ func buildAPISourceConfig(api *mcpv1alpha1.APISource) (*APIConfig, error) {
371409
Endpoint: api.Endpoint,
372410
}, nil
373411
}
412+
413+
// buildDatabaseConfig creates a DatabaseConfig from the CRD spec.
414+
// If the spec is nil or fields are empty, sensible defaults are used.
415+
func buildDatabaseConfig(dbConfig *mcpv1alpha1.MCPRegistryDatabaseConfig) *DatabaseConfig {
416+
// Default values
417+
config := &DatabaseConfig{
418+
Host: "postgres",
419+
Port: 5432,
420+
User: "db_app",
421+
MigrationUser: "db_migrator",
422+
Database: "registry",
423+
SSLMode: "prefer",
424+
MaxOpenConns: 10,
425+
MaxIdleConns: 2,
426+
ConnMaxLifetime: "30m",
427+
}
428+
429+
// If no database config specified, return defaults
430+
if dbConfig == nil {
431+
return config
432+
}
433+
434+
// Override defaults with values from CRD spec if provided
435+
if dbConfig.Host != "" {
436+
config.Host = dbConfig.Host
437+
}
438+
if dbConfig.Port != 0 {
439+
config.Port = dbConfig.Port
440+
}
441+
if dbConfig.User != "" {
442+
config.User = dbConfig.User
443+
}
444+
if dbConfig.MigrationUser != "" {
445+
config.MigrationUser = dbConfig.MigrationUser
446+
}
447+
if dbConfig.Database != "" {
448+
config.Database = dbConfig.Database
449+
}
450+
if dbConfig.SSLMode != "" {
451+
config.SSLMode = dbConfig.SSLMode
452+
}
453+
if dbConfig.MaxOpenConns != 0 {
454+
config.MaxOpenConns = dbConfig.MaxOpenConns
455+
}
456+
if dbConfig.MaxIdleConns != 0 {
457+
config.MaxIdleConns = dbConfig.MaxIdleConns
458+
}
459+
if dbConfig.ConnMaxLifetime != "" {
460+
config.ConnMaxLifetime = dbConfig.ConnMaxLifetime
461+
}
462+
463+
return config
464+
}

cmd/thv-operator/pkg/registryapi/config/config_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,3 +1000,149 @@ func TestBuildConfig_MultipleRegistries(t *testing.T) {
10001000
require.NotNil(t, config.Registries[1].Filter.Names)
10011001
assert.Equal(t, []string{"server-*"}, config.Registries[1].Filter.Names.Include)
10021002
}
1003+
1004+
func TestBuildConfig_DatabaseConfig(t *testing.T) {
1005+
t.Parallel()
1006+
1007+
t.Run("default database config when nil", func(t *testing.T) {
1008+
t.Parallel()
1009+
mcpRegistry := &mcpv1alpha1.MCPRegistry{
1010+
ObjectMeta: metav1.ObjectMeta{
1011+
Name: "test-registry",
1012+
},
1013+
Spec: mcpv1alpha1.MCPRegistrySpec{
1014+
Registries: []mcpv1alpha1.MCPRegistryConfig{
1015+
{
1016+
Name: "default",
1017+
Format: mcpv1alpha1.RegistryFormatToolHive,
1018+
ConfigMapRef: &corev1.ConfigMapKeySelector{
1019+
LocalObjectReference: corev1.LocalObjectReference{
1020+
Name: "test-configmap",
1021+
},
1022+
Key: "registry.json",
1023+
},
1024+
},
1025+
},
1026+
// DatabaseConfig not specified, should use defaults
1027+
},
1028+
}
1029+
1030+
manager := NewConfigManagerForTesting(mcpRegistry)
1031+
config, err := manager.BuildConfig()
1032+
1033+
require.NoError(t, err)
1034+
require.NotNil(t, config)
1035+
require.NotNil(t, config.Database)
1036+
1037+
// Verify default values
1038+
assert.Equal(t, "postgres", config.Database.Host)
1039+
assert.Equal(t, 5432, config.Database.Port)
1040+
assert.Equal(t, "db_app", config.Database.User)
1041+
assert.Equal(t, "db_migrator", config.Database.MigrationUser)
1042+
assert.Equal(t, "registry", config.Database.Database)
1043+
assert.Equal(t, "prefer", config.Database.SSLMode)
1044+
assert.Equal(t, 10, config.Database.MaxOpenConns)
1045+
assert.Equal(t, 2, config.Database.MaxIdleConns)
1046+
assert.Equal(t, "30m", config.Database.ConnMaxLifetime)
1047+
})
1048+
1049+
t.Run("custom database config", func(t *testing.T) {
1050+
t.Parallel()
1051+
mcpRegistry := &mcpv1alpha1.MCPRegistry{
1052+
ObjectMeta: metav1.ObjectMeta{
1053+
Name: "test-registry",
1054+
},
1055+
Spec: mcpv1alpha1.MCPRegistrySpec{
1056+
Registries: []mcpv1alpha1.MCPRegistryConfig{
1057+
{
1058+
Name: "default",
1059+
Format: mcpv1alpha1.RegistryFormatToolHive,
1060+
ConfigMapRef: &corev1.ConfigMapKeySelector{
1061+
LocalObjectReference: corev1.LocalObjectReference{
1062+
Name: "test-configmap",
1063+
},
1064+
Key: "registry.json",
1065+
},
1066+
},
1067+
},
1068+
DatabaseConfig: &mcpv1alpha1.MCPRegistryDatabaseConfig{
1069+
Host: "custom-postgres.example.com",
1070+
Port: 15432,
1071+
User: "custom_app_user",
1072+
MigrationUser: "custom_migrator",
1073+
Database: "custom_registry_db",
1074+
SSLMode: "require",
1075+
MaxOpenConns: 25,
1076+
MaxIdleConns: 5,
1077+
ConnMaxLifetime: "1h",
1078+
},
1079+
},
1080+
}
1081+
1082+
manager := NewConfigManagerForTesting(mcpRegistry)
1083+
config, err := manager.BuildConfig()
1084+
1085+
require.NoError(t, err)
1086+
require.NotNil(t, config)
1087+
require.NotNil(t, config.Database)
1088+
1089+
// Verify custom values
1090+
assert.Equal(t, "custom-postgres.example.com", config.Database.Host)
1091+
assert.Equal(t, 15432, config.Database.Port)
1092+
assert.Equal(t, "custom_app_user", config.Database.User)
1093+
assert.Equal(t, "custom_migrator", config.Database.MigrationUser)
1094+
assert.Equal(t, "custom_registry_db", config.Database.Database)
1095+
assert.Equal(t, "require", config.Database.SSLMode)
1096+
assert.Equal(t, 25, config.Database.MaxOpenConns)
1097+
assert.Equal(t, 5, config.Database.MaxIdleConns)
1098+
assert.Equal(t, "1h", config.Database.ConnMaxLifetime)
1099+
})
1100+
1101+
t.Run("partial database config uses defaults for missing fields", func(t *testing.T) {
1102+
t.Parallel()
1103+
mcpRegistry := &mcpv1alpha1.MCPRegistry{
1104+
ObjectMeta: metav1.ObjectMeta{
1105+
Name: "test-registry",
1106+
},
1107+
Spec: mcpv1alpha1.MCPRegistrySpec{
1108+
Registries: []mcpv1alpha1.MCPRegistryConfig{
1109+
{
1110+
Name: "default",
1111+
Format: mcpv1alpha1.RegistryFormatToolHive,
1112+
ConfigMapRef: &corev1.ConfigMapKeySelector{
1113+
LocalObjectReference: corev1.LocalObjectReference{
1114+
Name: "test-configmap",
1115+
},
1116+
Key: "registry.json",
1117+
},
1118+
},
1119+
},
1120+
DatabaseConfig: &mcpv1alpha1.MCPRegistryDatabaseConfig{
1121+
Host: "custom-host",
1122+
Database: "custom-db",
1123+
// Other fields omitted, should use defaults
1124+
},
1125+
},
1126+
}
1127+
1128+
manager := NewConfigManagerForTesting(mcpRegistry)
1129+
config, err := manager.BuildConfig()
1130+
1131+
require.NoError(t, err)
1132+
require.NotNil(t, config)
1133+
require.NotNil(t, config.Database)
1134+
1135+
// Verify custom values are used
1136+
assert.Equal(t, "custom-host", config.Database.Host)
1137+
assert.Equal(t, "custom-db", config.Database.Database)
1138+
1139+
// Verify defaults are used for omitted fields
1140+
assert.Equal(t, 5432, config.Database.Port)
1141+
assert.Equal(t, "db_app", config.Database.User)
1142+
assert.Equal(t, "db_migrator", config.Database.MigrationUser)
1143+
assert.Equal(t, "prefer", config.Database.SSLMode)
1144+
assert.Equal(t, 10, config.Database.MaxOpenConns)
1145+
assert.Equal(t, 2, config.Database.MaxIdleConns)
1146+
assert.Equal(t, "30m", config.Database.ConnMaxLifetime)
1147+
})
1148+
}

deploy/charts/operator-crds/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ apiVersion: v2
22
name: toolhive-operator-crds
33
description: A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.
44
type: application
5-
version: 0.0.70
5+
version: 0.0.71
66
appVersion: "0.0.1"

deploy/charts/operator-crds/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ToolHive Operator CRDs Helm Chart
22

3-
![Version: 0.0.70](https://img.shields.io/badge/Version-0.0.70-informational?style=flat-square)
3+
![Version: 0.0.71](https://img.shields.io/badge/Version-0.0.71-informational?style=flat-square)
44
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)
55

66
A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.

0 commit comments

Comments
 (0)