feat: 定义 v1alpha1 API 合同 #2
@@ -21,21 +21,47 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
// PostgreSQL identifier accepted by the v1alpha1 API.
|
||||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
// The same expression is repeated in markers because controller-gen markers
|
||||||
|
// cannot refer to Go constants.
|
||||||
|
const PostgreSQLIdentifierPattern = `^[a-z][a-z0-9_]{0,62}$`
|
||||||
|
|
||||||
// PostgreSQLInstanceSpec defines the desired state of PostgreSQLInstance
|
// PostgreSQLInstancePhase is the controller workflow checkpoint for an instance.
|
||||||
|
// +kubebuilder:validation:Enum=Pending;Validating;InitializingRegistry;Ready;Deleting
|
||||||
|
type PostgreSQLInstancePhase string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PostgreSQLInstancePhasePending PostgreSQLInstancePhase = "Pending"
|
||||||
|
PostgreSQLInstancePhaseValidating PostgreSQLInstancePhase = "Validating"
|
||||||
|
PostgreSQLInstancePhaseInitializingRegistry PostgreSQLInstancePhase = "InitializingRegistry"
|
||||||
|
PostgreSQLInstancePhaseReady PostgreSQLInstancePhase = "Ready"
|
||||||
|
PostgreSQLInstancePhaseDeleting PostgreSQLInstancePhase = "Deleting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostgreSQLSSLMode controls transport security for PostgreSQL connections.
|
||||||
|
// +kubebuilder:validation:Enum=disable;require;verify-ca;verify-full
|
||||||
|
type PostgreSQLSSLMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PostgreSQLSSLModeDisable PostgreSQLSSLMode = "disable"
|
||||||
|
PostgreSQLSSLModeRequire PostgreSQLSSLMode = "require"
|
||||||
|
PostgreSQLSSLModeVerifyCA PostgreSQLSSLMode = "verify-ca"
|
||||||
|
PostgreSQLSSLModeVerifyFull PostgreSQLSSLMode = "verify-full"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostgreSQLInstanceSpec defines an external PostgreSQL server managed by the controller.
|
||||||
type PostgreSQLInstanceSpec struct {
|
type PostgreSQLInstanceSpec struct {
|
||||||
// Endpoint is the PostgreSQL server managed by this instance.
|
// Endpoint identifies the PostgreSQL server and its administrative database.
|
||||||
// +required
|
// +required
|
||||||
Endpoint PostgreSQLEndpoint `json:"endpoint"`
|
Endpoint PostgreSQLEndpoint `json:"endpoint"`
|
||||||
|
|
||||||
// AdminCredentialRef points to an OpenBao KV secret containing the
|
// AdminCredentialRef identifies the OpenBao KV v2 record containing the
|
||||||
// administrative login. Secret values are never copied into this resource.
|
// administrative username and password. Values are never copied into this resource.
|
||||||
// +required
|
// +required
|
||||||
AdminCredentialRef OpenBaoSecretReference `json:"adminCredentialRef"`
|
AdminCredentialRef OpenBaoSecretReference `json:"adminCredentialRef"`
|
||||||
|
|
||||||
// AllowedExtensions is the allowlist tenants may request.
|
// AllowedExtensions is the set of extensions tenants may request.
|
||||||
|
// Removing an item does not remove it from databases where it already exists.
|
||||||
// +listType=set
|
// +listType=set
|
||||||
// +optional
|
// +optional
|
||||||
AllowedExtensions []string `json:"allowedExtensions,omitempty"`
|
AllowedExtensions []string `json:"allowedExtensions,omitempty"`
|
||||||
@@ -43,59 +69,75 @@ type PostgreSQLInstanceSpec struct {
|
|||||||
|
|
||||||
// PostgreSQLEndpoint identifies a PostgreSQL server.
|
// PostgreSQLEndpoint identifies a PostgreSQL server.
|
||||||
type PostgreSQLEndpoint struct {
|
type PostgreSQLEndpoint struct {
|
||||||
|
// Host is the DNS name used as a connection target and TLS server name.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
// +required
|
// +required
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
|
||||||
|
// HostAddr is an IPv4 or IPv6 address covered by the server certificate IP SAN.
|
||||||
|
// +kubebuilder:validation:Format=ip
|
||||||
|
// +required
|
||||||
|
HostAddr string `json:"hostaddr"`
|
||||||
|
|
||||||
|
// Port is the PostgreSQL TCP port.
|
||||||
// +kubebuilder:default=5432
|
// +kubebuilder:default=5432
|
||||||
// +kubebuilder:validation:Minimum=1
|
// +kubebuilder:validation:Minimum=1
|
||||||
// +kubebuilder:validation:Maximum=65535
|
// +kubebuilder:validation:Maximum=65535
|
||||||
// +optional
|
// +optional
|
||||||
Port int32 `json:"port,omitempty"`
|
Port int32 `json:"port,omitempty"`
|
||||||
|
|
||||||
// Database used for administrative connections.
|
// Database is used for administrative connections and the ownership registry.
|
||||||
// +kubebuilder:default=postgres
|
// +kubebuilder:default=postgres
|
||||||
|
// +kubebuilder:validation:Pattern="^[a-z][a-z0-9_]{0,62}$"
|
||||||
// +optional
|
// +optional
|
||||||
Database string `json:"database,omitempty"`
|
Database string `json:"database,omitempty"`
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=disable;require;verify-ca;verify-full
|
// SSLMode controls PostgreSQL TLS verification.
|
||||||
// +kubebuilder:default=verify-full
|
// +kubebuilder:default=verify-full
|
||||||
// +optional
|
// +optional
|
||||||
SSLMode string `json:"sslMode,omitempty"`
|
SSLMode PostgreSQLSSLMode `json:"sslMode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenBaoSecretReference identifies keys in an OpenBao KV secret.
|
// OpenBaoSecretReference identifies fields in a record within the deployment-level KV v2 mount.
|
||||||
type OpenBaoSecretReference struct {
|
type OpenBaoSecretReference struct {
|
||||||
|
// Path is mount-relative and must not include the KV v2 data or metadata API layer.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
// +kubebuilder:validation:MaxLength=512
|
||||||
|
// +kubebuilder:validation:Pattern="^[^/]+(/[^/]+)*$"
|
||||||
|
// +kubebuilder:validation:XValidation:rule="self.split('/').all(segment, segment != '.' && segment != '..')",message="path must not contain . or .. segments"
|
||||||
|
// +kubebuilder:validation:XValidation:rule="self.split('/')[0] != 'data' && self.split('/')[0] != 'metadata'",message="path must not include the KV v2 data or metadata API layer"
|
||||||
// +required
|
// +required
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
|
||||||
|
// UsernameKey is the key containing the administrative username.
|
||||||
// +kubebuilder:default=username
|
// +kubebuilder:default=username
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
// +optional
|
// +optional
|
||||||
UsernameKey string `json:"usernameKey,omitempty"`
|
UsernameKey string `json:"usernameKey,omitempty"`
|
||||||
|
|
||||||
|
// PasswordKey is the key containing the administrative password.
|
||||||
// +kubebuilder:default=password
|
// +kubebuilder:default=password
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
// +optional
|
// +optional
|
||||||
PasswordKey string `json:"passwordKey,omitempty"`
|
PasswordKey string `json:"passwordKey,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgreSQLInstanceStatus defines the observed state of PostgreSQLInstance.
|
// PostgreSQLInstanceStatus defines the observed state of PostgreSQLInstance.
|
||||||
type PostgreSQLInstanceStatus struct {
|
type PostgreSQLInstanceStatus struct {
|
||||||
// ObservedGeneration is the most recent generation observed by the controller.
|
// ObservedGeneration is the most recent generation for which reconciliation reached a conclusion.
|
||||||
// +optional
|
// +optional
|
||||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||||
|
|
||||||
// PostgreSQLVersion is reported by the target server.
|
// Phase is the authoritative checkpoint of the controller workflow.
|
||||||
|
// External state is still read back before and after every operation.
|
||||||
|
// +optional
|
||||||
|
Phase PostgreSQLInstancePhase `json:"phase,omitempty"`
|
||||||
|
|
||||||
|
// PostgreSQLVersion is reported by the target server for diagnostics.
|
||||||
// +optional
|
// +optional
|
||||||
PostgreSQLVersion string `json:"postgresqlVersion,omitempty"`
|
PostgreSQLVersion string `json:"postgresqlVersion,omitempty"`
|
||||||
|
|
||||||
// conditions represent the current state of the PostgreSQLInstance resource.
|
// Conditions contains the current Ready condition and any future auxiliary conditions.
|
||||||
// Each condition has a unique type and reflects the status of a specific aspect of the resource.
|
|
||||||
//
|
|
||||||
// Standard condition types include:
|
|
||||||
// - "Available": the resource is fully functional
|
|
||||||
// - "Progressing": the resource is being created or updated
|
|
||||||
// - "Degraded": the resource failed to reach or maintain its desired state
|
|
||||||
//
|
|
||||||
// The status of each condition is one of True, False, or Unknown.
|
|
||||||
// +listType=map
|
// +listType=map
|
||||||
// +listMapKey=type
|
// +listMapKey=type
|
||||||
// +optional
|
// +optional
|
||||||
@@ -106,29 +148,25 @@ type PostgreSQLInstanceStatus struct {
|
|||||||
// +kubebuilder:subresource:status
|
// +kubebuilder:subresource:status
|
||||||
// +kubebuilder:resource:scope=Cluster,shortName=pginstance
|
// +kubebuilder:resource:scope=Cluster,shortName=pginstance
|
||||||
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint.host`
|
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint.host`
|
||||||
|
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
|
||||||
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
|
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
|
||||||
// +kubebuilder:resource:scope=Cluster
|
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||||
|
|
||||||
// PostgreSQLInstance is the Schema for the postgresqlinstances API
|
// PostgreSQLInstance is the Schema for the postgresqlinstances API.
|
||||||
type PostgreSQLInstance struct {
|
type PostgreSQLInstance struct {
|
||||||
metav1.TypeMeta `json:",inline"`
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
|
||||||
// metadata is a standard object metadata
|
|
||||||
// +optional
|
|
||||||
metav1.ObjectMeta `json:"metadata,omitzero"`
|
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||||
|
|
||||||
// spec defines the desired state of PostgreSQLInstance
|
|
||||||
// +required
|
// +required
|
||||||
Spec PostgreSQLInstanceSpec `json:"spec"`
|
Spec PostgreSQLInstanceSpec `json:"spec"`
|
||||||
|
|
||||||
// status defines the observed state of PostgreSQLInstance
|
|
||||||
// +optional
|
// +optional
|
||||||
Status PostgreSQLInstanceStatus `json:"status,omitzero"`
|
Status PostgreSQLInstanceStatus `json:"status,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
|
||||||
// PostgreSQLInstanceList contains a list of PostgreSQLInstance
|
// PostgreSQLInstanceList contains a list of PostgreSQLInstance.
|
||||||
type PostgreSQLInstanceList struct {
|
type PostgreSQLInstanceList struct {
|
||||||
metav1.TypeMeta `json:",inline"`
|
metav1.TypeMeta `json:",inline"`
|
||||||
metav1.ListMeta `json:"metadata,omitzero"`
|
metav1.ListMeta `json:"metadata,omitzero"`
|
||||||
|
|||||||
@@ -21,102 +21,187 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
const credentialResourceSuffix = "-postgresql"
|
||||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
|
||||||
|
|
||||||
// PostgreSQLTenantSpec defines the desired state of PostgreSQLTenant
|
// PostgreSQLTenantPhase is the authoritative controller workflow checkpoint for a tenant.
|
||||||
|
// +kubebuilder:validation:Enum=Pending;Planned;CredentialCreated;RoleCreated;DatabaseCreated;ExternalSecretCreated;CredentialProjected;Ready;Deleting
|
||||||
|
type PostgreSQLTenantPhase string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PostgreSQLTenantPhasePending PostgreSQLTenantPhase = "Pending"
|
||||||
|
PostgreSQLTenantPhasePlanned PostgreSQLTenantPhase = "Planned"
|
||||||
|
PostgreSQLTenantPhaseCredentialCreated PostgreSQLTenantPhase = "CredentialCreated"
|
||||||
|
PostgreSQLTenantPhaseRoleCreated PostgreSQLTenantPhase = "RoleCreated"
|
||||||
|
PostgreSQLTenantPhaseDatabaseCreated PostgreSQLTenantPhase = "DatabaseCreated"
|
||||||
|
PostgreSQLTenantPhaseExternalSecretCreated PostgreSQLTenantPhase = "ExternalSecretCreated"
|
||||||
|
PostgreSQLTenantPhaseCredentialProjected PostgreSQLTenantPhase = "CredentialProjected"
|
||||||
|
PostgreSQLTenantPhaseReady PostgreSQLTenantPhase = "Ready"
|
||||||
|
PostgreSQLTenantPhaseDeleting PostgreSQLTenantPhase = "Deleting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeletionPolicy controls whether deleting a Tenant retains or destroys managed external resources.
|
||||||
|
// +kubebuilder:validation:Enum=Retain;Delete
|
||||||
|
type DeletionPolicy string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DeletionPolicyRetain DeletionPolicy = "Retain"
|
||||||
|
DeletionPolicyDelete DeletionPolicy = "Delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostgreSQLTenantSpec defines one application database and its login owner.
|
||||||
type PostgreSQLTenantSpec struct {
|
type PostgreSQLTenantSpec struct {
|
||||||
// InstanceRef names the cluster-scoped PostgreSQLInstance to use.
|
// InstanceRef names the cluster-scoped PostgreSQLInstance to use.
|
||||||
|
// +kubebuilder:validation:MinLength=1
|
||||||
|
// +kubebuilder:validation:MaxLength=253
|
||||||
|
// +kubebuilder:validation:Pattern="^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$"
|
||||||
// +required
|
// +required
|
||||||
InstanceRef string `json:"instanceRef"`
|
InstanceRef string `json:"instanceRef"`
|
||||||
|
|
||||||
// Database defaults to metadata.name when omitted.
|
// Database is the database to create. It semantically defaults to metadata.name.
|
||||||
|
// +kubebuilder:validation:Pattern="^[a-z][a-z0-9_]{0,62}$"
|
||||||
// +optional
|
// +optional
|
||||||
Database string `json:"database,omitempty"`
|
Database string `json:"database,omitempty"`
|
||||||
|
|
||||||
// OwnerRole defaults to <database>_owner when omitted.
|
// LoginRole is both the database owner and application login.
|
||||||
// +optional
|
// It semantically defaults to metadata.name.
|
||||||
OwnerRole string `json:"ownerRole,omitempty"`
|
// +kubebuilder:validation:Pattern="^[a-z][a-z0-9_]{0,62}$"
|
||||||
|
|
||||||
// LoginRole defaults to metadata.name when omitted.
|
|
||||||
// +optional
|
// +optional
|
||||||
LoginRole string `json:"loginRole,omitempty"`
|
LoginRole string `json:"loginRole,omitempty"`
|
||||||
|
|
||||||
// Extensions to install from the instance allowlist.
|
// Extensions is the set to install from the referenced Instance allowlist.
|
||||||
|
// Once provisioned, this set may only grow.
|
||||||
// +listType=set
|
// +listType=set
|
||||||
// +optional
|
// +optional
|
||||||
Extensions []string `json:"extensions,omitempty"`
|
Extensions []string `json:"extensions,omitempty"`
|
||||||
|
|
||||||
// Credential configures the application login credential.
|
// Credential configures projection of the application credential.
|
||||||
// +required
|
// +optional
|
||||||
Credential PostgreSQLCredentialSpec `json:"credential"`
|
Credential PostgreSQLCredentialSpec `json:"credential,omitempty"`
|
||||||
|
|
||||||
// DeletionPolicy controls whether deleting this object removes the database.
|
// DeletionPolicy controls cleanup when this object is deleted.
|
||||||
// +kubebuilder:validation:Enum=Retain;Delete
|
|
||||||
// +kubebuilder:default=Retain
|
// +kubebuilder:default=Retain
|
||||||
// +optional
|
// +optional
|
||||||
DeletionPolicy string `json:"deletionPolicy,omitempty"`
|
DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgreSQLCredentialSpec describes where credentials live and when to rotate them.
|
// PostgreSQLCredentialSpec configures the ESO target Secret. The OpenBao path is not tenant-configurable.
|
||||||
type PostgreSQLCredentialSpec struct {
|
type PostgreSQLCredentialSpec struct {
|
||||||
// OpenBaoPath is the KV path receiving the generated login credential.
|
// SecretName is the target Kubernetes Secret in the Tenant namespace.
|
||||||
// +required
|
// It semantically defaults to <instanceRef>-<metadata.name>-postgresql.
|
||||||
OpenBaoPath string `json:"openBaoPath"`
|
// +kubebuilder:validation:MaxLength=253
|
||||||
|
// +kubebuilder:validation:Pattern="^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$"
|
||||||
|
// +optional
|
||||||
|
SecretName string `json:"secretName,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgreSQLTenantStatus defines the observed state of PostgreSQLTenant.
|
// PostgreSQLTenantStatus defines the observed state of PostgreSQLTenant.
|
||||||
type PostgreSQLTenantStatus struct {
|
type PostgreSQLTenantStatus struct {
|
||||||
// ObservedGeneration is the most recent generation observed by the controller.
|
// ObservedGeneration is the most recent generation for which reconciliation reached a conclusion.
|
||||||
// +optional
|
// +optional
|
||||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||||
|
|
||||||
// DatabaseOID is the server-side identity observed for the database.
|
// Phase is the authoritative checkpoint of the controller workflow.
|
||||||
|
// External state is still read back before and after every operation.
|
||||||
|
// +optional
|
||||||
|
Phase PostgreSQLTenantPhase `json:"phase,omitempty"`
|
||||||
|
|
||||||
|
// Database is the effective database name after applying semantic defaults.
|
||||||
|
// +optional
|
||||||
|
Database string `json:"database,omitempty"`
|
||||||
|
|
||||||
|
// LoginRole is the effective owner/login role after applying semantic defaults.
|
||||||
|
// +optional
|
||||||
|
LoginRole string `json:"loginRole,omitempty"`
|
||||||
|
|
||||||
|
// DatabaseOID is the observed PostgreSQL object identifier for diagnostics.
|
||||||
// +optional
|
// +optional
|
||||||
DatabaseOID uint32 `json:"databaseOID,omitempty"`
|
DatabaseOID uint32 `json:"databaseOID,omitempty"`
|
||||||
|
|
||||||
// conditions represent the current state of the PostgreSQLTenant resource.
|
// Credential identifies the projected Secret and the non-authenticated OpenBao API URL.
|
||||||
// Each condition has a unique type and reflects the status of a specific aspect of the resource.
|
// +optional
|
||||||
//
|
Credential PostgreSQLCredentialStatus `json:"credential,omitempty"`
|
||||||
// Standard condition types include:
|
|
||||||
// - "Available": the resource is fully functional
|
// Conditions contains the current Ready condition and any future auxiliary conditions.
|
||||||
// - "Progressing": the resource is being created or updated
|
|
||||||
// - "Degraded": the resource failed to reach or maintain its desired state
|
|
||||||
//
|
|
||||||
// The status of each condition is one of True, False, or Unknown.
|
|
||||||
// +listType=map
|
// +listType=map
|
||||||
// +listMapKey=type
|
// +listMapKey=type
|
||||||
// +optional
|
// +optional
|
||||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostgreSQLCredentialStatus exposes credential locations, never credential values.
|
||||||
|
type PostgreSQLCredentialStatus struct {
|
||||||
|
// SecretRef identifies the target Secret in the Tenant namespace.
|
||||||
|
// +optional
|
||||||
|
SecretRef LocalSecretReference `json:"secretRef,omitempty"`
|
||||||
|
|
||||||
|
// OpenBaoURL is the complete KV v2 data API URL for non-Kubernetes consumers.
|
||||||
|
// It contains no token or credential value.
|
||||||
|
// +optional
|
||||||
|
OpenBaoURL string `json:"openBaoURL,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalSecretReference identifies a Secret in the namespace of the referring Tenant.
|
||||||
|
type LocalSecretReference struct {
|
||||||
|
// Name is the Secret name.
|
||||||
|
// +optional
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EffectiveDatabase returns the configured database or its semantic default.
|
||||||
|
func (t *PostgreSQLTenant) EffectiveDatabase() string {
|
||||||
|
if t.Spec.Database != "" {
|
||||||
|
return t.Spec.Database
|
||||||
|
}
|
||||||
|
return t.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// EffectiveLoginRole returns the configured login role or its semantic default.
|
||||||
|
func (t *PostgreSQLTenant) EffectiveLoginRole() string {
|
||||||
|
if t.Spec.LoginRole != "" {
|
||||||
|
return t.Spec.LoginRole
|
||||||
|
}
|
||||||
|
return t.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalSecretName returns the deterministic name of the controller-managed ExternalSecret.
|
||||||
|
func (t *PostgreSQLTenant) ExternalSecretName() string {
|
||||||
|
return t.Spec.InstanceRef + "-" + t.Name + credentialResourceSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
// EffectiveSecretName returns the configured target Secret or its semantic default.
|
||||||
|
func (t *PostgreSQLTenant) EffectiveSecretName() string {
|
||||||
|
if t.Spec.Credential.SecretName != "" {
|
||||||
|
return t.Spec.Credential.SecretName
|
||||||
|
}
|
||||||
|
return t.ExternalSecretName()
|
||||||
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
// +kubebuilder:subresource:status
|
// +kubebuilder:subresource:status
|
||||||
// +kubebuilder:resource:scope=Namespaced,shortName=pgtenant
|
// +kubebuilder:resource:scope=Namespaced,shortName=pgtenant
|
||||||
|
// +kubebuilder:validation:XValidation:rule="size(self.spec.instanceRef) + size(self.metadata.name) <= 241",message="instanceRef and metadata.name are too long to derive the ExternalSecret name"
|
||||||
// +kubebuilder:printcolumn:name="Instance",type=string,JSONPath=`.spec.instanceRef`
|
// +kubebuilder:printcolumn:name="Instance",type=string,JSONPath=`.spec.instanceRef`
|
||||||
// +kubebuilder:printcolumn:name="Database",type=string,JSONPath=`.spec.database`
|
// +kubebuilder:printcolumn:name="Database",type=string,JSONPath=`.status.database`
|
||||||
|
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
|
||||||
|
// +kubebuilder:printcolumn:name="Secret",type=string,JSONPath=`.status.credential.secretRef.name`
|
||||||
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
|
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
|
||||||
|
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||||
|
|
||||||
// PostgreSQLTenant is the Schema for the postgresqltenants API
|
// PostgreSQLTenant is the Schema for the postgresqltenants API.
|
||||||
type PostgreSQLTenant struct {
|
type PostgreSQLTenant struct {
|
||||||
metav1.TypeMeta `json:",inline"`
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
|
||||||
// metadata is a standard object metadata
|
|
||||||
// +optional
|
|
||||||
metav1.ObjectMeta `json:"metadata,omitzero"`
|
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||||
|
|
||||||
// spec defines the desired state of PostgreSQLTenant
|
|
||||||
// +required
|
// +required
|
||||||
Spec PostgreSQLTenantSpec `json:"spec"`
|
Spec PostgreSQLTenantSpec `json:"spec"`
|
||||||
|
|
||||||
// status defines the observed state of PostgreSQLTenant
|
|
||||||
// +optional
|
// +optional
|
||||||
Status PostgreSQLTenantStatus `json:"status,omitzero"`
|
Status PostgreSQLTenantStatus `json:"status,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
|
||||||
// PostgreSQLTenantList contains a list of PostgreSQLTenant
|
// PostgreSQLTenantList contains a list of PostgreSQLTenant.
|
||||||
type PostgreSQLTenantList struct {
|
type PostgreSQLTenantList struct {
|
||||||
metav1.TypeMeta `json:",inline"`
|
metav1.TypeMeta `json:",inline"`
|
||||||
metav1.ListMeta `json:"metadata,omitzero"`
|
metav1.ListMeta `json:"metadata,omitzero"`
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostgreSQLTenantSemanticDefaults(t *testing.T) {
|
||||||
|
const tenantName = "netbox"
|
||||||
|
|
||||||
|
tenant := &PostgreSQLTenant{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: tenantName, Namespace: tenantName},
|
||||||
|
Spec: PostgreSQLTenantSpec{
|
||||||
|
InstanceRef: "shared",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := tenant.EffectiveDatabase(); got != tenantName {
|
||||||
|
t.Fatalf("EffectiveDatabase() = %q, want netbox", got)
|
||||||
|
}
|
||||||
|
if got := tenant.EffectiveLoginRole(); got != tenantName {
|
||||||
|
t.Fatalf("EffectiveLoginRole() = %q, want netbox", got)
|
||||||
|
}
|
||||||
|
if got := tenant.ExternalSecretName(); got != "shared-netbox-postgresql" {
|
||||||
|
t.Fatalf("ExternalSecretName() = %q, want shared-netbox-postgresql", got)
|
||||||
|
}
|
||||||
|
if got := tenant.EffectiveSecretName(); got != "shared-netbox-postgresql" {
|
||||||
|
t.Fatalf("EffectiveSecretName() = %q, want shared-netbox-postgresql", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostgreSQLTenantExplicitNames(t *testing.T) {
|
||||||
|
const tenantName = "netbox"
|
||||||
|
|
||||||
|
tenant := &PostgreSQLTenant{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: tenantName, Namespace: tenantName},
|
||||||
|
Spec: PostgreSQLTenantSpec{
|
||||||
|
InstanceRef: "shared",
|
||||||
|
Database: "netbox_db",
|
||||||
|
LoginRole: "netbox_app",
|
||||||
|
Credential: PostgreSQLCredentialSpec{
|
||||||
|
SecretName: "database-credentials",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := tenant.EffectiveDatabase(); got != "netbox_db" {
|
||||||
|
t.Fatalf("EffectiveDatabase() = %q, want netbox_db", got)
|
||||||
|
}
|
||||||
|
if got := tenant.EffectiveLoginRole(); got != "netbox_app" {
|
||||||
|
t.Fatalf("EffectiveLoginRole() = %q, want netbox_app", got)
|
||||||
|
}
|
||||||
|
if got := tenant.EffectiveSecretName(); got != "database-credentials" {
|
||||||
|
t.Fatalf("EffectiveSecretName() = %q, want database-credentials", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,21 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalSecretReference.
|
||||||
|
func (in *LocalSecretReference) DeepCopy() *LocalSecretReference {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(LocalSecretReference)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *OpenBaoSecretReference) DeepCopyInto(out *OpenBaoSecretReference) {
|
func (in *OpenBaoSecretReference) DeepCopyInto(out *OpenBaoSecretReference) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@@ -55,6 +70,22 @@ func (in *PostgreSQLCredentialSpec) DeepCopy() *PostgreSQLCredentialSpec {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *PostgreSQLCredentialStatus) DeepCopyInto(out *PostgreSQLCredentialStatus) {
|
||||||
|
*out = *in
|
||||||
|
out.SecretRef = in.SecretRef
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLCredentialStatus.
|
||||||
|
func (in *PostgreSQLCredentialStatus) DeepCopy() *PostgreSQLCredentialStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(PostgreSQLCredentialStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *PostgreSQLEndpoint) DeepCopyInto(out *PostgreSQLEndpoint) {
|
func (in *PostgreSQLEndpoint) DeepCopyInto(out *PostgreSQLEndpoint) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@@ -256,6 +287,7 @@ func (in *PostgreSQLTenantSpec) DeepCopy() *PostgreSQLTenantSpec {
|
|||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *PostgreSQLTenantStatus) DeepCopyInto(out *PostgreSQLTenantStatus) {
|
func (in *PostgreSQLTenantStatus) DeepCopyInto(out *PostgreSQLTenantStatus) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
out.Credential = in.Credential
|
||||||
if in.Conditions != nil {
|
if in.Conditions != nil {
|
||||||
in, out := &in.Conditions, &out.Conditions
|
in, out := &in.Conditions, &out.Conditions
|
||||||
*out = make([]v1.Condition, len(*in))
|
*out = make([]v1.Condition, len(*in))
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ spec:
|
|||||||
kind: PostgreSQLInstance
|
kind: PostgreSQLInstance
|
||||||
listKind: PostgreSQLInstanceList
|
listKind: PostgreSQLInstanceList
|
||||||
plural: postgresqlinstances
|
plural: postgresqlinstances
|
||||||
|
shortNames:
|
||||||
|
- pginstance
|
||||||
singular: postgresqlinstance
|
singular: postgresqlinstance
|
||||||
scope: Cluster
|
scope: Cluster
|
||||||
versions:
|
versions:
|
||||||
@@ -18,14 +20,20 @@ spec:
|
|||||||
- jsonPath: .spec.endpoint.host
|
- jsonPath: .spec.endpoint.host
|
||||||
name: Endpoint
|
name: Endpoint
|
||||||
type: string
|
type: string
|
||||||
|
- jsonPath: .status.phase
|
||||||
|
name: Phase
|
||||||
|
type: string
|
||||||
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||||
name: Ready
|
name: Ready
|
||||||
type: string
|
type: string
|
||||||
|
- jsonPath: .metadata.creationTimestamp
|
||||||
|
name: Age
|
||||||
|
type: date
|
||||||
name: v1alpha1
|
name: v1alpha1
|
||||||
schema:
|
schema:
|
||||||
openAPIV3Schema:
|
openAPIV3Schema:
|
||||||
description: PostgreSQLInstance is the Schema for the postgresqlinstances
|
description: PostgreSQLInstance is the Schema for the postgresqlinstances
|
||||||
API
|
API.
|
||||||
properties:
|
properties:
|
||||||
apiVersion:
|
apiVersion:
|
||||||
description: |-
|
description: |-
|
||||||
@@ -45,47 +53,82 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
type: object
|
type: object
|
||||||
spec:
|
spec:
|
||||||
description: spec defines the desired state of PostgreSQLInstance
|
description: PostgreSQLInstanceSpec defines an external PostgreSQL server
|
||||||
|
managed by the controller.
|
||||||
properties:
|
properties:
|
||||||
adminCredentialRef:
|
adminCredentialRef:
|
||||||
description: |-
|
description: |-
|
||||||
AdminCredentialRef points to an OpenBao KV secret containing the
|
AdminCredentialRef identifies the OpenBao KV v2 record containing the
|
||||||
administrative login. Secret values are never copied into this resource.
|
administrative username and password. Values are never copied into this resource.
|
||||||
properties:
|
properties:
|
||||||
passwordKey:
|
passwordKey:
|
||||||
default: password
|
default: password
|
||||||
|
description: PasswordKey is the key containing the administrative
|
||||||
|
password.
|
||||||
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
path:
|
path:
|
||||||
|
description: Path is mount-relative and must not include the KV
|
||||||
|
v2 data or metadata API layer.
|
||||||
|
maxLength: 512
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[^/]+(/[^/]+)*$
|
||||||
type: string
|
type: string
|
||||||
|
x-kubernetes-validations:
|
||||||
|
- message: path must not contain . or .. segments
|
||||||
|
rule: self.split('/').all(segment, segment != '.' && segment
|
||||||
|
!= '..')
|
||||||
|
- message: path must not include the KV v2 data or metadata API
|
||||||
|
layer
|
||||||
|
rule: self.split('/')[0] != 'data' && self.split('/')[0] !=
|
||||||
|
'metadata'
|
||||||
usernameKey:
|
usernameKey:
|
||||||
default: username
|
default: username
|
||||||
|
description: UsernameKey is the key containing the administrative
|
||||||
|
username.
|
||||||
|
minLength: 1
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- path
|
- path
|
||||||
type: object
|
type: object
|
||||||
allowedExtensions:
|
allowedExtensions:
|
||||||
description: AllowedExtensions is the allowlist tenants may request.
|
description: |-
|
||||||
|
AllowedExtensions is the set of extensions tenants may request.
|
||||||
|
Removing an item does not remove it from databases where it already exists.
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
x-kubernetes-list-type: set
|
x-kubernetes-list-type: set
|
||||||
endpoint:
|
endpoint:
|
||||||
description: Endpoint is the PostgreSQL server managed by this instance.
|
description: Endpoint identifies the PostgreSQL server and its administrative
|
||||||
|
database.
|
||||||
properties:
|
properties:
|
||||||
database:
|
database:
|
||||||
default: postgres
|
default: postgres
|
||||||
description: Database used for administrative connections.
|
description: Database is used for administrative connections and
|
||||||
|
the ownership registry.
|
||||||
|
pattern: ^[a-z][a-z0-9_]{0,62}$
|
||||||
type: string
|
type: string
|
||||||
host:
|
host:
|
||||||
|
description: Host is the DNS name used as a connection target
|
||||||
|
and TLS server name.
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
hostaddr:
|
||||||
|
description: HostAddr is an IPv4 or IPv6 address covered by the
|
||||||
|
server certificate IP SAN.
|
||||||
|
format: ip
|
||||||
type: string
|
type: string
|
||||||
port:
|
port:
|
||||||
default: 5432
|
default: 5432
|
||||||
|
description: Port is the PostgreSQL TCP port.
|
||||||
format: int32
|
format: int32
|
||||||
maximum: 65535
|
maximum: 65535
|
||||||
minimum: 1
|
minimum: 1
|
||||||
type: integer
|
type: integer
|
||||||
sslMode:
|
sslMode:
|
||||||
default: verify-full
|
default: verify-full
|
||||||
|
description: SSLMode controls PostgreSQL TLS verification.
|
||||||
enum:
|
enum:
|
||||||
- disable
|
- disable
|
||||||
- require
|
- require
|
||||||
@@ -94,25 +137,18 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- host
|
- host
|
||||||
|
- hostaddr
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- adminCredentialRef
|
- adminCredentialRef
|
||||||
- endpoint
|
- endpoint
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
description: status defines the observed state of PostgreSQLInstance
|
description: PostgreSQLInstanceStatus defines the observed state of PostgreSQLInstance.
|
||||||
properties:
|
properties:
|
||||||
conditions:
|
conditions:
|
||||||
description: |-
|
description: Conditions contains the current Ready condition and any
|
||||||
conditions represent the current state of the PostgreSQLInstance resource.
|
future auxiliary conditions.
|
||||||
Each condition has a unique type and reflects the status of a specific aspect of the resource.
|
|
||||||
|
|
||||||
Standard condition types include:
|
|
||||||
- "Available": the resource is fully functional
|
|
||||||
- "Progressing": the resource is being created or updated
|
|
||||||
- "Degraded": the resource failed to reach or maintain its desired state
|
|
||||||
|
|
||||||
The status of each condition is one of True, False, or Unknown.
|
|
||||||
items:
|
items:
|
||||||
description: Condition contains details for one aspect of the current
|
description: Condition contains details for one aspect of the current
|
||||||
state of this API Resource.
|
state of this API Resource.
|
||||||
@@ -172,15 +208,28 @@ spec:
|
|||||||
- type
|
- type
|
||||||
x-kubernetes-list-type: map
|
x-kubernetes-list-type: map
|
||||||
observedGeneration:
|
observedGeneration:
|
||||||
description: ObservedGeneration is the most recent generation observed
|
description: ObservedGeneration is the most recent generation for
|
||||||
by the controller.
|
which reconciliation reached a conclusion.
|
||||||
format: int64
|
format: int64
|
||||||
type: integer
|
type: integer
|
||||||
|
phase:
|
||||||
|
description: |-
|
||||||
|
Phase is the authoritative checkpoint of the controller workflow.
|
||||||
|
External state is still read back before and after every operation.
|
||||||
|
enum:
|
||||||
|
- Pending
|
||||||
|
- Validating
|
||||||
|
- InitializingRegistry
|
||||||
|
- Ready
|
||||||
|
- Deleting
|
||||||
|
type: string
|
||||||
postgresqlVersion:
|
postgresqlVersion:
|
||||||
description: PostgreSQLVersion is reported by the target server.
|
description: PostgreSQLVersion is reported by the target server for
|
||||||
|
diagnostics.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
- metadata
|
||||||
- spec
|
- spec
|
||||||
type: object
|
type: object
|
||||||
served: true
|
served: true
|
||||||
|
|||||||
@@ -20,16 +20,25 @@ spec:
|
|||||||
- jsonPath: .spec.instanceRef
|
- jsonPath: .spec.instanceRef
|
||||||
name: Instance
|
name: Instance
|
||||||
type: string
|
type: string
|
||||||
- jsonPath: .spec.database
|
- jsonPath: .status.database
|
||||||
name: Database
|
name: Database
|
||||||
type: string
|
type: string
|
||||||
|
- jsonPath: .status.phase
|
||||||
|
name: Phase
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.credential.secretRef.name
|
||||||
|
name: Secret
|
||||||
|
type: string
|
||||||
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||||
name: Ready
|
name: Ready
|
||||||
type: string
|
type: string
|
||||||
|
- jsonPath: .metadata.creationTimestamp
|
||||||
|
name: Age
|
||||||
|
type: date
|
||||||
name: v1alpha1
|
name: v1alpha1
|
||||||
schema:
|
schema:
|
||||||
openAPIV3Schema:
|
openAPIV3Schema:
|
||||||
description: PostgreSQLTenant is the Schema for the postgresqltenants API
|
description: PostgreSQLTenant is the Schema for the postgresqltenants API.
|
||||||
properties:
|
properties:
|
||||||
apiVersion:
|
apiVersion:
|
||||||
description: |-
|
description: |-
|
||||||
@@ -49,31 +58,36 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
type: object
|
type: object
|
||||||
spec:
|
spec:
|
||||||
description: spec defines the desired state of PostgreSQLTenant
|
description: PostgreSQLTenantSpec defines one application database and
|
||||||
|
its login owner.
|
||||||
properties:
|
properties:
|
||||||
credential:
|
credential:
|
||||||
description: Credential configures the application login credential.
|
description: Credential configures projection of the application credential.
|
||||||
properties:
|
properties:
|
||||||
openBaoPath:
|
secretName:
|
||||||
description: OpenBaoPath is the KV path receiving the generated
|
description: |-
|
||||||
login credential.
|
SecretName is the target Kubernetes Secret in the Tenant namespace.
|
||||||
|
It semantically defaults to <instanceRef>-<metadata.name>-postgresql.
|
||||||
|
maxLength: 253
|
||||||
|
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- openBaoPath
|
|
||||||
type: object
|
type: object
|
||||||
database:
|
database:
|
||||||
description: Database defaults to metadata.name when omitted.
|
description: Database is the database to create. It semantically defaults
|
||||||
|
to metadata.name.
|
||||||
|
pattern: ^[a-z][a-z0-9_]{0,62}$
|
||||||
type: string
|
type: string
|
||||||
deletionPolicy:
|
deletionPolicy:
|
||||||
default: Retain
|
default: Retain
|
||||||
description: DeletionPolicy controls whether deleting this object
|
description: DeletionPolicy controls cleanup when this object is deleted.
|
||||||
removes the database.
|
|
||||||
enum:
|
enum:
|
||||||
- Retain
|
- Retain
|
||||||
- Delete
|
- Delete
|
||||||
type: string
|
type: string
|
||||||
extensions:
|
extensions:
|
||||||
description: Extensions to install from the instance allowlist.
|
description: |-
|
||||||
|
Extensions is the set to install from the referenced Instance allowlist.
|
||||||
|
Once provisioned, this set may only grow.
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
@@ -81,31 +95,25 @@ spec:
|
|||||||
instanceRef:
|
instanceRef:
|
||||||
description: InstanceRef names the cluster-scoped PostgreSQLInstance
|
description: InstanceRef names the cluster-scoped PostgreSQLInstance
|
||||||
to use.
|
to use.
|
||||||
|
maxLength: 253
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
|
||||||
type: string
|
type: string
|
||||||
loginRole:
|
loginRole:
|
||||||
description: LoginRole defaults to metadata.name when omitted.
|
description: |-
|
||||||
type: string
|
LoginRole is both the database owner and application login.
|
||||||
ownerRole:
|
It semantically defaults to metadata.name.
|
||||||
description: OwnerRole defaults to <database>_owner when omitted.
|
pattern: ^[a-z][a-z0-9_]{0,62}$
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- credential
|
|
||||||
- instanceRef
|
- instanceRef
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
description: status defines the observed state of PostgreSQLTenant
|
description: PostgreSQLTenantStatus defines the observed state of PostgreSQLTenant.
|
||||||
properties:
|
properties:
|
||||||
conditions:
|
conditions:
|
||||||
description: |-
|
description: Conditions contains the current Ready condition and any
|
||||||
conditions represent the current state of the PostgreSQLTenant resource.
|
future auxiliary conditions.
|
||||||
Each condition has a unique type and reflects the status of a specific aspect of the resource.
|
|
||||||
|
|
||||||
Standard condition types include:
|
|
||||||
- "Available": the resource is fully functional
|
|
||||||
- "Progressing": the resource is being created or updated
|
|
||||||
- "Degraded": the resource failed to reach or maintain its desired state
|
|
||||||
|
|
||||||
The status of each condition is one of True, False, or Unknown.
|
|
||||||
items:
|
items:
|
||||||
description: Condition contains details for one aspect of the current
|
description: Condition contains details for one aspect of the current
|
||||||
state of this API Resource.
|
state of this API Resource.
|
||||||
@@ -164,20 +172,66 @@ spec:
|
|||||||
x-kubernetes-list-map-keys:
|
x-kubernetes-list-map-keys:
|
||||||
- type
|
- type
|
||||||
x-kubernetes-list-type: map
|
x-kubernetes-list-type: map
|
||||||
|
credential:
|
||||||
|
description: Credential identifies the projected Secret and the non-authenticated
|
||||||
|
OpenBao API URL.
|
||||||
|
properties:
|
||||||
|
openBaoURL:
|
||||||
|
description: |-
|
||||||
|
OpenBaoURL is the complete KV v2 data API URL for non-Kubernetes consumers.
|
||||||
|
It contains no token or credential value.
|
||||||
|
type: string
|
||||||
|
secretRef:
|
||||||
|
description: SecretRef identifies the target Secret in the Tenant
|
||||||
|
namespace.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Name is the Secret name.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
database:
|
||||||
|
description: Database is the effective database name after applying
|
||||||
|
semantic defaults.
|
||||||
|
type: string
|
||||||
databaseOID:
|
databaseOID:
|
||||||
description: DatabaseOID is the server-side identity observed for
|
description: DatabaseOID is the observed PostgreSQL object identifier
|
||||||
the database.
|
for diagnostics.
|
||||||
format: int32
|
format: int32
|
||||||
type: integer
|
type: integer
|
||||||
|
loginRole:
|
||||||
|
description: LoginRole is the effective owner/login role after applying
|
||||||
|
semantic defaults.
|
||||||
|
type: string
|
||||||
observedGeneration:
|
observedGeneration:
|
||||||
description: ObservedGeneration is the most recent generation observed
|
description: ObservedGeneration is the most recent generation for
|
||||||
by the controller.
|
which reconciliation reached a conclusion.
|
||||||
format: int64
|
format: int64
|
||||||
type: integer
|
type: integer
|
||||||
|
phase:
|
||||||
|
description: |-
|
||||||
|
Phase is the authoritative checkpoint of the controller workflow.
|
||||||
|
External state is still read back before and after every operation.
|
||||||
|
enum:
|
||||||
|
- Pending
|
||||||
|
- Planned
|
||||||
|
- CredentialCreated
|
||||||
|
- RoleCreated
|
||||||
|
- DatabaseCreated
|
||||||
|
- ExternalSecretCreated
|
||||||
|
- CredentialProjected
|
||||||
|
- Ready
|
||||||
|
- Deleting
|
||||||
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
- metadata
|
||||||
- spec
|
- spec
|
||||||
type: object
|
type: object
|
||||||
|
x-kubernetes-validations:
|
||||||
|
- message: instanceRef and metadata.name are too long to derive the ExternalSecret
|
||||||
|
name
|
||||||
|
rule: size(self.spec.instanceRef) + size(self.metadata.name) <= 241
|
||||||
served: true
|
served: true
|
||||||
storage: true
|
storage: true
|
||||||
subresources:
|
subresources:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
endpoint:
|
endpoint:
|
||||||
host: postgres.internal
|
host: postgres.internal
|
||||||
|
hostaddr: 192.0.2.10
|
||||||
port: 5432
|
port: 5432
|
||||||
database: postgres
|
database: postgres
|
||||||
sslMode: verify-full
|
sslMode: verify-full
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
instanceRef: shared
|
instanceRef: shared
|
||||||
database: netbox
|
database: netbox
|
||||||
ownerRole: netbox_owner
|
|
||||||
loginRole: netbox
|
loginRole: netbox
|
||||||
extensions:
|
extensions:
|
||||||
- pg_trgm
|
- pg_trgm
|
||||||
credential:
|
credential:
|
||||||
openBaoPath: kv/k8s/netbox/database
|
secretName: shared-netbox-database-credentials
|
||||||
deletionPolicy: Retain
|
deletionPolicy: Retain
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
- Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不超过 241 个字符,确保派生的
|
- Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不超过 241 个字符,确保派生的
|
||||||
`<instanceRef>-<metadata.name>-postgresql` 不超过 Kubernetes DNS subdomain 的
|
`<instanceRef>-<metadata.name>-postgresql` 不超过 Kubernetes DNS subdomain 的
|
||||||
253 字符限制。
|
253 字符限制。
|
||||||
- 默认值由 CRD defaulting 提供;需要读取旧值的校验由 CEL 或 webhook 完成。
|
- port、TLS mode、deletion policy 等固定默认值由 CRD defaulting 提供。database、
|
||||||
|
loginRole、Secret 名称等依赖其他字段的值是 controller 语义默认值:字段保持省略,
|
||||||
|
controller 计算 effective value 并通过 status/受管资源展示,不引入 mutating webhook。
|
||||||
|
- 需要读取旧值或跨字段的校验由 CEL 或 controller 完成。
|
||||||
- `status` 由 controller 独占写入,禁止出现密码、Token、管理用户名或完整连接串。
|
- `status` 由 controller 独占写入,禁止出现密码、Token、管理用户名或完整连接串。
|
||||||
- 两个 Kind 都只承诺一个 `Ready` Condition;调用方不得依赖内部协调阶段。
|
- 两个 Kind 都只承诺一个 `Ready` Condition;调用方不得依赖内部协调阶段。
|
||||||
|
|
||||||
@@ -91,6 +94,8 @@ Tenant 不声明 OpenBao mount 或 path。controller 使用部署级 mount/base
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `status.observedGeneration` | int64 | 最近完成有结论协调的 generation |
|
| `status.observedGeneration` | int64 | 最近完成有结论协调的 generation |
|
||||||
| `status.phase` | enum | controller 状态机的权威 checkpoint |
|
| `status.phase` | enum | controller 状态机的权威 checkpoint |
|
||||||
|
| `status.database` | string | 应用语义默认值后的实际 database 名称 |
|
||||||
|
| `status.loginRole` | string | 应用语义默认值后的实际 owner/login role 名称 |
|
||||||
| `status.databaseOID` | uint32 | 回读的 database OID,仅供诊断 |
|
| `status.databaseOID` | uint32 | 回读的 database OID,仅供诊断 |
|
||||||
| `status.credential.secretRef.name` | string | 同 namespace 目标 Secret 名称 |
|
| `status.credential.secretRef.name` | string | 同 namespace 目标 Secret 名称 |
|
||||||
| `status.credential.openBaoURL` | string | 完整 KV v2 API URL,不含认证信息 |
|
| `status.credential.openBaoURL` | string | 完整 KV v2 API URL,不含认证信息 |
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不得超过 241
|
|||||||
派生的 ExternalSecret/Secret 默认名称
|
派生的 ExternalSecret/Secret 默认名称
|
||||||
`<instanceRef>-<metadata.name>-postgresql` 不超过 Kubernetes 253 字符限制。
|
`<instanceRef>-<metadata.name>-postgresql` 不超过 Kubernetes 253 字符限制。
|
||||||
|
|
||||||
|
固定默认值由 CRD defaulting 写入。依赖 `metadata.name` 或 `instanceRef` 的 database、
|
||||||
|
login role、ExternalSecret/Secret 名称属于 controller 语义默认值:省略字段不会被 admission
|
||||||
|
回写,controller 必须始终计算同一个 effective value,并通过 status 的 database、
|
||||||
|
loginRole、credential reference 以及实际资源展示。
|
||||||
|
v1alpha1 不为此引入 mutating webhook。
|
||||||
|
|
||||||
database 和 role 名称必须作为 PostgreSQL identifier 参数安全引用,禁止通过字符串
|
database 和 role 名称必须作为 PostgreSQL identifier 参数安全引用,禁止通过字符串
|
||||||
拼接执行。名称校验必须拒绝空字符串、NUL 和超过 PostgreSQL identifier 长度限制的
|
拼接执行。名称校验必须拒绝空字符串、NUL 和超过 PostgreSQL identifier 长度限制的
|
||||||
值,并统一限制为小写字母、数字和下划线。
|
值,并统一限制为小写字母、数字和下划线。
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
databasev1alpha1 "git.ddupan.top/panxiao81/postgresql-tenant-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testInstanceName = "shared"
|
||||||
|
testNamespace = "default"
|
||||||
|
testPostgreSQLHost = "postgres.example.test"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("v1alpha1 API contract", func() {
|
||||||
|
It("applies fixed Instance defaults through the API server", func() {
|
||||||
|
instance := &databasev1alpha1.PostgreSQLInstance{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "defaults"},
|
||||||
|
Spec: databasev1alpha1.PostgreSQLInstanceSpec{
|
||||||
|
Endpoint: databasev1alpha1.PostgreSQLEndpoint{
|
||||||
|
Host: testPostgreSQLHost,
|
||||||
|
HostAddr: "192.0.2.20",
|
||||||
|
},
|
||||||
|
AdminCredentialRef: databasev1alpha1.OpenBaoSecretReference{
|
||||||
|
Path: "infrastructure/postgresql/admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Expect(k8sClient.Create(ctx, instance)).To(Succeed())
|
||||||
|
DeferCleanup(func() { Expect(k8sClient.Delete(ctx, instance)).To(Succeed()) })
|
||||||
|
Expect(instance.Spec.Endpoint.Port).To(Equal(int32(5432)))
|
||||||
|
Expect(instance.Spec.Endpoint.Database).To(Equal("postgres"))
|
||||||
|
Expect(instance.Spec.Endpoint.SSLMode).To(Equal(databasev1alpha1.PostgreSQLSSLModeVerifyFull))
|
||||||
|
Expect(instance.Spec.AdminCredentialRef.UsernameKey).To(Equal("username"))
|
||||||
|
Expect(instance.Spec.AdminCredentialRef.PasswordKey).To(Equal("password"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("rejects invalid PostgreSQL identifiers and host addresses", func() {
|
||||||
|
instance := &databasev1alpha1.PostgreSQLInstance{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "invalid-endpoint"},
|
||||||
|
Spec: databasev1alpha1.PostgreSQLInstanceSpec{
|
||||||
|
Endpoint: databasev1alpha1.PostgreSQLEndpoint{
|
||||||
|
Host: testPostgreSQLHost,
|
||||||
|
HostAddr: "not-an-ip",
|
||||||
|
Database: "Invalid-Database",
|
||||||
|
},
|
||||||
|
AdminCredentialRef: databasev1alpha1.OpenBaoSecretReference{Path: "admin"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, instance)
|
||||||
|
Expect(apierrors.IsInvalid(err)).To(BeTrue(), "expected invalid error, got %v", err)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("rejects unsafe OpenBao paths", func() {
|
||||||
|
for index, path := range []string{"data/postgresql/admin", "postgresql/../admin", "/postgresql/admin"} {
|
||||||
|
instance := &databasev1alpha1.PostgreSQLInstance{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("invalid-path-%d", index)},
|
||||||
|
Spec: databasev1alpha1.PostgreSQLInstanceSpec{
|
||||||
|
Endpoint: databasev1alpha1.PostgreSQLEndpoint{
|
||||||
|
Host: testPostgreSQLHost,
|
||||||
|
HostAddr: "192.0.2.21",
|
||||||
|
},
|
||||||
|
AdminCredentialRef: databasev1alpha1.OpenBaoSecretReference{Path: path},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, instance)
|
||||||
|
Expect(apierrors.IsInvalid(err)).To(BeTrue(), "path %q: expected invalid error, got %v", path, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("applies fixed Tenant defaults while preserving semantic defaults", func() {
|
||||||
|
tenant := &databasev1alpha1.PostgreSQLTenant{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "api-defaults", Namespace: testNamespace},
|
||||||
|
Spec: databasev1alpha1.PostgreSQLTenantSpec{
|
||||||
|
InstanceRef: testInstanceName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Expect(k8sClient.Create(ctx, tenant)).To(Succeed())
|
||||||
|
DeferCleanup(func() { Expect(k8sClient.Delete(ctx, tenant)).To(Succeed()) })
|
||||||
|
Expect(tenant.Spec.DeletionPolicy).To(Equal(databasev1alpha1.DeletionPolicyRetain))
|
||||||
|
Expect(tenant.Spec.Database).To(BeEmpty())
|
||||||
|
Expect(tenant.Spec.LoginRole).To(BeEmpty())
|
||||||
|
Expect(tenant.Spec.Credential.SecretName).To(BeEmpty())
|
||||||
|
Expect(tenant.EffectiveDatabase()).To(Equal("api-defaults"))
|
||||||
|
Expect(tenant.EffectiveLoginRole()).To(Equal("api-defaults"))
|
||||||
|
Expect(tenant.EffectiveSecretName()).To(Equal("shared-api-defaults-postgresql"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("accepts a custom target Secret name", func() {
|
||||||
|
tenant := &databasev1alpha1.PostgreSQLTenant{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "custom-secret", Namespace: testNamespace},
|
||||||
|
Spec: databasev1alpha1.PostgreSQLTenantSpec{
|
||||||
|
InstanceRef: testInstanceName,
|
||||||
|
Credential: databasev1alpha1.PostgreSQLCredentialSpec{
|
||||||
|
SecretName: "database-credentials",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Expect(k8sClient.Create(ctx, tenant)).To(Succeed())
|
||||||
|
DeferCleanup(func() { Expect(k8sClient.Delete(ctx, tenant)).To(Succeed()) })
|
||||||
|
Expect(tenant.EffectiveSecretName()).To(Equal("database-credentials"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("rejects names that cannot produce a valid ExternalSecret name", func() {
|
||||||
|
tenant := &databasev1alpha1.PostgreSQLTenant{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: strings.Repeat("t", 121), Namespace: testNamespace},
|
||||||
|
Spec: databasev1alpha1.PostgreSQLTenantSpec{
|
||||||
|
InstanceRef: strings.Repeat("i", 121),
|
||||||
|
Database: "valid_database",
|
||||||
|
LoginRole: "valid_role",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, tenant)
|
||||||
|
Expect(apierrors.IsInvalid(err)).To(BeTrue(), "expected invalid error, got %v", err)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -33,15 +33,13 @@ import (
|
|||||||
var _ = Describe("PostgreSQLInstance Controller", func() {
|
var _ = Describe("PostgreSQLInstance Controller", func() {
|
||||||
Context("When reconciling a resource", func() {
|
Context("When reconciling a resource", func() {
|
||||||
const (
|
const (
|
||||||
resourceName = "test-resource"
|
resourceName = "test-resource"
|
||||||
resourceNamespace = "default"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
typeNamespacedName := types.NamespacedName{
|
typeNamespacedName := types.NamespacedName{
|
||||||
Name: resourceName,
|
Name: resourceName,
|
||||||
Namespace: resourceNamespace,
|
|
||||||
}
|
}
|
||||||
postgresqlinstance := &databasev1alpha1.PostgreSQLInstance{}
|
postgresqlinstance := &databasev1alpha1.PostgreSQLInstance{}
|
||||||
|
|
||||||
@@ -51,10 +49,17 @@ var _ = Describe("PostgreSQLInstance Controller", func() {
|
|||||||
if err != nil && errors.IsNotFound(err) {
|
if err != nil && errors.IsNotFound(err) {
|
||||||
resource := &databasev1alpha1.PostgreSQLInstance{
|
resource := &databasev1alpha1.PostgreSQLInstance{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: resourceName,
|
Name: resourceName,
|
||||||
Namespace: resourceNamespace,
|
},
|
||||||
|
Spec: databasev1alpha1.PostgreSQLInstanceSpec{
|
||||||
|
Endpoint: databasev1alpha1.PostgreSQLEndpoint{
|
||||||
|
Host: testPostgreSQLHost,
|
||||||
|
HostAddr: "192.0.2.10",
|
||||||
|
},
|
||||||
|
AdminCredentialRef: databasev1alpha1.OpenBaoSecretReference{
|
||||||
|
Path: "infrastructure/postgresql/admin",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// TODO(user): Specify other spec details if needed.
|
|
||||||
}
|
}
|
||||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ var _ = Describe("PostgreSQLTenant Controller", func() {
|
|||||||
Context("When reconciling a resource", func() {
|
Context("When reconciling a resource", func() {
|
||||||
const (
|
const (
|
||||||
resourceName = "test-resource"
|
resourceName = "test-resource"
|
||||||
resourceNamespace = "default"
|
resourceNamespace = testNamespace
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -54,7 +54,9 @@ var _ = Describe("PostgreSQLTenant Controller", func() {
|
|||||||
Name: resourceName,
|
Name: resourceName,
|
||||||
Namespace: resourceNamespace,
|
Namespace: resourceNamespace,
|
||||||
},
|
},
|
||||||
// TODO(user): Specify other spec details if needed.
|
Spec: databasev1alpha1.PostgreSQLTenantSpec{
|
||||||
|
InstanceRef: testInstanceName,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user