feat: 实现 PostgreSQL 所有权 registry
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
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 registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func TestRegistryLifecycle(t *testing.T) {
|
||||
dsn := os.Getenv("POSTGRES_TEST_DSN")
|
||||
if dsn == "" {
|
||||
t.Skip("POSTGRES_TEST_DSN is not set")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("create PostgreSQL pool: %v", err)
|
||||
}
|
||||
t.Cleanup(pool.Close)
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
t.Fatalf("ping PostgreSQL: %v", err)
|
||||
}
|
||||
|
||||
dropRegistrySchema(t, ctx, pool)
|
||||
t.Cleanup(func() { dropRegistrySchema(t, ctx, pool) })
|
||||
|
||||
store := NewStore(pool)
|
||||
if err := store.Bootstrap(ctx); err != nil {
|
||||
t.Fatalf("Bootstrap() error = %v", err)
|
||||
}
|
||||
if err := store.Bootstrap(ctx); err != nil {
|
||||
t.Fatalf("second Bootstrap() error = %v", err)
|
||||
}
|
||||
|
||||
owner := testOwnership("tenant-uid-1", "netbox", "netbox", "netbox", "postgresql-tenants/netbox/netbox")
|
||||
result, err := store.Claim(ctx, owner)
|
||||
if err != nil || result != ClaimCreated {
|
||||
t.Fatalf("first Claim() = %q, %v; want %q, nil", result, err, ClaimCreated)
|
||||
}
|
||||
result, err = store.Claim(ctx, owner)
|
||||
if err != nil || result != ClaimOwned {
|
||||
t.Fatalf("second Claim() = %q, %v; want %q, nil", result, err, ClaimOwned)
|
||||
}
|
||||
|
||||
record, err := store.Get(ctx, owner.InstanceUID, owner.TenantUID)
|
||||
if err != nil {
|
||||
t.Fatalf("Get() error = %v", err)
|
||||
}
|
||||
if record.Ownership != owner || !record.Managed || record.RetainedAt != nil {
|
||||
t.Fatalf("Get() = %#v; want matching managed owner", record)
|
||||
}
|
||||
|
||||
conflict := testOwnership("tenant-uid-2", "other", owner.DatabaseName, "other", "postgresql-tenants/other/other")
|
||||
if _, err := store.Claim(ctx, conflict); !errors.Is(err, ErrConflict) {
|
||||
t.Fatalf("conflicting Claim() error = %v, want ErrConflict", err)
|
||||
}
|
||||
|
||||
if err := store.MarkRetained(ctx, owner); err != nil {
|
||||
t.Fatalf("MarkRetained() error = %v", err)
|
||||
}
|
||||
if err := store.MarkRetained(ctx, owner); err != nil {
|
||||
t.Fatalf("second MarkRetained() error = %v", err)
|
||||
}
|
||||
record, err = store.Get(ctx, owner.InstanceUID, owner.TenantUID)
|
||||
if err != nil || record.Managed || record.RetainedAt == nil {
|
||||
t.Fatalf("retained Get() = %#v, %v; want unmanaged tombstone", record, err)
|
||||
}
|
||||
if err := store.Delete(ctx, owner); !errors.Is(err, ErrConflict) {
|
||||
t.Fatalf("Delete(retained) error = %v, want ErrConflict", err)
|
||||
}
|
||||
if _, err := store.Claim(ctx, owner); !errors.Is(err, ErrConflict) {
|
||||
t.Fatalf("Claim(retained) error = %v, want ErrConflict", err)
|
||||
}
|
||||
|
||||
deletable := testOwnership("tenant-uid-3", "gitea", "gitea", "gitea", "postgresql-tenants/gitea/gitea")
|
||||
if _, err := store.Claim(ctx, deletable); err != nil {
|
||||
t.Fatalf("Claim(deletable) error = %v", err)
|
||||
}
|
||||
if err := store.Delete(ctx, deletable); err != nil {
|
||||
t.Fatalf("Delete() error = %v", err)
|
||||
}
|
||||
if err := store.Delete(ctx, deletable); err != nil {
|
||||
t.Fatalf("second Delete() error = %v", err)
|
||||
}
|
||||
if _, err := store.Get(ctx, deletable.InstanceUID, deletable.TenantUID); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatalf("Get(deleted) error = %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testOwnership(tenantUID, tenantName, databaseName, roleName, credentialPath string) Ownership {
|
||||
return Ownership{
|
||||
InstanceUID: "instance-uid-1",
|
||||
TenantUID: tenantUID,
|
||||
TenantNamespace: tenantName,
|
||||
TenantName: tenantName,
|
||||
DatabaseName: databaseName,
|
||||
RoleName: roleName,
|
||||
CredentialPath: credentialPath,
|
||||
}
|
||||
}
|
||||
|
||||
type schemaDropper interface {
|
||||
Exec(context.Context, string, ...any) (pgconn.CommandTag, error)
|
||||
}
|
||||
|
||||
func dropRegistrySchema(t *testing.T, ctx context.Context, db schemaDropper) {
|
||||
t.Helper()
|
||||
if _, err := db.Exec(ctx, `DROP SCHEMA IF EXISTS postgresql_tenant_operator CASCADE`); err != nil {
|
||||
t.Fatalf("drop registry schema: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user