Skip to content

Commit c7f59b9

Browse files
author
vfernandezg
committed
feat(database): add validation to PluginFactoryConfig and improve documentation
- Add validate() method to PluginFactoryConfig to check for required fields (Sys and Logger) before plugin initialization - Prevent nil dereference panics by surfacing validation errors early to callers - Refactor PluginFactoryWithConfig to use cfg directly instead of local variable assignments, improving consistency - Update comments in backend.go to clarify mount point telemetry label behavior when opt-in is disabled - Update telemetry.go documentation to reflect namespace labels in addition to mount_point and connection_name - Add comprehensive test suite (plugin_factory_test.go) covering config validation scenarios
1 parent 1be27d9 commit c7f59b9

4 files changed

Lines changed: 172 additions & 19 deletions

File tree

builtin/logical/database/backend.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,10 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
151151
}
152152

153153
// metricsLabelsForConnection returns the telemetry labels to attach to metrics
154-
// emitted on behalf of the named connection. It returns the zero value unless
155-
// the operator opted in, in which case metrics stay unlabeled and aggregate
156-
// across mounts as they always have.
154+
// emitted on behalf of the named connection.
155+
//
156+
// When the operator has not opted in, it returns the zero value, which leaves
157+
// metrics unlabeled so they keep aggregating across mounts as they always have.
157158
func (b *databaseBackend) metricsLabelsForConnection(name string) databaseWrapperMetricsLabels {
158159
if !b.includeMountPointInMetrics {
159160
return databaseWrapperMetricsLabels{}

internalshared/configutil/telemetry.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,10 @@ type Telemetry struct {
214214
// metrics
215215
RollbackMetricsIncludeMountPoint bool `hcl:"add_mount_point_rollback_metrics"`
216216

217-
// Whether or not telemetry should add mount_point and connection_name
218-
// labels to the database secrets engine metrics. This is opt-in because it
219-
// multiplies the series count by the number of configured connections.
217+
// Whether or not telemetry should add namespace, mount_point, and
218+
// connection_name labels to the database secrets engine metrics. This is
219+
// opt-in because it multiplies the series count by the number of
220+
// namespaces, mounts, and configured connections.
220221
DatabaseMetricsIncludeMountPoint bool `hcl:"add_mount_point_database_metrics"`
221222
}
222223

sdk/database/dbplugin/v5/plugin_factory.go

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package dbplugin
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910

1011
"github.com/hashicorp/errwrap"
@@ -37,6 +38,25 @@ type PluginFactoryConfig struct {
3738
ConnectionName string
3839
}
3940

41+
// validate reports whether the config carries the collaborators required to
42+
// build a plugin. It exists because callers construct PluginFactoryConfig as a
43+
// struct literal, so an omitted field would otherwise surface as a nil
44+
// dereference deep inside plugin startup rather than as an error the caller can
45+
// handle.
46+
//
47+
// Only the fields that would panic are checked. PluginName is deliberately not
48+
// validated: an unknown or empty name is already reported by the plugin catalog
49+
// lookup, which some callers rely on to resolve a default plugin.
50+
func (cfg PluginFactoryConfig) validate() error {
51+
if cfg.Sys == nil {
52+
return errors.New("plugin system view is required")
53+
}
54+
if cfg.Logger == nil {
55+
return errors.New("logger is required")
56+
}
57+
return nil
58+
}
59+
4060
// PluginFactory is used to build plugin database types. It wraps the database
4161
// object in a logging and metrics middleware.
4262
func PluginFactory(ctx context.Context, pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger) (Database, error) {
@@ -57,18 +77,17 @@ func PluginFactoryVersion(ctx context.Context, pluginName string, pluginVersion
5777
// PluginFactoryWithConfig is used to build plugin database types from a config
5878
// struct. It wraps the database object in a logging and metrics middleware.
5979
func PluginFactoryWithConfig(ctx context.Context, cfg PluginFactoryConfig) (Database, error) {
60-
pluginName := cfg.PluginName
61-
pluginVersion := cfg.PluginVersion
62-
sys := cfg.Sys
63-
logger := cfg.Logger
80+
if err := cfg.validate(); err != nil {
81+
return nil, err
82+
}
6483

6584
// Look for plugin in the plugin catalog
66-
pluginRunner, err := sys.LookupPluginVersion(ctx, pluginName, consts.PluginTypeDatabase, pluginVersion)
85+
pluginRunner, err := cfg.Sys.LookupPluginVersion(ctx, cfg.PluginName, consts.PluginTypeDatabase, cfg.PluginVersion)
6786
if err != nil {
6887
return nil, err
6988
}
7089

71-
namedLogger := logger.Named(pluginName)
90+
namedLogger := cfg.Logger.Named(cfg.PluginName)
7291

7392
var transport string
7493
var db Database
@@ -83,34 +102,34 @@ func PluginFactoryWithConfig(ctx context.Context, cfg PluginFactoryConfig) (Data
83102
var ok bool
84103
db, ok = dbRaw.(Database)
85104
if !ok {
86-
return nil, fmt.Errorf("unsupported database type: %q", pluginName)
105+
return nil, fmt.Errorf("unsupported database type: %q", cfg.PluginName)
87106
}
88107

89108
transport = "builtin"
90109

91110
} else {
92111
if pluginRunner.Download {
93-
if err = sys.DownloadExtractVerifyPlugin(ctx, pluginRunner); err != nil {
112+
if err = cfg.Sys.DownloadExtractVerifyPlugin(ctx, pluginRunner); err != nil {
94113
return nil, fmt.Errorf("failed to extract and verify plugin=%q version=%q: %w",
95114
pluginRunner.Name, pluginRunner.Version, err)
96115
}
97116
}
98117

99118
config := pluginutil.PluginClientConfig{
100-
Name: pluginName,
119+
Name: cfg.PluginName,
101120
PluginType: consts.PluginTypeDatabase,
102-
Version: pluginVersion,
121+
Version: cfg.PluginVersion,
103122
PluginSets: PluginSets,
104123
HandshakeConfig: HandshakeConfig,
105124
Logger: namedLogger,
106125
IsMetadataMode: false,
107126
AutoMTLS: true,
108-
Wrapper: sys,
127+
Wrapper: cfg.Sys,
109128
Tier: pluginRunner.Tier,
110129
}
111130

112131
// create a DatabasePluginClient instance
113-
db, err = NewPluginClient(ctx, sys, config)
132+
db, err = NewPluginClient(ctx, cfg.Sys, config)
114133
if err != nil {
115134
return nil, err
116135
}
@@ -128,7 +147,7 @@ func PluginFactoryWithConfig(ctx context.Context, cfg PluginFactoryConfig) (Data
128147
if err != nil {
129148
return nil, errwrap.Wrapf("error getting plugin type: {{err}}", err)
130149
}
131-
logger.Debug("got database plugin instance", "type", typeStr)
150+
cfg.Logger.Debug("got database plugin instance", "type", typeStr)
132151

133152
// Wrap with metrics middleware
134153
db = &databaseMetricsMiddleware{
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright IBM Corp. 2016, 2025
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dbplugin
5+
6+
import (
7+
"context"
8+
"strings"
9+
"testing"
10+
11+
"github.com/hashicorp/go-hclog"
12+
"github.com/hashicorp/vault/sdk/helper/pluginutil"
13+
)
14+
15+
// stubLookRunnerUtil is a non-functional pluginutil.LookRunnerUtil used only to
16+
// supply a non-nil Sys. The embedded nil interface is never dereferenced because
17+
// config validation rejects the input before any plugin lookup happens.
18+
type stubLookRunnerUtil struct {
19+
pluginutil.LookRunnerUtil
20+
}
21+
22+
func TestPluginFactoryConfig_Validate(t *testing.T) {
23+
type testCase struct {
24+
config PluginFactoryConfig
25+
26+
// expectedErrContains is empty when the config is expected to be valid.
27+
expectedErrContains string
28+
}
29+
30+
tests := map[string]testCase{
31+
"missing sys": {
32+
config: PluginFactoryConfig{
33+
PluginName: "postgresql-database-plugin",
34+
Logger: hclog.NewNullLogger(),
35+
},
36+
expectedErrContains: "system view",
37+
},
38+
"missing logger": {
39+
config: PluginFactoryConfig{
40+
PluginName: "postgresql-database-plugin",
41+
Sys: stubLookRunnerUtil{},
42+
},
43+
expectedErrContains: "logger",
44+
},
45+
"fully populated config is valid": {
46+
config: PluginFactoryConfig{
47+
PluginName: "postgresql-database-plugin",
48+
Sys: stubLookRunnerUtil{},
49+
Logger: hclog.NewNullLogger(),
50+
},
51+
expectedErrContains: "",
52+
},
53+
// An empty plugin name is left for the plugin catalog lookup to report,
54+
// because some callers rely on it to resolve a default plugin.
55+
"empty plugin name is not rejected here": {
56+
config: PluginFactoryConfig{
57+
Sys: stubLookRunnerUtil{},
58+
Logger: hclog.NewNullLogger(),
59+
},
60+
expectedErrContains: "",
61+
},
62+
"telemetry labels are optional": {
63+
config: PluginFactoryConfig{
64+
PluginName: "postgresql-database-plugin",
65+
Sys: stubLookRunnerUtil{},
66+
Logger: hclog.NewNullLogger(),
67+
Namespace: "root",
68+
MountPoint: "database/",
69+
ConnectionName: "prod-primary",
70+
},
71+
expectedErrContains: "",
72+
},
73+
}
74+
75+
for name, test := range tests {
76+
t.Run(name, func(t *testing.T) {
77+
err := test.config.validate()
78+
79+
if test.expectedErrContains == "" {
80+
if err != nil {
81+
t.Fatalf("Expected no error, but got: %s", err)
82+
}
83+
return
84+
}
85+
86+
if err == nil {
87+
t.Fatal("Expected an error, but got none")
88+
}
89+
if !strings.Contains(err.Error(), test.expectedErrContains) {
90+
t.Fatalf("Expected error mentioning %q, but got: %s", test.expectedErrContains, err)
91+
}
92+
})
93+
}
94+
}
95+
96+
// TestPluginFactoryWithConfig_InvalidConfigReturnsError asserts that an
97+
// incomplete config produces an error from the exported factory rather than a
98+
// nil dereference panic. The factory takes a struct literal, so a caller can
99+
// omit a required field and still compile.
100+
func TestPluginFactoryWithConfig_InvalidConfigReturnsError(t *testing.T) {
101+
tests := map[string]PluginFactoryConfig{
102+
"empty config": {},
103+
"missing sys": {PluginName: "postgresql-database-plugin", Logger: hclog.NewNullLogger()},
104+
"missing logger": {PluginName: "postgresql-database-plugin", Sys: stubLookRunnerUtil{}},
105+
}
106+
107+
for name, config := range tests {
108+
t.Run(name, func(t *testing.T) {
109+
// A panic here is a test failure, which is the regression this
110+
// guards against.
111+
db, err := PluginFactoryWithConfig(context.Background(), config)
112+
if err == nil {
113+
t.Fatal("Expected an error, but got none")
114+
}
115+
if db != nil {
116+
t.Fatalf("Expected a nil Database on error, but got: %#v", db)
117+
}
118+
})
119+
}
120+
}
121+
122+
// TestPluginFactoryVersion_RejectsNilInput asserts the positional wrappers
123+
// inherit the same validation, since they previously panicked on nil input too.
124+
func TestPluginFactoryVersion_RejectsNilInput(t *testing.T) {
125+
db, err := PluginFactoryVersion(context.Background(), "postgresql-database-plugin", "", nil, hclog.NewNullLogger())
126+
if err == nil {
127+
t.Fatal("Expected an error, but got none")
128+
}
129+
if db != nil {
130+
t.Fatalf("Expected a nil Database on error, but got: %#v", db)
131+
}
132+
}

0 commit comments

Comments
 (0)