From 4e2f3ba489ccad3271dbabec71856376bd95f31c Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 28 Nov 2025 16:37:26 -0500 Subject: [PATCH] Support external plugins in NewTestDockerCluster (#11023) (#11035) --- changelog/_11023.txt | 3 + sdk/helper/testcluster/docker/environment.go | 68 +++++++++++++++++++- sdk/helper/testcluster/types.go | 2 + 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 changelog/_11023.txt diff --git a/changelog/_11023.txt b/changelog/_11023.txt new file mode 100644 index 0000000000..595fa1580c --- /dev/null +++ b/changelog/_11023.txt @@ -0,0 +1,3 @@ +```release-note:improvement +sdk: Add NewTestDockerCluster support for running external plugins within the same container as the server. Also add support for those plugins to expose their own listeners, as KMIP does. +``` diff --git a/sdk/helper/testcluster/docker/environment.go b/sdk/helper/testcluster/docker/environment.go index 1fc7277397..90f0caf7bc 100644 --- a/sdk/helper/testcluster/docker/environment.go +++ b/sdk/helper/testcluster/docker/environment.go @@ -498,6 +498,7 @@ type DockerClusterNode struct { DataVolumeName string cleanupVolume func() AllClients []*api.Client + ExtraAddrs []string } func (n *DockerClusterNode) TLSConfig() *tls.Config { @@ -699,6 +700,11 @@ func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOption ports = append(ports, portStr) } } + if opts.VaultNodeConfig != nil { + for _, portNo := range opts.VaultNodeConfig.AdditionalTCPPorts { + ports = append(ports, fmt.Sprintf("%d/tcp", portNo)) + } + } vaultCfg["listener"] = listenerConfig vaultCfg["telemetry"] = map[string]interface{}{ "disable_hostname": true, @@ -934,7 +940,11 @@ func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOption n.AllClients = append(n.AllClients, client) - for _, addr := range svc.StartResult.Addrs[2:] { + end := len(svc.StartResult.Addrs) + if opts.VaultNodeConfig != nil { + end = 2 + len(opts.VaultNodeConfig.AdditionalListeners) + } + for _, addr := range svc.StartResult.Addrs[2:end] { // The second element of this list of addresses is the cluster address // We do not want to create a client for the cluster address mapping client, err := n.newAPIClientForAddress(addr) @@ -944,6 +954,9 @@ func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOption client.SetToken(n.Cluster.rootToken) n.AllClients = append(n.AllClients, client) } + if len(svc.StartResult.Addrs) > end { + n.ExtraAddrs = svc.StartResult.Addrs[end:] + } return nil } @@ -1254,12 +1267,65 @@ func (dc *DockerCluster) addNode(ctx context.Context, opts *DockerClusterOptions if err := os.MkdirAll(node.WorkDir, 0o755); err != nil { return err } + if err := copyDirContents(node.WorkDir, dc.tmpDir); err != nil { + return err + } if err := node.Start(ctx, opts); err != nil { return err } return nil } +func copyFile(to string, from string) error { + in, err := os.Open(from) + if err != nil { + return fmt.Errorf("failed to open source file %s: %w", from, err) + } + defer in.Close() + + out, err := os.Create(to) + if err != nil { + return fmt.Errorf("failed to create destination file %s: %w", to, err) + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return fmt.Errorf("failed to copy file content from %s to %s: %w", from, to, err) + } + + // Copy file permissions + info, err := os.Stat(from) + if err != nil { + return fmt.Errorf("failed to get source file info %s: %w", from, err) + } + err = os.Chmod(to, info.Mode()) + if err != nil { + return fmt.Errorf("failed to set destination file permissions %s: %w", to, err) + } + + return nil +} + +func copyDirContents(to string, from string) error { + entries, err := os.ReadDir(from) + if err != nil { + return fmt.Errorf("failed to read source directory %s: %w", from, err) + } + + for _, entry := range entries { + fromPath := filepath.Join(from, entry.Name()) + toPath := filepath.Join(to, entry.Name()) + + if !entry.IsDir() { + if err = copyFile(toPath, fromPath); err != nil { + return err + } + } + } + return nil +} + func (dc *DockerCluster) joinNode(ctx context.Context, nodeIdx int, leaderIdx int) error { if dc.storage != nil && dc.storage.Type() != "raft" { // Storage is not raft so nothing to do but unseal. diff --git a/sdk/helper/testcluster/types.go b/sdk/helper/testcluster/types.go index c57687db7c..b16eb0612b 100644 --- a/sdk/helper/testcluster/types.go +++ b/sdk/helper/testcluster/types.go @@ -56,6 +56,7 @@ type VaultNodeConfig struct { StorageOptions map[string]string `json:"-"` AdditionalListeners []VaultNodeListenerConfig `json:"-"` + AdditionalTCPPorts []int `json:"-"` DefaultMaxRequestDuration time.Duration `json:"default_max_request_duration"` LogFormat string `json:"log_format"` @@ -67,6 +68,7 @@ type VaultNodeConfig struct { MaxLeaseTTL time.Duration `json:"max_lease_ttl"` DefaultLeaseTTL time.Duration `json:"default_lease_ttl"` ClusterCipherSuites string `json:"cluster_cipher_suites"` + PluginDirectory string `json:"plugin_directory"` PluginFileUid int `json:"plugin_file_uid"` PluginFilePermissions int `json:"plugin_file_permissions"` EnableRawEndpoint bool `json:"raw_storage_endpoint"`