feat: 接入 Instance 版本与扩展可用性观测
Verify / test (pull_request) Successful in 6m1s
Verify / lint (pull_request) Successful in 7m19s
Verify / database-integration (pull_request) Successful in 5m11s

This commit is contained in:
2026-09-24 15:14:43 +00:00
parent 6acba0ca46
commit 9441b568da
11 changed files with 347 additions and 35 deletions
@@ -47,14 +47,6 @@ func (d *database) Close() {
d.pool.Close()
}
func (d *database) Version(ctx context.Context) (string, error) {
var version string
if err := d.pool.QueryRow(ctx, "SHOW server_version").Scan(&version); err != nil {
return "", safeError(err, application.ErrObservation)
}
return version, nil
}
func (c Connector) Connect(ctx context.Context, endpoint instance.Endpoint, credentials application.Credentials) (application.Database, error) {
if err := endpoint.Validate(); err != nil {
return nil, err
@@ -131,13 +131,17 @@ func TestObservationDiscardsResultWhenCredentialsChange(t *testing.T) {
}
}
version, err := fixture.service.ObserveVersion(fixture.ctx, fixture.target)
observation, err := fixture.service.ObserveMetadata(fixture.ctx, fixture.target)
if !errors.Is(err, application.ErrCredentialsChanged) {
t.Fatal("in-flight rotation was not detected")
}
if version != "" {
if observation.Version() != "" {
t.Fatal("observation returned data obtained with stale credentials")
}
requested := instance.NewExtensionSet([]string{fixtureExtension})
if observation.Extensions().Check(requested).Decision != instance.ExtensionSupportUnobserved {
t.Fatal("observation returned extension support obtained with stale credentials")
}
if fixture.backendIDs(t) != "" {
t.Fatal("stale connection was retained after rotation")
}
@@ -42,6 +42,7 @@ import (
const (
fixtureHost = "fixture.invalid"
fixtureUser = "postgres"
fixtureExtension = "plpgsql"
dockerExec = "exec"
fixtureImage = "postgres@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73"
fixturePassword = "AYATORI-TEST-ONLY-initial-password"
@@ -0,0 +1,48 @@
/*
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 postgresql
import (
"context"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
)
// 使用 pg_catalog 限定名称,避免管理账号的 search_path 改变查询来源。
// 一个语句读取版本和可用列表;ARRAY 子查询在无行时返回空数组,而非 NULL。
// 不查询 pg_extension:已安装集合不能代表服务器提供的全部扩展。
const inspectMetadataStatement = `
SELECT
pg_catalog.current_setting('server_version'),
ARRAY(
SELECT name::text
FROM pg_catalog.pg_available_extensions
ORDER BY name
)`
func (d *database) InspectMetadata(ctx context.Context) (application.DatabaseMetadata, error) {
var metadata application.DatabaseMetadata
err := d.pool.QueryRow(ctx, inspectMetadataStatement).Scan(
&metadata.Version,
&metadata.AvailableExtensions,
)
if err != nil {
// 不返回部分结果,也不把查询失败转换为“成功观察到空列表”。
return application.DatabaseMetadata{}, safeError(err, application.ErrObservation)
}
return metadata, nil
}
@@ -0,0 +1,115 @@
//go:build integration
/*
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 postgresql_test
import (
"errors"
"testing"
corev1 "k8s.io/api/core/v1"
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
)
func TestMetadataObservesAvailableExtensionsWithoutInstalling(t *testing.T) {
f := newCredentialFixture(t)
f.createSecret(t, controllerNamespace)
if installed := f.queryPostgres(t, "SELECT count(*) FROM pg_catalog.pg_extension WHERE extname = 'hstore'"); installed != "0" {
t.Fatal("fixture unexpectedly has hstore installed")
}
// 提供同名遮蔽对象,验证 adapter 不依赖管理账号可修改的 search_path。
f.queryPostgres(t, "CREATE VIEW public.pg_available_extensions AS SELECT 'fake_extension'::name AS name")
f.queryPostgres(t, "ALTER ROLE postgres SET search_path = public, pg_catalog")
observed, err := f.service.ObserveMetadata(f.ctx, f.target)
if err != nil {
t.Fatal(err)
}
if !observed.Target().Matches(f.target) || observed.Version() == "" {
t.Fatal("metadata was not bound to the current target")
}
requested := instance.NewExtensionSet([]string{"hstore", fixtureExtension})
if observed.Extensions().Check(requested).Decision != instance.ExtensionsAccepted {
t.Fatal("available but uninstalled extension was omitted")
}
unsupported := instance.NewExtensionSet([]string{"fake_extension", "HSTORE"})
check := observed.Extensions().Check(unsupported)
if check.Decision != instance.ExtensionsUnsupported || len(check.Unsupported) != 2 {
t.Fatal("metadata accepted shadowed or case-normalized extension names")
}
if installed := f.queryPostgres(t, "SELECT count(*) FROM pg_catalog.pg_extension WHERE extname = 'hstore'"); installed != "0" {
t.Fatal("metadata observation installed an extension")
}
if schemas := f.queryPostgres(t, "SELECT count(*) FROM pg_catalog.pg_namespace WHERE nspname = 'postgresql_tenant_operator'"); schemas != "0" {
t.Fatal("metadata observation initialized the registry")
}
aggregate, err := instance.Reconstitute(f.target, instance.Snapshot{}, false)
if err != nil {
t.Fatal(err)
}
if err := aggregate.ObserveExtensions(observed.Target(), observed.Extensions()); err != nil {
t.Fatal(err)
}
if aggregate.CheckExtensions(requested).Decision != instance.ExtensionsAccepted {
t.Fatal("domain rejected observed extension availability")
}
if err := aggregate.RequireProvisioningReady(); err == nil {
t.Fatal("extension availability incorrectly authorized provisioning")
}
}
func TestMetadataPermissionFailureAndRecovery(t *testing.T) {
f := newCredentialFixture(t)
f.createSecret(t, controllerNamespace)
// 低权限账号也能读取可用列表;这不能证明具备 role/database/extension 管理权限。
f.queryPostgres(t, "CREATE ROLE metadata_reader LOGIN PASSWORD '"+fixturePassword+"'")
f.updateSecret(t, func(secret *corev1.Secret) {
secret.Data["login"] = []byte("metadata_reader")
})
if flags := f.queryPostgres(t, "SELECT rolsuper, rolcreaterole, rolcreatedb FROM pg_catalog.pg_roles WHERE rolname = 'metadata_reader'"); flags != "f|f|f" {
t.Fatal("metadata reader unexpectedly has management privileges")
}
requested := instance.NewExtensionSet([]string{fixtureExtension})
observed, err := f.service.ObserveMetadata(f.ctx, f.target)
if err != nil || observed.Extensions().Check(requested).Decision != instance.ExtensionsAccepted {
t.Fatalf("read-only account could not observe metadata: %v", err)
}
// 仅操作本测试独占容器的 catalog ACL;失败不能转换成“不支持任何扩展”。
f.queryPostgres(t, "REVOKE SELECT ON pg_catalog.pg_available_extensions FROM PUBLIC")
failed, err := f.service.ObserveMetadata(f.ctx, f.target)
if !errors.Is(err, application.ErrObservation) {
t.Fatalf("metadata permission failure was not reported safely: %v", err)
}
if failed.Version() != "" || failed.Target().Validate() == nil {
t.Fatal("permission failure returned partial metadata")
}
if failed.Extensions().Check(requested).Decision != instance.ExtensionSupportUnobserved {
t.Fatal("permission failure returned an observed empty set")
}
if f.backendIDs(t) != "" {
t.Fatal("failed metadata connection was retained")
}
f.queryPostgres(t, "GRANT SELECT ON pg_catalog.pg_available_extensions TO PUBLIC")
recovered, err := f.service.ObserveMetadata(f.ctx, f.target)
if err != nil || recovered.Extensions().Check(requested).Decision != instance.ExtensionsAccepted {
t.Fatalf("metadata observation did not recover: %v", err)
}
}
@@ -107,7 +107,7 @@ func TestPostgreSQLTLSHostIdentity(t *testing.T) {
if err != nil {
t.Fatal("trusted DNS SAN connection failed", err)
}
if version, err := db.Version(ctx); err != nil || version == "" {
if metadata, err := db.InspectMetadata(ctx); err != nil || metadata.Version == "" {
db.Close()
t.Fatal("TLS metadata read failed", err)
}