feat: 定义 v1alpha1 API 合同
E2E Tests / Run on Ubuntu (pull_request) Failing after 1m6s
Lint / Run on Ubuntu (pull_request) Successful in 7m7s
Tests / Run on Ubuntu (pull_request) Successful in 4m42s

This commit is contained in:
2026-09-10 06:25:34 +00:00
parent b443d78d14
commit 1f5aa3bd2f
13 changed files with 630 additions and 137 deletions
+69 -31
View File
@@ -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"`
+125 -40
View File
@@ -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)
}
}
+32
View File
@@ -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))