Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 58 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ The Database User Operator creates and manages databases and users declaratively
### Prerequisites

- Kubernetes cluster (v1.28+)
- PostgreSQL, MySQL, or MariaDB database instance
- AWS credentials with Secrets Manager permissions
- PostgreSQL 14+ (tested on 15, 16, 17), MySQL 8.0+, or MariaDB 10.6+
- AWS credentials with Secrets Manager permissions (only when using `secretBackend.aws` or `connectionString.aws`)

### Installation

Expand Down Expand Up @@ -78,12 +78,14 @@ metadata:
spec:
engine: postgres
databaseName: myapp_db
connectionStringSecretRef:
name: postgres-admin
awsSecretsManager:
region: us-east-1
tags:
Environment: production
connectionString:
kubernetes:
name: postgres-admin
secretBackend:
aws:
region: us-east-1
tags:
Environment: production
```

3. Apply and check status:
Expand Down Expand Up @@ -136,12 +138,14 @@ metadata:
spec:
engine: mysql
databaseName: myapp_db
connectionStringSecretRef:
name: mysql-admin
awsSecretsManager:
region: us-east-1
tags:
Environment: production
connectionString:
kubernetes:
name: mysql-admin
secretBackend:
aws:
region: us-east-1
tags:
Environment: production
```

3. Apply and check status:
Expand Down Expand Up @@ -181,10 +185,12 @@ metadata:
spec:
engine: mariadb # Uses MySQL driver and protocol
databaseName: myapp_db
connectionStringSecretRef:
name: mariadb-admin
awsSecretsManager:
region: us-east-1
connectionString:
kubernetes:
name: mariadb-admin
secretBackend:
aws:
region: us-east-1
```

The operator treats MariaDB identically to MySQL, using the same driver and SQL syntax. The stored secret will use the `MYSQL_URL` format for compatibility.
Expand All @@ -205,10 +211,12 @@ metadata:
spec:
engine: postgres
databaseName: spring_app
connectionStringSecretRef:
name: postgres-admin
awsSecretsManager:
region: us-east-1
connectionString:
kubernetes:
name: postgres-admin
secretBackend:
aws:
region: us-east-1
secretTemplate: |
{
"spring.datasource.url": "jdbc:postgresql://{{.DBHost}}:{{.DBPort}}/{{.DBName}}",
Expand All @@ -228,10 +236,12 @@ metadata:
spec:
engine: mysql
databaseName: simple_app
connectionStringSecretRef:
name: mysql-admin
awsSecretsManager:
region: us-east-1
connectionString:
kubernetes:
name: mysql-admin
secretBackend:
aws:
region: us-east-1
secretTemplate: |
{
"connectionString": "{{.DatabaseURL}}"
Expand All @@ -253,10 +263,12 @@ spec:
username: readonly_user
privileges:
- SELECT
connectionStringSecretRef:
name: postgres-admin
awsSecretsManager:
region: us-east-1
connectionString:
kubernetes:
name: postgres-admin
secretBackend:
aws:
region: us-east-1
```

### Custom Username and Secret Path
Expand All @@ -271,14 +283,16 @@ spec:
databaseName: myapp_db
username: custom_user
secretName: /custom/path/myapp-credentials
connectionStringSecretRef:
name: postgres-admin
awsSecretsManager:
region: us-east-1
description: "Custom application database"
tags:
Team: platform
CostCenter: engineering
connectionString:
kubernetes:
name: postgres-admin
secretBackend:
aws:
region: us-east-1
description: "Custom application database"
tags:
Team: platform
CostCenter: engineering
```

### Delete Resources on CR Deletion
Expand All @@ -294,10 +308,12 @@ spec:
engine: postgres
databaseName: temp_db
retainOnDelete: false # Delete database and user when CR is deleted
connectionStringSecretRef:
name: postgres-admin
awsSecretsManager:
region: us-east-1
connectionString:
kubernetes:
name: postgres-admin
secretBackend:
aws:
region: us-east-1
```

