Skip to content

Commit 55d5200

Browse files
Add SetDefaultHostMetadataResolverFactory (#1636)
## Changes [PR #1572](#1572) added `Config.HostMetadataResolver` so callers could override the SDK's `/.well-known/databricks-config` fetch on a per-Config basis. That covers "I have one Config and I want to wrap it." The gap: programs that construct many Configs across their command surface (e.g. the Databricks CLI) end up copying the same `cfg.HostMetadataResolver = ...` assignment at every construction site, in the CLI roughly 10 sites across 7 files plus a guardrail test to catch drift. This PR adds a package-level default consulted when a Config has no explicit resolver set. Callers set a factory once during startup; every subsequent Config gets the same resolver without per-site wiring. The Config-level field still takes precedence, so PR #1572's contract is unchanged. ### API ```go // config/host_metadata.go var DefaultHostMetadataResolverFactory func(*Config) HostMetadataResolver ``` Plain public variable, set once at init. Matches the stdlib pattern for single-default hooks: `http.DefaultClient`, `http.DefaultTransport`, `log.Default`. Callers needing per-Config or dynamic behaviour should use `Config.HostMetadataResolver` instead. ### Resolution order inside `Config.EnsureResolved` 1. If `Config.HostMetadataResolver` is set, use it. 2. Else, if `DefaultHostMetadataResolverFactory` is non-nil, invoke it with the resolving Config and use its return value. If it returns nil, fall through. 3. Else, SDK's default HTTP fetch (unchanged behavior for all existing callers). ## How the Databricks CLI will use this The canonical Go idiom for "library A registers itself with library B" is a blank import that triggers an `init()` in A. This is how `database/sql` drivers (`_ "github.com/lib/pq"`), image codecs (`_ "image/png"`), and encoding formats register themselves. After this PR lands and is bumped into the CLI, [CLI PR #5011](databricks/cli#5011) will collapse from ~10 wired-in `hostmetadata.Attach(cfg)` calls + a guardrail test down to two small pieces: **`repos/cli/libs/hostmetadata/resolver.go`** — set the caching factory at package init: ```go func init() { config.DefaultHostMetadataResolverFactory = func(cfg *config.Config) config.HostMetadataResolver { return NewResolver(cfg.DefaultHostMetadataResolver()) } } ``` **`repos/cli/cmd/databricks/main.go`** — one blank import to pull the package in at startup: ```go import ( // Registers a disk-cached HostMetadataResolver with the SDK so every // Config the CLI constructs reuses the cached /.well-known lookup. _ "github.com/databricks/cli/libs/hostmetadata" ) ``` That's the full integration. Every Config the CLI creates, now and in the future from any new command a developer adds, automatically gets caching. No per-site `Attach` call to remember, no guardrail test to maintain, no new developer ever has to learn this mechanism exists to benefit from it. ### Experimental Marked experimental to match the existing `HostMetadataResolver` field. No default behavior change for callers that never set `DefaultHostMetadataResolverFactory`. ## Tests Three new tests in `config/config_test.go`, each using a small `withDefaultHostMetadataResolverFactory(t, factory)` helper that captures and restores the prior value, so tests never clobber each other via the package-level default: - Factory is invoked when Config has no resolver; back-fill works end-to-end. - Config-level resolver takes precedence (factory not consulted). - Factory returning nil falls through to the SDK's HTTP fetch. - `make fmt test lint` clean - `go test ./config/... -count=1 -race` clean Signed-off-by: simon <simon.faltum@databricks.com> --------- Signed-off-by: simon <simon.faltum@databricks.com> Co-authored-by: Renaud Hartert <renaud.hartert@databricks.com>
1 parent f171c1f commit 55d5200

4 files changed

Lines changed: 102 additions & 2 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
### New Features and Improvements
88

9+
* Add `config.DefaultHostMetadataResolverFactory`: a package-level variable consulted when `Config.HostMetadataResolver` is unset. Lets programs install a shared resolver (e.g. a caching one) once from an `init()` block (typically in a blank-imported package) instead of wiring per-Config. Experimental.
10+
911
### Bug Fixes
1012

1113
* Add `X-Databricks-Org-Id` header to `Workspace.Download()` and `Workspace.Upload()` for SPOG host compatibility.

config/config.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -701,10 +701,17 @@ func (c *Config) resolveHostMetadata(ctx context.Context) {
701701
return
702702
}
703703

704+
resolver := c.HostMetadataResolver
705+
if resolver == nil {
706+
if factory := DefaultHostMetadataResolverFactory; factory != nil {
707+
resolver = factory(c)
708+
}
709+
}
710+
704711
var meta *HostMetadata
705712
var err error
706-
if c.HostMetadataResolver != nil {
707-
meta, err = c.HostMetadataResolver(ctx, c.CanonicalHostName())
713+
if resolver != nil {
714+
meta, err = resolver(ctx, c.CanonicalHostName())
708715
} else {
709716
meta, err = getHostMetadata(ctx, c.CanonicalHostName(), c.refreshClient)
710717
}

config/config_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/http"
77
"sync"
8+
"sync/atomic"
89
"testing"
910

1011
"github.com/databricks/databricks-sdk-go/common/environment"
@@ -1169,3 +1170,79 @@ func TestConfig_ResolveHostMetadata_HostTypes(t *testing.T) {
11691170
})
11701171
}
11711172
}
1173+
1174+
// withDefaultHostMetadataResolverFactory installs factory for the duration of
1175+
// the current test, restoring whatever was previously set on cleanup.
1176+
// Capture/set/restore are not atomic — do not use with t.Parallel across
1177+
// multiple tests that touch the package-level default.
1178+
func withDefaultHostMetadataResolverFactory(t *testing.T, factory func(*Config) HostMetadataResolver) {
1179+
t.Helper()
1180+
prev := DefaultHostMetadataResolverFactory
1181+
DefaultHostMetadataResolverFactory = factory
1182+
t.Cleanup(func() { DefaultHostMetadataResolverFactory = prev })
1183+
}
1184+
1185+
func TestDefaultHostMetadataResolverFactory_UsedWhenConfigHasNoResolver(t *testing.T) {
1186+
var factoryCalls atomic.Int32
1187+
withDefaultHostMetadataResolverFactory(t, func(c *Config) HostMetadataResolver {
1188+
factoryCalls.Add(1)
1189+
return func(ctx context.Context, host string) (*HostMetadata, error) {
1190+
return &HostMetadata{AccountID: testHMAccountID, WorkspaceID: testHMWorkspaceID}, nil
1191+
}
1192+
})
1193+
1194+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
1195+
cfg := &Config{Host: testHMHost, Loaders: []Loader{noopLoader}}
1196+
require.NoError(t, cfg.EnsureResolved())
1197+
1198+
assert.Equal(t, int32(1), factoryCalls.Load(), "factory must be invoked exactly once per resolve")
1199+
assert.Equal(t, testHMAccountID, cfg.AccountID)
1200+
assert.Equal(t, testHMWorkspaceID, cfg.WorkspaceID)
1201+
}
1202+
1203+
func TestDefaultHostMetadataResolverFactory_PerConfigResolverTakesPrecedence(t *testing.T) {
1204+
var factoryCalls atomic.Int32
1205+
withDefaultHostMetadataResolverFactory(t, func(c *Config) HostMetadataResolver {
1206+
factoryCalls.Add(1)
1207+
return func(ctx context.Context, host string) (*HostMetadata, error) {
1208+
return &HostMetadata{AccountID: "factory-account"}, nil
1209+
}
1210+
})
1211+
1212+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
1213+
cfg := &Config{
1214+
Host: testHMHost,
1215+
Loaders: []Loader{noopLoader},
1216+
HostMetadataResolver: func(ctx context.Context, host string) (*HostMetadata, error) {
1217+
return &HostMetadata{AccountID: testHMAccountID}, nil
1218+
},
1219+
}
1220+
require.NoError(t, cfg.EnsureResolved())
1221+
1222+
assert.Equal(t, int32(0), factoryCalls.Load(), "factory must not be consulted when Config has its own resolver")
1223+
assert.Equal(t, testHMAccountID, cfg.AccountID)
1224+
}
1225+
1226+
func TestDefaultHostMetadataResolverFactory_NilResolverFromFactoryFallsThroughToHTTP(t *testing.T) {
1227+
withDefaultHostMetadataResolverFactory(t, func(c *Config) HostMetadataResolver {
1228+
return nil
1229+
})
1230+
1231+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
1232+
cfg := &Config{
1233+
Host: testHMHost,
1234+
Loaders: []Loader{noopLoader},
1235+
HTTPTransport: fixtures.SliceTransport{
1236+
{
1237+
Method: "GET",
1238+
Resource: "/.well-known/databricks-config",
1239+
ReuseRequest: true,
1240+
Status: 200,
1241+
Response: `{"oidc_endpoint": "` + testHMHost + `/oidc", "account_id": "` + testHMAccountID + `"}`,
1242+
},
1243+
},
1244+
}
1245+
require.NoError(t, cfg.EnsureResolved())
1246+
1247+
assert.Equal(t, testHMAccountID, cfg.AccountID)
1248+
}

config/host_metadata.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ type HostMetadata struct {
3838
// This allows callers to provide cached metadata without the SDK making an HTTP call.
3939
type HostMetadataResolver func(ctx context.Context, host string) (*HostMetadata, error)
4040

41+
// DefaultHostMetadataResolverFactory is consulted by [Config.EnsureResolved]
42+
// when [Config.HostMetadataResolver] is nil. When set, the factory is invoked
43+
// with the resolving Config and must return the resolver to use for that
44+
// Config (or nil to fall through to the SDK's default HTTP fetch).
45+
//
46+
// Intended for programs that want a single hook to install a caching or
47+
// otherwise-customised resolver across every Config they construct, without
48+
// per-site wiring. Set once from an init() block in a package that is
49+
// blank-imported by the main binary. Callers needing a per-Config resolver
50+
// should use [Config.HostMetadataResolver] instead.
51+
//
52+
// Experimental: subject to change.
53+
var DefaultHostMetadataResolverFactory func(*Config) HostMetadataResolver
54+
4155
// getHostMetadata fetches the raw Databricks well-known configuration from
4256
// {host}/.well-known/databricks-config. The returned HostMetadata contains
4357
// raw values with no substitution (e.g., {account_id} placeholders are left

0 commit comments

Comments
 (0)