feat: 定义 v1alpha1 API 合同 #2
@@ -105,9 +105,15 @@ make generate # Regenerate DeepCopy methods
|
||||
**After editing `*.go` files:**
|
||||
```
|
||||
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
|
||||
|
||||
### Create API (your own types)
|
||||
@@ -182,7 +188,8 @@ kubebuilder create webhook \
|
||||
## Testing & Development
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
|
||||
+5
-2
@@ -43,11 +43,14 @@ kubeconfig 或本地生成的二进制。
|
||||
5. 提交前运行:
|
||||
|
||||
```sh
|
||||
make test
|
||||
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
|
||||
可以包含多个这样的 commit。
|
||||
7. 推送分支后,在创建 PR 前再次请求批准。PR 说明应包含规格链接、动机、行为变化、
|
||||
|
||||
@@ -21,21 +21,47 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||
// PostgreSQL identifier accepted by the v1alpha1 API.
|
||||
// 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 {
|
||||
// Endpoint is the PostgreSQL server managed by this instance.
|
||||
// Endpoint identifies the PostgreSQL server and its administrative database.
|
||||
// +required
|
||||
Endpoint PostgreSQLEndpoint `json:"endpoint"`
|
||||
|
||||
// AdminCredentialRef points to an OpenBao KV secret containing the
|
||||
// administrative login. Secret values are never copied into this resource.
|
||||
// AdminCredentialRef identifies the OpenBao KV v2 record containing the
|
||||
// administrative username and password. Values are never copied into this resource.
|
||||
// +required
|
||||
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
|
||||
// +optional
|
||||
AllowedExtensions []string `json:"allowedExtensions,omitempty"`
|
||||
@@ -43,59 +69,75 @@ type PostgreSQLInstanceSpec struct {
|
||||
|
||||
// PostgreSQLEndpoint identifies a PostgreSQL server.
|
||||
type PostgreSQLEndpoint struct {
|
||||
// Host is the DNS name used as a connection target and TLS server name.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +required
|
||||
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:validation:Minimum=1
|
||||
// +kubebuilder:validation:Maximum=65535
|
||||
// +optional
|
||||
Port int32 `json:"port,omitempty"`
|
||||
|
||||
// Database used for administrative connections.
|
||||
// Database is used for administrative connections and the ownership registry.
|
||||
// +kubebuilder:default=postgres
|
||||
// +kubebuilder:validation:Pattern="^[a-z][a-z0-9_]{0,62}$"
|
||||
// +optional
|
||||
Database string `json:"database,omitempty"`
|
||||
|
||||
// +kubebuilder:validation:Enum=disable;require;verify-ca;verify-full
|
||||
// SSLMode controls PostgreSQL TLS verification.
|
||||
// +kubebuilder:default=verify-full
|
||||
// +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 {
|
||||
// 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
|
||||
Path string `json:"path"`
|
||||
|
||||
// UsernameKey is the key containing the administrative username.
|
||||
// +kubebuilder:default=username
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +optional
|
||||
UsernameKey string `json:"usernameKey,omitempty"`
|
||||
|
||||
// PasswordKey is the key containing the administrative password.
|
||||
// +kubebuilder:default=password
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +optional
|
||||
PasswordKey string `json:"passwordKey,omitempty"`
|
||||
}
|
||||
|
||||
// PostgreSQLInstanceStatus defines the observed state of PostgreSQLInstance.
|
||||
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
|
||||
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
|
||||
PostgreSQLVersion string `json:"postgresqlVersion,omitempty"`
|
||||
|
||||
// conditions represent the current state of the PostgreSQLInstance resource.
|
||||
// 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.
|
||||
// Conditions contains the current Ready condition and any future auxiliary conditions.
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
// +optional
|
||||
@@ -106,29 +148,25 @@ type PostgreSQLInstanceStatus struct {
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=pginstance
|
||||
// +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: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 {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// metadata is a standard object metadata
|
||||
// +optional
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||
|
||||
// spec defines the desired state of PostgreSQLInstance
|
||||
// +required
|
||||
Spec PostgreSQLInstanceSpec `json:"spec"`
|
||||
|
||||
// status defines the observed state of PostgreSQLInstance
|
||||
// +optional
|
||||
Status PostgreSQLInstanceStatus `json:"status,omitzero"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PostgreSQLInstanceList contains a list of PostgreSQLInstance
|
||||
// PostgreSQLInstanceList contains a list of PostgreSQLInstance.
|
||||
type PostgreSQLInstanceList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitzero"`
|
||||
|
||||
@@ -21,102 +21,187 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||
const credentialResourceSuffix = "-postgresql"
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
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
|
||||
Database string `json:"database,omitempty"`
|
||||
|
||||
// OwnerRole defaults to <database>_owner when omitted.
|
||||
// +optional
|
||||
OwnerRole string `json:"ownerRole,omitempty"`
|
||||
|
||||
// LoginRole defaults to metadata.name when omitted.
|
||||
// LoginRole is both the database owner and application login.
|
||||
// It semantically defaults to metadata.name.
|
||||
// +kubebuilder:validation:Pattern="^[a-z][a-z0-9_]{0,62}$"
|
||||
// +optional
|
||||
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
|
||||
// +optional
|
||||
Extensions []string `json:"extensions,omitempty"`
|
||||
|
||||
// Credential configures the application login credential.
|
||||
// +required
|
||||
Credential PostgreSQLCredentialSpec `json:"credential"`
|
||||
// Credential configures projection of the application credential.
|
||||
// +optional
|
||||
Credential PostgreSQLCredentialSpec `json:"credential,omitempty"`
|
||||
|
||||
// DeletionPolicy controls whether deleting this object removes the database.
|
||||
// +kubebuilder:validation:Enum=Retain;Delete
|
||||
// DeletionPolicy controls cleanup when this object is deleted.
|
||||
// +kubebuilder:default=Retain
|
||||
// +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 {
|
||||
// OpenBaoPath is the KV path receiving the generated login credential.
|
||||
// +required
|
||||
OpenBaoPath string `json:"openBaoPath"`
|
||||
// SecretName is the target Kubernetes Secret in the Tenant namespace.
|
||||
// It semantically defaults to <instanceRef>-<metadata.name>-postgresql.
|
||||
// +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.
|
||||
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
|
||||
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
|
||||
DatabaseOID uint32 `json:"databaseOID,omitempty"`
|
||||
|
||||
// conditions represent the current state of the PostgreSQLTenant resource.
|
||||
// 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.
|
||||
// Credential identifies the projected Secret and the non-authenticated OpenBao API URL.
|
||||
// +optional
|
||||
Credential PostgreSQLCredentialStatus `json:"credential,omitempty"`
|
||||
|
||||
// Conditions contains the current Ready condition and any future auxiliary conditions.
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
// +optional
|
||||
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:subresource:status
|
||||
// +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="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="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 {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// metadata is a standard object metadata
|
||||
// +optional
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||
|
||||
// spec defines the desired state of PostgreSQLTenant
|
||||
// +required
|
||||
Spec PostgreSQLTenantSpec `json:"spec"`
|
||||
|
||||
// status defines the observed state of PostgreSQLTenant
|
||||
// +optional
|
||||
Status PostgreSQLTenantStatus `json:"status,omitzero"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PostgreSQLTenantList contains a list of PostgreSQLTenant
|
||||
// PostgreSQLTenantList contains a list of PostgreSQLTenant.
|
||||
type PostgreSQLTenantList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
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"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func (in *OpenBaoSecretReference) DeepCopyInto(out *OpenBaoSecretReference) {
|
||||
*out = *in
|
||||
@@ -55,6 +70,22 @@ func (in *PostgreSQLCredentialSpec) DeepCopy() *PostgreSQLCredentialSpec {
|
||||
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.
|
||||
func (in *PostgreSQLEndpoint) DeepCopyInto(out *PostgreSQLEndpoint) {
|
||||
*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.
|
||||
func (in *PostgreSQLTenantStatus) DeepCopyInto(out *PostgreSQLTenantStatus) {
|
||||
*out = *in
|
||||
out.Credential = in.Credential
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
|
||||
@@ -11,6 +11,8 @@ spec:
|
||||
kind: PostgreSQLInstance
|
||||
listKind: PostgreSQLInstanceList
|
||||
plural: postgresqlinstances
|
||||
shortNames:
|
||||
- pginstance
|
||||
singular: postgresqlinstance
|
||||
scope: Cluster
|
||||
versions:
|
||||
@@ -18,14 +20,20 @@ spec:
|
||||
- jsonPath: .spec.endpoint.host
|
||||
name: Endpoint
|
||||
type: string
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
name: Ready
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: PostgreSQLInstance is the Schema for the postgresqlinstances
|
||||
API
|
||||
API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
@@ -45,47 +53,82 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: spec defines the desired state of PostgreSQLInstance
|
||||
description: PostgreSQLInstanceSpec defines an external PostgreSQL server
|
||||
managed by the controller.
|
||||
properties:
|
||||
adminCredentialRef:
|
||||
description: |-
|
||||
AdminCredentialRef points to an OpenBao KV secret containing the
|
||||
administrative login. Secret values are never copied into this resource.
|
||||
AdminCredentialRef identifies the OpenBao KV v2 record containing the
|
||||
administrative username and password. Values are never copied into this resource.
|
||||
properties:
|
||||
passwordKey:
|
||||
default: password
|
||||
description: PasswordKey is the key containing the administrative
|
||||
password.
|
||||
minLength: 1
|
||||
type: string
|
||||
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
|
||||
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:
|
||||
default: username
|
||||
description: UsernameKey is the key containing the administrative
|
||||
username.
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
type: object
|
||||
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:
|
||||
type: string
|
||||
type: array
|
||||
x-kubernetes-list-type: set
|
||||
endpoint:
|
||||
description: Endpoint is the PostgreSQL server managed by this instance.
|
||||
description: Endpoint identifies the PostgreSQL server and its administrative
|
||||
database.
|
||||
properties:
|
||||
database:
|
||||
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
|
||||
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
|
||||
port:
|
||||
default: 5432
|
||||
description: Port is the PostgreSQL TCP port.
|
||||
format: int32
|
||||
maximum: 65535
|
||||
minimum: 1
|
||||
type: integer
|
||||
sslMode:
|
||||
default: verify-full
|
||||
description: SSLMode controls PostgreSQL TLS verification.
|
||||
enum:
|
||||
- disable
|
||||
- require
|
||||
@@ -94,25 +137,18 @@ spec:
|
||||
type: string
|
||||
required:
|
||||
- host
|
||||
- hostaddr
|
||||
type: object
|
||||
required:
|
||||
- adminCredentialRef
|
||||
- endpoint
|
||||
type: object
|
||||
status:
|
||||
description: status defines the observed state of PostgreSQLInstance
|
||||
description: PostgreSQLInstanceStatus defines the observed state of PostgreSQLInstance.
|
||||
properties:
|
||||
conditions:
|
||||
description: |-
|
||||
conditions represent the current state of the PostgreSQLInstance resource.
|
||||
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.
|
||||
description: Conditions contains the current Ready condition and any
|
||||
future auxiliary conditions.
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
@@ -172,15 +208,28 @@ spec:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the most recent generation observed
|
||||
by the controller.
|
||||
description: ObservedGeneration is the most recent generation for
|
||||
which reconciliation reached a conclusion.
|
||||
format: int64
|
||||
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:
|
||||
description: PostgreSQLVersion is reported by the target server.
|
||||
description: PostgreSQLVersion is reported by the target server for
|
||||
diagnostics.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- metadata
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
|
||||
@@ -20,16 +20,25 @@ spec:
|
||||
- jsonPath: .spec.instanceRef
|
||||
name: Instance
|
||||
type: string
|
||||
- jsonPath: .spec.database
|
||||
- jsonPath: .status.database
|
||||
name: Database
|
||||
type: string
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .status.credential.secretRef.name
|
||||
name: Secret
|
||||
type: string
|
||||
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
name: Ready
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: PostgreSQLTenant is the Schema for the postgresqltenants API
|
||||
description: PostgreSQLTenant is the Schema for the postgresqltenants API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
@@ -49,31 +58,36 @@ spec:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: spec defines the desired state of PostgreSQLTenant
|
||||
description: PostgreSQLTenantSpec defines one application database and
|
||||
its login owner.
|
||||
properties:
|
||||
credential:
|
||||
description: Credential configures the application login credential.
|
||||
description: Credential configures projection of the application credential.
|
||||
properties:
|
||||
openBaoPath:
|
||||
description: OpenBaoPath is the KV path receiving the generated
|
||||
login credential.
|
||||
secretName:
|
||||
description: |-
|
||||
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
|
||||
required:
|
||||
- openBaoPath
|
||||
type: object
|
||||
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
|
||||
deletionPolicy:
|
||||
default: Retain
|
||||
description: DeletionPolicy controls whether deleting this object
|
||||
removes the database.
|
||||
description: DeletionPolicy controls cleanup when this object is deleted.
|
||||
enum:
|
||||
- Retain
|
||||
- Delete
|
||||
type: string
|
||||
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:
|
||||
type: string
|
||||
type: array
|
||||
@@ -81,31 +95,25 @@ spec:
|
||||
instanceRef:
|
||||
description: InstanceRef names the cluster-scoped PostgreSQLInstance
|
||||
to use.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
|
||||
type: string
|
||||
loginRole:
|
||||
description: LoginRole defaults to metadata.name when omitted.
|
||||
type: string
|
||||
ownerRole:
|
||||
description: OwnerRole defaults to <database>_owner when omitted.
|
||||
description: |-
|
||||
LoginRole is both the database owner and application login.
|
||||
It semantically defaults to metadata.name.
|
||||
pattern: ^[a-z][a-z0-9_]{0,62}$
|
||||
type: string
|
||||
required:
|
||||
- credential
|
||||
- instanceRef
|
||||
type: object
|
||||
status:
|
||||
description: status defines the observed state of PostgreSQLTenant
|
||||
description: PostgreSQLTenantStatus defines the observed state of PostgreSQLTenant.
|
||||
properties:
|
||||
conditions:
|
||||
description: |-
|
||||
conditions represent the current state of the PostgreSQLTenant resource.
|
||||
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.
|
||||
description: Conditions contains the current Ready condition and any
|
||||
future auxiliary conditions.
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
@@ -164,20 +172,66 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
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:
|
||||
description: DatabaseOID is the server-side identity observed for
|
||||
the database.
|
||||
description: DatabaseOID is the observed PostgreSQL object identifier
|
||||
for diagnostics.
|
||||
format: int32
|
||||
type: integer
|
||||
loginRole:
|
||||
description: LoginRole is the effective owner/login role after applying
|
||||
semantic defaults.
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the most recent generation observed
|
||||
by the controller.
|
||||
description: ObservedGeneration is the most recent generation for
|
||||
which reconciliation reached a conclusion.
|
||||
format: int64
|
||||
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
|
||||
required:
|
||||
- metadata
|
||||
- spec
|
||||
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
|
||||
storage: true
|
||||
subresources:
|
||||
|
||||
@@ -8,6 +8,7 @@ metadata:
|
||||
spec:
|
||||
endpoint:
|
||||
host: postgres.internal
|
||||
hostaddr: 192.0.2.10
|
||||
port: 5432
|
||||
database: postgres
|
||||
sslMode: verify-full
|
||||
|
||||
@@ -9,10 +9,9 @@ metadata:
|
||||
spec:
|
||||
instanceRef: shared
|
||||
database: netbox
|
||||
ownerRole: netbox_owner
|
||||
loginRole: netbox
|
||||
extensions:
|
||||
- pg_trgm
|
||||
credential:
|
||||
openBaoPath: kv/k8s/netbox/database
|
||||
secretName: shared-netbox-database-credentials
|
||||
deletionPolicy: Retain
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
- Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不超过 241 个字符,确保派生的
|
||||
`<instanceRef>-<metadata.name>-postgresql` 不超过 Kubernetes DNS subdomain 的
|
||||
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、管理用户名或完整连接串。
|
||||
- 两个 Kind 都只承诺一个 `Ready` Condition;调用方不得依赖内部协调阶段。
|
||||
|
||||
@@ -91,6 +94,8 @@ Tenant 不声明 OpenBao mount 或 path。controller 使用部署级 mount/base
|
||||
| --- | --- | --- |
|
||||
| `status.observedGeneration` | int64 | 最近完成有结论协调的 generation |
|
||||
| `status.phase` | enum | controller 状态机的权威 checkpoint |
|
||||
| `status.database` | string | 应用语义默认值后的实际 database 名称 |
|
||||
| `status.loginRole` | string | 应用语义默认值后的实际 owner/login role 名称 |
|
||||
| `status.databaseOID` | uint32 | 回读的 database OID,仅供诊断 |
|
||||
| `status.credential.secretRef.name` | string | 同 namespace 目标 Secret 名称 |
|
||||
| `status.credential.openBaoURL` | string | 完整 KV v2 API URL,不含认证信息 |
|
||||
|
||||
@@ -131,6 +131,12 @@ Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不得超过 241
|
||||
派生的 ExternalSecret/Secret 默认名称
|
||||
`<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 参数安全引用,禁止通过字符串
|
||||
拼接执行。名称校验必须拒绝空字符串、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() {
|
||||
Context("When reconciling a resource", func() {
|
||||
const (
|
||||
resourceName = "test-resource"
|
||||
resourceNamespace = "default"
|
||||
resourceName = "test-resource"
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
typeNamespacedName := types.NamespacedName{
|
||||
Name: resourceName,
|
||||
Namespace: resourceNamespace,
|
||||
Name: resourceName,
|
||||
}
|
||||
postgresqlinstance := &databasev1alpha1.PostgreSQLInstance{}
|
||||
|
||||
@@ -51,10 +49,17 @@ var _ = Describe("PostgreSQLInstance Controller", func() {
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
resource := &databasev1alpha1.PostgreSQLInstance{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resourceName,
|
||||
Namespace: resourceNamespace,
|
||||
Name: resourceName,
|
||||
},
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ var _ = Describe("PostgreSQLTenant Controller", func() {
|
||||
Context("When reconciling a resource", func() {
|
||||
const (
|
||||
resourceName = "test-resource"
|
||||
resourceNamespace = "default"
|
||||
resourceNamespace = testNamespace
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -54,7 +54,9 @@ var _ = Describe("PostgreSQLTenant Controller", func() {
|
||||
Name: resourceName,
|
||||
Namespace: resourceNamespace,
|
||||
},
|
||||
// TODO(user): Specify other spec details if needed.
|
||||
Spec: databasev1alpha1.PostgreSQLTenantSpec{
|
||||
InstanceRef: testInstanceName,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user