For more advanced examples and secret template documentation, see [Secret Templates Guide](docs/SECRET_TEMPLATES.md).
Expand Down
150 changes: 113 additions & 37 deletions api/v1alpha1/database_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,11 @@ type DatabaseSpec struct {
// +kubebuilder:validation:Pattern=`^[a-z][a-z0-9_]*$`
DatabaseName string `json:"databaseName"`

// ConnectionStringSecretRef references a Kubernetes Secret containing the admin connection string
// to the existing database instance. Must have proper permissions to create databases and users.
// Either ConnectionStringSecretRef or ConnectionStringAWSSecretRef must be specified.
// Note: Created database credentials will always be stored in AWS Secrets Manager.
// +optional
ConnectionStringSecretRef *SecretKeyReference `json:"connectionStringSecretRef,omitempty"`

// ConnectionStringAWSSecretRef references an AWS Secrets Manager secret containing the admin connection string
// Either ConnectionStringSecretRef or ConnectionStringAWSSecretRef must be specified.
// Note: Created database credentials will always be stored in AWS Secrets Manager.
// +optional
ConnectionStringAWSSecretRef *AWSSecretReference `json:"connectionStringAWSSecretRef,omitempty"`
// ConnectionString sources the admin DSN used to create the database
// and user. Exactly one of the inner fields (aws, kubernetes, ...)
// must be set.
// +kubebuilder:validation:Required
ConnectionString ConnectionStringSource `json:"connectionString"`

// Username for the database user to be created
// Defaults to the DatabaseName if not specified
Expand All @@ -56,8 +49,10 @@ type DatabaseSpec struct {
// +kubebuilder:validation:Pattern=`^[a-z][a-z0-9_]*$`
Username string `json:"username,omitempty"`

// SecretName is the name/path for storing the created credentials in AWS Secrets Manager
// Defaults to rds/<engine>/<databaseName>
// SecretName is the name/path used by the chosen SecretBackend for the
// generated user credentials. For AWS this is the Secrets Manager
// secret name; for Kubernetes the Secret name; for Infisical the path
// inside the configured environment. Defaults to rds/<engine>/<databaseName>.
// +optional
SecretName string `json:"secretName,omitempty"`

Expand All @@ -72,10 +67,11 @@ type DatabaseSpec struct {
// +kubebuilder:default=true
RetainOnDelete *bool `json:"retainOnDelete,omitempty"`

// AWSSecretsManager contains AWS Secrets Manager specific configuration for storing created credentials
// All created credentials are stored in AWS Secrets Manager regardless of connection string source
// +optional
AWSSecretsManager *AWSSecretsManagerConfig `json:"awsSecretsManager,omitempty"`
// SecretBackend chooses where to store the generated user credentials.
// Exactly one of the inner fields (aws, kubernetes, infisical) must
// be set.
// +kubebuilder:validation:Required
SecretBackend SecretBackend `json:"secretBackend"`

// SecretTemplate is a Go template for customizing the secret structure
// Available variables: .DBHost, .DBPort, .DBName, .DBUsername, .DBPassword, .DatabaseURL, .Engine
Expand All @@ -85,8 +81,26 @@ type DatabaseSpec struct {
SecretTemplate string `json:"secretTemplate,omitempty"`
}

// AWSSecretsManagerConfig contains AWS Secrets Manager specific settings
type AWSSecretsManagerConfig struct {
// SecretBackend selects where the generated user credentials are stored.
// Exactly one inner field must be set; the controller fails reconciliation
// if zero or multiple are configured.
type SecretBackend struct {
// AWS stores credentials in AWS Secrets Manager.
// +optional
AWS *AWSSecretBackend `json:"aws,omitempty"`

// Kubernetes stores credentials as a Kubernetes Secret.
// +optional
Kubernetes *KubernetesSecretBackend `json:"kubernetes,omitempty"`

// Infisical stores credentials in Infisical Cloud (or self-hosted)
// via Universal Auth.
// +optional
Infisical *InfisicalSecretBackend `json:"infisical,omitempty"`
}

// AWSSecretBackend contains AWS Secrets Manager configuration.
type AWSSecretBackend struct {
// Region is the AWS region for Secrets Manager
// +kubebuilder:validation:Required
// +kubebuilder:validation:Enum=us-east-1;us-east-2;us-west-1;us-west-2;us-gov-west-1;us-gov-east-1;af-south-1;ap-east-1;ap-south-1;ap-south-2;ap-northeast-1;ap-northeast-2;ap-northeast-3;ap-southeast-1;ap-southeast-2;ap-southeast-3;ap-southeast-4;ca-central-1;ca-west-1;eu-central-1;eu-central-2;eu-west-1;eu-west-2;eu-west-3;eu-south-1;eu-south-2;eu-north-1;me-south-1;me-central-1;sa-east-1;cn-north-1;cn-northwest-1;il-central-1
Expand All @@ -101,26 +115,69 @@ type AWSSecretsManagerConfig struct {
Tags map[string]string `json:"tags,omitempty"`
}

// SecretKeyReference references a key in a Kubernetes Secret
type SecretKeyReference struct {
// Name of the secret
// KubernetesSecretBackend stores generated credentials in a Kubernetes
// Secret. The Secret is created in `namespace`; the secret name comes from
// spec.secretName (default rds/<engine>/<db>).
type KubernetesSecretBackend struct {
// Namespace the Secret is created in. Defaults to the namespace of
// the Database resource.
// +optional
Namespace string `json:"namespace,omitempty"`
}

// InfisicalSecretBackend stores generated credentials in Infisical
// (Cloud or self-hosted) via Universal Auth.
//
// The Infisical V3 secrets API requires a project UUID for
// create/update/delete operations, so we expose ProjectID rather than
// the slug used elsewhere (e.g. the ESO ClusterSecretStore). Find the
// UUID in the Infisical UI under Project Settings.
type InfisicalSecretBackend struct {
// HostAPI is the Infisical API endpoint. Default: https://app.infisical.com
// +optional
// +kubebuilder:default="https://app.infisical.com"
HostAPI string `json:"hostAPI,omitempty"`

// ProjectID is the Infisical project UUID.
// +kubebuilder:validation:Required
Name string `json:"name"`
ProjectID string `json:"projectID"`

// Environment is the Infisical environment slug (e.g. dev, staging, prod).
// +kubebuilder:validation:Required
Environment string `json:"environment"`

// Key within the secret
// Defaults to "connectionString"
// SecretsPath is the folder path inside the environment. Default: "/"
// +optional
Key string `json:"key,omitempty"`
// +kubebuilder:default="/"
SecretsPath string `json:"secretsPath,omitempty"`

// AuthSecretRef references a Kubernetes Secret in the same namespace
// as the Database holding `clientId` and `clientSecret` keys for
// Infisical Universal Auth.
// +kubebuilder:validation:Required
AuthSecretRef KubernetesSecretRef `json:"authSecretRef"`
}

// ConnectionStringSource selects where the admin DSN is read from.
// Exactly one inner field must be set.
type ConnectionStringSource struct {
// AWS reads the connection string from an AWS Secrets Manager secret.
// +optional
AWS *AWSConnectionStringRef `json:"aws,omitempty"`

// Kubernetes reads the connection string from a Kubernetes Secret in
// the same namespace as the Database.
// +optional
Kubernetes *KubernetesConnectionStringRef `json:"kubernetes,omitempty"`
}

// AWSSecretReference references an AWS Secrets Manager secret
type AWSSecretReference struct {
// AWSConnectionStringRef points at a key in an AWS Secrets Manager secret.
type AWSConnectionStringRef struct {
// SecretName is the name or ARN of the AWS Secrets Manager secret
// +kubebuilder:validation:Required
SecretName string `json:"secretName"`

// Key within the secret JSON
// Defaults to "connectionString"
// Key within the secret JSON. Defaults to "connectionString".
// +optional
Key string `json:"key,omitempty"`

Expand All @@ -130,6 +187,25 @@ type AWSSecretReference struct {
Region string `json:"region"`
}

// KubernetesConnectionStringRef points at a key in a Kubernetes Secret.
type KubernetesConnectionStringRef struct {
// Name of the secret in the same namespace as the Database.
// +kubebuilder:validation:Required
Name string `json:"name"`

// Key within the secret. Defaults to "connectionString".
// +optional
Key string `json:"key,omitempty"`
}

// KubernetesSecretRef is a generic reference to a Kubernetes Secret in
// the same namespace as the Database resource.
type KubernetesSecretRef struct {
// Name of the Secret.
// +kubebuilder:validation:Required
Name string `json:"name"`
}

// DatabaseStatus defines the observed state of Database
type DatabaseStatus struct {
// Conditions represent the latest available observations of the Database's state
Expand All @@ -154,8 +230,11 @@ type DatabaseStatus struct {
// SecretCreated indicates whether the secret has been created
SecretCreated bool `json:"secretCreated,omitempty"`

// SecretARN is the ARN of the created AWS Secrets Manager secret (if applicable)
SecretARN string `json:"secretARN,omitempty"`
// SecretLocator is the backend-specific identifier for the stored
// credential secret. For AWS Secrets Manager, this is the ARN
// (which carries the region). For a Kubernetes Secret it is
// "namespace/name". For Infisical it is the full project/env/path.
SecretLocator string `json:"secretLocator,omitempty"`

// SecretVersion is the version ID of the secret
SecretVersion string `json:"secretVersion,omitempty"`
Expand All @@ -169,9 +248,6 @@ type DatabaseStatus struct {
// ActualSecretName is the actual secret name that was created
ActualSecretName string `json:"actualSecretName,omitempty"`

// SecretRegion is the AWS region where the secret is stored
SecretRegion string `json:"secretRegion,omitempty"`

// ConnectionInfo provides non-sensitive connection information
ConnectionInfo ConnectionInfo `json:"connectionInfo,omitempty"`
}
Expand Down Expand Up @@ -201,7 +277,7 @@ type ConnectionInfo struct {
// +kubebuilder:printcolumn:name="Database",type=string,JSONPath=`.spec.databaseName`
// +kubebuilder:printcolumn:name="Username",type=string,JSONPath=`.status.actualUsername`
// +kubebuilder:printcolumn:name="SecretName",type=string,JSONPath=`.status.actualSecretName`
// +kubebuilder:printcolumn:name="Region",type=string,JSONPath=`.status.secretRegion`
// +kubebuilder:printcolumn:name="Locator",type=string,priority=1,JSONPath=`.status.secretLocator`
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

Expand Down
Loading
Loading