feat: 定义 v1alpha1 API 合同 #2

Merged
panxiao81 merged 2 commits from feature/v1alpha1-api into main 2026-09-10 08:12:44 +00:00
15 changed files with 644 additions and 141 deletions
+9 -2
View File
@@ -105,9 +105,15 @@ make generate # Regenerate DeepCopy methods
**After editing `*.go` files:** **After editing `*.go` files:**
``` ```
make lint-fix # Auto-fix code style make lint-fix # Auto-fix code style
make test # Run unit tests make lint # Local verification; full tests run in PR CI
``` ```
Do not routinely run the full `make test` or Kind E2E suite during local agent
iteration. Keep local feedback fast with generation checks and `make lint`; after the
human approves opening the PR, rely on the configured self-hosted Gitea Actions jobs for
the complete test suite. Run a focused local test only when it is needed to diagnose a
specific failure or the user explicitly requests it.
## CLI Commands Cheat Sheet ## CLI Commands Cheat Sheet
### Create API (your own types) ### Create API (your own types)
@@ -182,7 +188,8 @@ kubebuilder create webhook \
## Testing & Development ## Testing & Development
```bash ```bash
make test # Run unit tests (uses envtest: real K8s API + etcd) make lint # Default local verification
make test # Full unit/envtest suite; normally run by PR CI
make run # Run locally (uses current kubeconfig context) make run # Run locally (uses current kubeconfig context)
``` ```
+5 -2
View File
@@ -43,11 +43,14 @@ kubeconfig 或本地生成的二进制。
5. 提交前运行: 5. 提交前运行:
```sh ```sh
make test
make lint make lint
git diff --exit-code git diff --check
``` ```
Agent 日常本地迭代不运行耗时的完整 `make test` 或 Kind E2E;生成一致性、Tests 和 E2E
由获准创建 PR 后的 self-hosted Gitea Actions 执行。只有排查特定失败或人工明确要求时
才运行针对性的本地测试。
6. 达到一个小而完整、可独立 review 的边界时,先请求批准再创建 commit;一个 PR 6. 达到一个小而完整、可独立 review 的边界时,先请求批准再创建 commit;一个 PR
可以包含多个这样的 commit。 可以包含多个这样的 commit。
7. 推送分支后,在创建 PR 前再次请求批准。PR 说明应包含规格链接、动机、行为变化、 7. 推送分支后,在创建 PR 前再次请求批准。PR 说明应包含规格链接、动机、行为变化、
+68 -30
View File
@@ -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"`
+124 -39
View File
@@ -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)
}
}
+32
View File
@@ -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
+6 -1
View File
@@ -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,不含认证信息 |
+6
View File
@@ -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 长度限制的
值,并统一限制为小写字母、数字和下划线。 值,并统一限制为小写字母、数字和下划线。
+144
View File
@@ -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)
})
})
@@ -34,14 +34,12 @@ 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{}
@@ -52,9 +50,16 @@ var _ = Describe("PostgreSQLInstance Controller", func() {
resource := &databasev1alpha1.PostgreSQLInstance{ resource := &databasev1alpha1.PostgreSQLInstance{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: resourceName, Name: resourceName,
Namespace: resourceNamespace,
}, },
// TODO(user): Specify other spec details if needed. Spec: databasev1alpha1.PostgreSQLInstanceSpec{
Endpoint: databasev1alpha1.PostgreSQLEndpoint{
Host: testPostgreSQLHost,
HostAddr: "192.0.2.10",
},
AdminCredentialRef: databasev1alpha1.OpenBaoSecretReference{
Path: "infrastructure/postgresql/admin",
},
},
} }
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())
} }