diff --git a/Makefile b/Makefile index 05b9541..f3a8a19 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/database.go b/cmd/database.go index 9603c38..75e87fc 100644 --- a/cmd/database.go +++ b/cmd/database.go @@ -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 } diff --git a/cmd/openbao.go b/cmd/openbao.go index 5615ec9..6184d24 100644 --- a/cmd/openbao.go +++ b/cmd/openbao.go @@ -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 } diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 5c1485b..7736cef 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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、 diff --git a/docs/database/README.md b/docs/database/README.md index e6acf3d..899cc7e 100644 --- a/docs/database/README.md +++ b/docs/database/README.md @@ -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):规范性行为与验收标准; diff --git a/internal/database/adapter/kubernetes/credentials.go b/internal/database/adapter/kubernetes/credentials.go index 2e01583..c4df00a 100644 --- a/internal/database/adapter/kubernetes/credentials.go +++ b/internal/database/adapter/kubernetes/credentials.go @@ -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 } diff --git a/internal/database/adapter/openbao/authentication_integration_test.go b/internal/database/adapter/openbao/authentication_integration_test.go index f48f7a3..8c6401c 100644 --- a/internal/database/adapter/openbao/authentication_integration_test.go +++ b/internal/database/adapter/openbao/authentication_integration_test.go @@ -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() diff --git a/internal/database/adapter/openbao/credentials.go b/internal/database/adapter/openbao/credentials.go index ff80459..ecfddd8 100644 --- a/internal/database/adapter/openbao/credentials.go +++ b/internal/database/adapter/openbao/credentials.go @@ -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 } diff --git a/internal/database/adapter/openbao/credentials_test.go b/internal/database/adapter/openbao/credentials_test.go index 26cce76..b164173 100644 --- a/internal/database/adapter/openbao/credentials_test.go +++ b/internal/database/adapter/openbao/credentials_test.go @@ -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") diff --git a/internal/database/adapter/postgresql/fixture_integration_test.go b/internal/database/adapter/postgresql/fixture_integration_test.go index 593c487..9e37531 100644 --- a/internal/database/adapter/postgresql/fixture_integration_test.go +++ b/internal/database/adapter/postgresql/fixture_integration_test.go @@ -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) } diff --git a/internal/database/adapter/postgresql/instance_controller_integration_test.go b/internal/database/adapter/postgresql/instance_controller_integration_test.go index 78e8f5c..c6e9d57 100644 --- a/internal/database/adapter/postgresql/instance_controller_integration_test.go +++ b/internal/database/adapter/postgresql/instance_controller_integration_test.go @@ -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) diff --git a/internal/database/adapter/openbao/authentication.go b/internal/infra/openbao/authentication.go similarity index 92% rename from internal/database/adapter/openbao/authentication.go rename to internal/infra/openbao/authentication.go index 6f0123c..9918ebd 100644 --- a/internal/database/adapter/openbao/authentication.go +++ b/internal/infra/openbao/authentication.go @@ -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 diff --git a/internal/database/adapter/openbao/authentication_test.go b/internal/infra/openbao/authentication_test.go similarity index 94% rename from internal/database/adapter/openbao/authentication_test.go rename to internal/infra/openbao/authentication_test.go index 4e262c8..779cfb8 100644 --- a/internal/database/adapter/openbao/authentication_test.go +++ b/internal/infra/openbao/authentication_test.go @@ -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" + 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 { diff --git a/internal/infra/openbao/client.go b/internal/infra/openbao/client.go new file mode 100644 index 0000000..70b7715 --- /dev/null +++ b/internal/infra/openbao/client.go @@ -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 +} diff --git a/cmd/openbao_test.go b/internal/infra/openbao/client_test.go similarity index 79% rename from cmd/openbao_test.go rename to internal/infra/openbao/client_test.go index f307d10..fa15381 100644 --- a/cmd/openbao_test.go +++ b/internal/infra/openbao/client_test.go @@ -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:secret@bao.example", "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") } }