From b443d78d142dc6f21c8a6cdf458597921f4ff45b Mon Sep 17 00:00:00 2001 From: panxiao81 Date: Thu, 10 Sep 2026 06:25:08 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=E8=B0=83=E6=95=B4=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E4=B8=8E=20CI=20=E9=AA=8C=E8=AF=81=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 11 +++++++++-- CONTRIBUTING.md | 7 +++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4ad5892..398b5e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28e84be..e77bff7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 说明应包含规格链接、动机、行为变化、 -- 2.54.0 From 1f5aa3bd2f202c8a87a5e45120655fbc37151aa2 Mon Sep 17 00:00:00 2001 From: panxiao81 Date: Thu, 10 Sep 2026 06:25:34 +0000 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=AE=9A=E4=B9=89=20v1alpha1=20API?= =?UTF-8?q?=20=E5=90=88=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1alpha1/postgresqlinstance_types.go | 100 +++++++---- api/v1alpha1/postgresqltenant_types.go | 165 +++++++++++++----- api/v1alpha1/postgresqltenant_types_test.go | 73 ++++++++ api/v1alpha1/zz_generated.deepcopy.go | 32 ++++ ...tabase.ddupan.top_postgresqlinstances.yaml | 91 +++++++--- ...database.ddupan.top_postgresqltenants.yaml | 120 +++++++++---- .../database_v1alpha1_postgresqlinstance.yaml | 1 + .../database_v1alpha1_postgresqltenant.yaml | 3 +- docs/api-reference.md | 7 +- docs/specification.md | 6 + internal/controller/api_contract_test.go | 144 +++++++++++++++ .../postgresqlinstance_controller_test.go | 19 +- .../postgresqltenant_controller_test.go | 6 +- 13 files changed, 630 insertions(+), 137 deletions(-) create mode 100644 api/v1alpha1/postgresqltenant_types_test.go create mode 100644 internal/controller/api_contract_test.go diff --git a/api/v1alpha1/postgresqlinstance_types.go b/api/v1alpha1/postgresqlinstance_types.go index eb6f752..ad1c043 100644 --- a/api/v1alpha1/postgresqlinstance_types.go +++ b/api/v1alpha1/postgresqlinstance_types.go @@ -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"` diff --git a/api/v1alpha1/postgresqltenant_types.go b/api/v1alpha1/postgresqltenant_types.go index 3c17962..708438d 100644 --- a/api/v1alpha1/postgresqltenant_types.go +++ b/api/v1alpha1/postgresqltenant_types.go @@ -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 _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 --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"` diff --git a/api/v1alpha1/postgresqltenant_types_test.go b/api/v1alpha1/postgresqltenant_types_test.go new file mode 100644 index 0000000..06fdbe5 --- /dev/null +++ b/api/v1alpha1/postgresqltenant_types_test.go @@ -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) + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 78cffb2..21c00fb 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -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)) diff --git a/config/crd/bases/database.ddupan.top_postgresqlinstances.yaml b/config/crd/bases/database.ddupan.top_postgresqlinstances.yaml index 7ceaa92..af3b608 100644 --- a/config/crd/bases/database.ddupan.top_postgresqlinstances.yaml +++ b/config/crd/bases/database.ddupan.top_postgresqlinstances.yaml @@ -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 diff --git a/config/crd/bases/database.ddupan.top_postgresqltenants.yaml b/config/crd/bases/database.ddupan.top_postgresqltenants.yaml index f017522..397e6bb 100644 --- a/config/crd/bases/database.ddupan.top_postgresqltenants.yaml +++ b/config/crd/bases/database.ddupan.top_postgresqltenants.yaml @@ -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 --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 _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: diff --git a/config/samples/database_v1alpha1_postgresqlinstance.yaml b/config/samples/database_v1alpha1_postgresqlinstance.yaml index 5701f45..5bccbea 100644 --- a/config/samples/database_v1alpha1_postgresqlinstance.yaml +++ b/config/samples/database_v1alpha1_postgresqlinstance.yaml @@ -8,6 +8,7 @@ metadata: spec: endpoint: host: postgres.internal + hostaddr: 192.0.2.10 port: 5432 database: postgres sslMode: verify-full diff --git a/config/samples/database_v1alpha1_postgresqltenant.yaml b/config/samples/database_v1alpha1_postgresqltenant.yaml index d00621f..d0bd344 100644 --- a/config/samples/database_v1alpha1_postgresqltenant.yaml +++ b/config/samples/database_v1alpha1_postgresqltenant.yaml @@ -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 diff --git a/docs/api-reference.md b/docs/api-reference.md index eb931b4..261b0c3 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -17,7 +17,10 @@ - Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不超过 241 个字符,确保派生的 `--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,不含认证信息 | diff --git a/docs/specification.md b/docs/specification.md index 8baa40e..fc731b8 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -131,6 +131,12 @@ Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不得超过 241 派生的 ExternalSecret/Secret 默认名称 `--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 长度限制的 值,并统一限制为小写字母、数字和下划线。 diff --git a/internal/controller/api_contract_test.go b/internal/controller/api_contract_test.go new file mode 100644 index 0000000..2bdedd1 --- /dev/null +++ b/internal/controller/api_contract_test.go @@ -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) + }) +}) diff --git a/internal/controller/postgresqlinstance_controller_test.go b/internal/controller/postgresqlinstance_controller_test.go index 8d3dc32..592b6a1 100644 --- a/internal/controller/postgresqlinstance_controller_test.go +++ b/internal/controller/postgresqlinstance_controller_test.go @@ -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()) } diff --git a/internal/controller/postgresqltenant_controller_test.go b/internal/controller/postgresqltenant_controller_test.go index 57ed114..d227b51 100644 --- a/internal/controller/postgresqltenant_controller_test.go +++ b/internal/controller/postgresqltenant_controller_test.go @@ -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()) } -- 2.54.0