refactor: 分离公共基础设施与 Database 适配器
This commit is contained in:
@@ -69,11 +69,11 @@ lint: golangci-lint ## Run golangci-lint linter
|
||||
|
||||
.PHONY: test-database-integration
|
||||
test-database-integration: setup-envtest ## 使用临时 API server、PostgreSQL 与 OpenBao 容器验证 Database 后端。
|
||||
KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test -tags=integration -race -count=1 ./internal/database/...
|
||||
KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test -tags=integration -race -count=1 ./internal/database/... ./internal/infra/...
|
||||
|
||||
.PHONY: lint-database-integration
|
||||
lint-database-integration: golangci-lint ## 检查集成测试构建标签下的 Database 代码。
|
||||
"$(GOLANGCI_LINT)" run --build-tags=integration ./internal/database/...
|
||||
"$(GOLANGCI_LINT)" run --build-tags=integration ./internal/database/... ./internal/infra/...
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func setupInstanceObservation(manager ctrl.Manager, namespace, rootCert string) (*application.InstanceService, error) {
|
||||
credentials, err := kubernetes.NewSecretCredentials(manager.GetConfig(), namespace)
|
||||
credentials, err := kubernetes.NewSecretCredentials(manager.GetAPIReader(), namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+2
-25
@@ -20,13 +20,10 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
bao "github.com/openbao/openbao/api/v2"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/openbao"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/infra/openbao"
|
||||
)
|
||||
|
||||
type openBaoOptions struct {
|
||||
@@ -48,31 +45,11 @@ func (o *openBaoOptions) bindFlags(flags *flag.FlagSet) {
|
||||
flags.StringVar(&o.identity.Audience, "openbao-token-audience", "openbao", "SA JWT audience,须匹配 OpenBao role")
|
||||
}
|
||||
|
||||
func (o openBaoOptions) client() (*bao.Client, error) {
|
||||
address, err := url.Parse(o.address)
|
||||
if err != nil || address.Scheme != "https" || address.Host == "" || address.User != nil ||
|
||||
address.RawQuery != "" || address.ForceQuery || address.Fragment != "" ||
|
||||
(address.Path != "" && address.Path != "/") {
|
||||
return nil, errors.New("OpenBao address must be an absolute HTTPS URL without credentials, query, fragment or path")
|
||||
}
|
||||
// NewConfig 不读取 BAO_TOKEN/BAO_SKIP_VERIFY 等环境配置,不允许旁路 Kubernetes 身份或 TLS。
|
||||
config := bao.NewConfig()
|
||||
config.Address = strings.TrimSuffix(o.address, "/")
|
||||
if config.Error != nil || config.ConfigureTLS(&bao.TLSConfig{CACert: o.caCert}) != nil {
|
||||
return nil, errors.New("cannot configure OpenBao TLS trust")
|
||||
}
|
||||
client, err := bao.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, errors.New("cannot construct OpenBao client")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func setupOpenBaoAuthentication(manager ctrl.Manager, options openBaoOptions) error {
|
||||
if options.address == "" {
|
||||
return nil
|
||||
}
|
||||
client, err := options.client()
|
||||
client, err := openbao.NewClient(options.address, options.caCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -43,6 +43,23 @@ Dev 与 Prod 使用独立的 Kubernetes API、数据库、身份和 controller
|
||||
Proxmox 作为稀缺物理基础设施可以共享,通过 pool、tag、token 和明确的资源范围区分
|
||||
环境。其他后端尽量使用独立数据库、角色、地址池、DNS 空间与凭据。
|
||||
|
||||
## 进程内依赖边界
|
||||
|
||||
基础设施能力属于整个 controller-manager,不因首个消费者是 Database 就归入该领域。
|
||||
`internal/infra/openbao` 管理官方 SDK client 的 TLS 配置、Kubernetes 认证及 token 生命周期,
|
||||
不依赖 Database 或其他产品领域。Bao client 默认禁用自动重试,写入结果不确定时由用例处理;
|
||||
领域适配器不修改共享 client 的全局配置。Kubernetes 客户端、cache 和直连 reader 由 manager 管理;
|
||||
启动入口负责装配与注入,不在领域适配器内重复创建客户端。
|
||||
|
||||
读写能力优先直接使用官方 `client.Reader`、`client.Client`、OpenBao KV API 等接口,
|
||||
不为统一命名再包一层通用 reader/writer,也不引入全局注册中心。共享连接不表示扩大授权;
|
||||
不同身份或权限边界仍由启动装配显式隔离。
|
||||
|
||||
领域按用例需要维护 repository 契约,其 adapter 负责 CR/领域对象映射及业务结果转换。
|
||||
例如 Database 的七键凭据格式、UID 路径、禁止覆盖和不确定结果处理仍由 Database 维护;
|
||||
它们不是公共 KV 存储的业务规则。Secret 管理凭据读取注入 `manager.GetAPIReader()`,
|
||||
保持直连 API server、不缓存 Secret 内容的安全边界;资源写入复用 `manager.GetClient()`。
|
||||
|
||||
## 数据面
|
||||
|
||||
Ayatori 不承载或重新实现数据面。控制面故障只应阻止创建与变更,不应停止已有 VM、
|
||||
|
||||
+14
-2
@@ -174,7 +174,7 @@ Database 状态中的稳定位置和已确认步骤、供应 service/controller
|
||||
|
||||
## OpenBao Kubernetes 认证会话
|
||||
|
||||
`adapter/openbao.KubernetesSession` 复用官方 Kubernetes auth helper 和 `LifetimeWatcher`
|
||||
公共 `internal/infra/openbao.KubernetesSession` 复用官方 Kubernetes auth helper 和 `LifetimeWatcher`
|
||||
(均为 v2.7.0)。认证直接注入 `manager.GetClient()`,与 reconcile 共用已装配的 Kubernetes
|
||||
client,不从配置另建客户端。标准 `--kubeconfig` /
|
||||
`KUBECONFIG` 支持 systemd 或其他集群外运行方式,集群内使用 in-cluster 配置,不要求存在 Pod。
|
||||
@@ -201,12 +201,24 @@ HTTPS 和显式 CA/系统信任根不可通过 BAO 环境变量降级,参数
|
||||
依据官方 [Kubernetes auth](https://openbao.org/docs/auth/kubernetes/) 与
|
||||
[token 生命周期](https://openbao.org/docs/concepts/auth/)。真实测试使用 envtest 签发 SA token,
|
||||
OpenBao 通过专用 reviewer 调用真实 TokenReview;集群外受限 kubeconfig 启动实际 manager,
|
||||
验证同一配置申请 JWT、短 TTL 续期、RBAC 撤回/恢复与重新登录、跨 namespace/其他 SA 拒绝、
|
||||
验证共享 client 申请 JWT、短 TTL 续期、RBAC 撤回/恢复与重新登录、跨 namespace/其他 SA 拒绝、
|
||||
错误 OpenBao audience 拒绝以及凭据访问恢复。临时 TokenReview 入口仅允许对应 POST,
|
||||
两段连接均验证 TLS;其 Docker bridge 入口仅为隔离测试,不修改生产 OpenBao 或 Kubernetes。
|
||||
单元测试补充 TokenRequest 失败/空 token 无回退、重新申请 JWT、无期限 lease 拒绝、
|
||||
并发生命周期、退出清理及 manager 显式参数不受 BAO 环境身份覆盖。
|
||||
|
||||
## 公共基础设施与领域适配
|
||||
|
||||
Bao client/TLS 与认证生命周期已移至 `internal/infra/openbao`,与任何产品领域无关。
|
||||
Kubernetes 读写客户端由 manager 管理,Secret 凭据适配器只接收直连的 `client.Reader`。
|
||||
`adapter/openbao.Credentials` 仍属于 Database:它直接使用官方 KV v2 API,实现七键凭据、
|
||||
UID 路径、CAS=0 与回读确认的领域合同,不把这些规则推广为公共存储语义。
|
||||
后续供应用例需要的 repository 接口由领域侧按实际操作定义,不提前增加通用仓储抽象。
|
||||
分层约定见[总体架构](../architecture/overview.md#进程内依赖边界)。
|
||||
|
||||
认证单元测试归公共 infra;真实认证与 Database 凭据读写的组合测试仍在 Database adapter。
|
||||
Database 集成测试和 lint 入口同时覆盖 `internal/infra/...`,避免拆包导致 CI 漏测。
|
||||
|
||||
## 设计入口
|
||||
|
||||
- [系统规格](specification.md):规范性行为与验收标准;
|
||||
|
||||
@@ -22,10 +22,8 @@ import (
|
||||
"errors"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
typedcore "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/application"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||
@@ -34,18 +32,16 @@ import (
|
||||
// SecretCredentials 直接读取 API server,不将 Secret 数据纳入共享 informer cache。
|
||||
// namespace 在装配时固定,Instance 不能选择跨 namespace 读取。
|
||||
type SecretCredentials struct {
|
||||
secrets typedcore.SecretInterface
|
||||
reader client.Reader
|
||||
namespace string
|
||||
}
|
||||
|
||||
func NewSecretCredentials(config *rest.Config, namespace string) (*SecretCredentials, error) {
|
||||
if config == nil || len(validation.IsDNS1123Label(namespace)) != 0 {
|
||||
return nil, errors.New("valid controller namespace and API configuration required")
|
||||
// NewSecretCredentials 要求注入 manager.GetAPIReader() 或等价直连 reader,不可使用缓存 reader。
|
||||
func NewSecretCredentials(reader client.Reader, namespace string) (*SecretCredentials, error) {
|
||||
if reader == nil || len(validation.IsDNS1123Label(namespace)) != 0 {
|
||||
return nil, errors.New("valid controller namespace and API reader required")
|
||||
}
|
||||
client, err := typedcore.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, application.ErrCredentialsUnavailable
|
||||
}
|
||||
return &SecretCredentials{secrets: client.Secrets(namespace)}, nil
|
||||
return &SecretCredentials{reader: reader, namespace: namespace}, nil
|
||||
}
|
||||
|
||||
func (r *SecretCredentials) Read(ctx context.Context, ref instance.CredentialReference) (application.Credentials, error) {
|
||||
@@ -53,7 +49,8 @@ func (r *SecretCredentials) Read(ctx context.Context, ref instance.CredentialRef
|
||||
return application.Credentials{}, application.ErrCredentialsInvalid
|
||||
}
|
||||
keys := ref.Values()
|
||||
secret, err := r.secrets.Get(ctx, keys.Name, metav1.GetOptions{})
|
||||
secret := &corev1.Secret{}
|
||||
err := r.reader.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: keys.Name}, secret)
|
||||
if err != nil {
|
||||
return application.Credentials{}, application.ErrCredentialsUnavailable
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/openbao"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/infra/openbao"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -232,6 +232,40 @@ func newKubernetesAuthFixture(t *testing.T) *kubernetesAuthFixture {
|
||||
}
|
||||
}
|
||||
|
||||
const authRole = "controller"
|
||||
|
||||
var testIdentity = openbao.KubernetesIdentity{Namespace: authNamespace, ServiceAccount: authRole, Audience: authAudience}
|
||||
|
||||
func waitForAuthentication(t *testing.T, check func() bool) {
|
||||
t.Helper()
|
||||
deadline := time.NewTimer(20 * time.Second)
|
||||
defer deadline.Stop()
|
||||
for !check() {
|
||||
select {
|
||||
case <-deadline.C:
|
||||
t.Fatal("authentication condition timed out")
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startSession(t *testing.T, session interface{ Start(context.Context) error }) context.CancelFunc {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- session.Start(ctx) }()
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Error("authentication did not stop")
|
||||
}
|
||||
})
|
||||
return cancel
|
||||
}
|
||||
|
||||
// 此处验证公共认证会话与 Database 凭据适配器的跨层集成。
|
||||
func TestKubernetesSessionWithRealTokenReview(t *testing.T) {
|
||||
api := newKubernetesAuthFixture(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -49,12 +49,11 @@ type Credentials struct {
|
||||
}
|
||||
|
||||
// NewCredentials 不登录、不读取环境 token。调用方必须提供专用的已认证 client。
|
||||
// 禁用 SDK 写入重试,防止第一次结果丢失后被 CAS 错误掩盖。
|
||||
// client 由公共 infra 禁用自动重试,防止第一次结果丢失后被 CAS 错误掩盖。
|
||||
func NewCredentials(client *bao.Client, mount, basePath string) (*Credentials, error) {
|
||||
if client == nil || !validPath(mount) || !validPath(basePath) {
|
||||
return nil, ErrInvalidLocation
|
||||
}
|
||||
client.SetMaxRetries(0)
|
||||
return &Credentials{kv: client.KVv2(mount), basePath: basePath}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ func fixtureClient(t *testing.T, address string) *bao.Client {
|
||||
t.Helper()
|
||||
config := bao.DefaultConfig()
|
||||
config.Address = address
|
||||
config.MaxRetries = 0
|
||||
client, err := bao.NewClient(config)
|
||||
if err != nil {
|
||||
t.Fatal("cannot construct fixture client")
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
|
||||
secretadapter "git.ddupan.top/panxiao81/ayatori/internal/database/adapter/kubernetes"
|
||||
@@ -244,7 +245,11 @@ func newCredentialFixture(t *testing.T) *credentialFixture {
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := secretadapter.NewSecretCredentials(config, controllerNamespace)
|
||||
apiReader, err := kubeclient.New(config, kubeclient.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reader, err := secretadapter.NewSecretCredentials(apiReader, controllerNamespace)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -252,7 +257,11 @@ func newCredentialFixture(t *testing.T) *credentialFixture {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deniedReader, err := secretadapter.NewSecretCredentials(user.Config(), controllerNamespace)
|
||||
deniedAPIReader, err := kubeclient.New(user.Config(), kubeclient.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deniedReader, err := secretadapter.NewSecretCredentials(deniedAPIReader, controllerNamespace)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -50,14 +50,6 @@ func TestInstanceControllerWithRealPostgreSQL(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restricted := instanceControllerRBAC(t, f, apiClient)
|
||||
credentials, err := secretadapter.NewSecretCredentials(restricted, controllerNamespace)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service, err := application.NewInstanceService(credentials, postgresql.Connector{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 同进程 -count 重复启动测试 manager;生产继续校验 controller 名称唯一。
|
||||
skipRepeatedName := true
|
||||
manager, err := ctrl.NewManager(restricted, ctrl.Options{
|
||||
@@ -68,6 +60,14 @@ func TestInstanceControllerWithRealPostgreSQL(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credentials, err := secretadapter.NewSecretCredentials(manager.GetAPIReader(), controllerNamespace)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service, err := application.NewInstanceService(credentials, postgresql.Connector{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reconciler := &databasecontroller.InstanceReconciler{Observer: service, SecretNamespace: controllerNamespace}
|
||||
if err := reconciler.SetupWithManager(manager); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
+14
-2
@@ -19,6 +19,7 @@ package openbao
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -34,6 +35,17 @@ import (
|
||||
|
||||
var ErrAuthenticationConfiguration = errors.New("invalid OpenBao Kubernetes authentication configuration")
|
||||
|
||||
var authPathSegment = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||
|
||||
func validAuthMount(mount string) bool {
|
||||
for segment := range strings.SplitSeq(mount, "/") {
|
||||
if !authPathSegment.MatchString(segment) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// KubernetesSession 为专用 SDK client 维护短期登录,不持久化或对外返回 token。
|
||||
// 每次登录通过 manager 的 Kubernetes 身份申请新的 SA JWT,不依赖 controller 的部署位置。
|
||||
// 续期调度由官方 LifetimeWatcher 负责,不实现自己的 lease 算法。
|
||||
@@ -47,7 +59,7 @@ type KubernetesSession struct {
|
||||
ready atomic.Bool
|
||||
}
|
||||
|
||||
// KubernetesIdentity 是部署固定的登录目标,不由 Tenant 选择。
|
||||
// KubernetesIdentity 是部署固定的登录目标,不由业务请求选择。
|
||||
type KubernetesIdentity struct {
|
||||
Namespace string
|
||||
ServiceAccount string
|
||||
@@ -60,7 +72,7 @@ func NewKubernetesSession(
|
||||
mount, role string,
|
||||
identity KubernetesIdentity,
|
||||
) (*KubernetesSession, error) {
|
||||
if client == nil || kubernetes == nil || !validPath(mount) || !pathSegment.MatchString(role) ||
|
||||
if client == nil || kubernetes == nil || !validAuthMount(mount) || !authPathSegment.MatchString(role) ||
|
||||
len(validation.IsDNS1123Label(identity.Namespace)) != 0 ||
|
||||
len(validation.IsDNS1123Subdomain(identity.ServiceAccount)) != 0 || strings.TrimSpace(identity.Audience) == "" {
|
||||
return nil, ErrAuthenticationConfiguration
|
||||
+15
-1
@@ -25,6 +25,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
bao "github.com/openbao/openbao/api/v2"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
@@ -33,13 +34,26 @@ import (
|
||||
"k8s.io/client-go/rest"
|
||||
kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/openbao"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/infra/openbao"
|
||||
)
|
||||
|
||||
const (
|
||||
authRole = "controller"
|
||||
fixtureToken = "AYATORI-TEST-ONLY-bao-token"
|
||||
)
|
||||
|
||||
func fixtureClient(t *testing.T, address string) *bao.Client {
|
||||
t.Helper()
|
||||
config := bao.NewConfig()
|
||||
config.Address = address
|
||||
client, err := bao.NewClient(config)
|
||||
if err != nil {
|
||||
t.Fatal("cannot construct fixture client")
|
||||
}
|
||||
client.SetToken(fixtureToken)
|
||||
return client
|
||||
}
|
||||
|
||||
var testIdentity = openbao.KubernetesIdentity{Namespace: "bao-controller", ServiceAccount: authRole, Audience: "openbao"}
|
||||
|
||||
func authenticationClient(t *testing.T, address string) kubeclient.Client {
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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 openbao 管理控制面共享的 OpenBao 连接与认证,不依赖任何产品领域。
|
||||
package openbao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
bao "github.com/openbao/openbao/api/v2"
|
||||
)
|
||||
|
||||
// NewClient 创建供读写适配器共享的官方 SDK client;身份由 KubernetesSession 管理。
|
||||
func NewClient(addressURL, caCert string) (*bao.Client, error) {
|
||||
address, err := url.Parse(addressURL)
|
||||
if err != nil || address.Scheme != "https" || address.Host == "" || address.User != nil ||
|
||||
address.RawQuery != "" || address.ForceQuery || address.Fragment != "" ||
|
||||
(address.Path != "" && address.Path != "/") {
|
||||
return nil, errors.New("OpenBao address must be an absolute HTTPS URL without credentials, query, fragment or path")
|
||||
}
|
||||
// NewConfig 不读取 BAO_TOKEN/BAO_SKIP_VERIFY 等环境配置,不允许旁路 Kubernetes 身份或 TLS。
|
||||
config := bao.NewConfig()
|
||||
config.Address = strings.TrimSuffix(addressURL, "/")
|
||||
// 公共读写 client 不自动重试:写入结果不确定时交由具体用例决定恢复行为。
|
||||
config.MaxRetries = 0
|
||||
config.Timeout = 15 * time.Second
|
||||
if config.Error != nil || config.ConfigureTLS(&bao.TLSConfig{CACert: caCert}) != nil {
|
||||
return nil, errors.New("cannot configure OpenBao TLS trust")
|
||||
}
|
||||
client, err := bao.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, errors.New("cannot construct OpenBao client")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
@@ -14,35 +14,35 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
package openbao_test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/infra/openbao"
|
||||
)
|
||||
|
||||
func TestOpenBaoClientDoesNotUseEnvironmentIdentityOrAddress(t *testing.T) {
|
||||
t.Setenv("BAO_TOKEN", "TEST-ONLY-unwanted-static-token")
|
||||
t.Setenv("BAO_ADDR", "http://unwanted.invalid")
|
||||
t.Setenv("BAO_SKIP_VERIFY", "true")
|
||||
var options openBaoOptions
|
||||
options.bindFlags(flag.NewFlagSet("test", flag.ContinueOnError))
|
||||
options.address = "https://bao.example/"
|
||||
client, err := options.client()
|
||||
client, err := openbao.NewClient("https://bao.example/", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if client.Address() != "https://bao.example" || client.Token() != "" {
|
||||
t.Fatal("ambient environment replaced the explicit connection or identity")
|
||||
}
|
||||
if client.MaxRetries() != 0 {
|
||||
t.Fatal("shared client must not automatically retry uncertain writes")
|
||||
}
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
options.address = server.URL
|
||||
untrusted, err := options.client()
|
||||
untrusted, err := openbao.NewClient(server.URL, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -57,11 +57,11 @@ func TestOpenBaoClientRejectsUnsafeConfiguration(t *testing.T) {
|
||||
"", "http://bao.example", "https://user:[email protected]",
|
||||
"https://bao.example/?token=secret", "https://bao.example/#secret", "https://bao.example/path",
|
||||
} {
|
||||
if _, err := (openBaoOptions{address: address}).client(); err == nil {
|
||||
if _, err := openbao.NewClient(address, ""); err == nil {
|
||||
t.Fatal("accepted unsafe OpenBao address")
|
||||
}
|
||||
}
|
||||
if _, err := (openBaoOptions{address: "https://bao.example", caCert: "/nonexistent/fixture-ca"}).client(); err == nil {
|
||||
if _, err := openbao.NewClient("https://bao.example", "/nonexistent/fixture-ca"); err == nil {
|
||||
t.Fatal("accepted missing explicit CA")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user