From a99e58637bb4fdce1cdbbbe226ad00dfe23f1fc6 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Sun, 3 May 2026 13:25:59 -0700 Subject: [PATCH] direct: reject variable references in resource map keys Variable references in resource map keys (e.g. resources.schemas.${var.schema}) are not resolvable at plan time and the literal '${var.foo}' string is re-parsed via dyn.NewPathFromString, which splits on '.' and produces a non-existent path. GetResourceConfig then returns (nil, nil), and the nil *resources.Schema is dereferenced inside PrepareState, panicking with a nil pointer. Detect variable references in the resource key during the plan walk and return an actionable error. Fixes #5098. Signed-off-by: SAY-5 --- bundle/direct/bundle_plan.go | 11 +++++++++++ bundle/direct/bundle_plan_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 3fab4c3f4ff..e5e4123011c 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -763,6 +763,17 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root pat, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { s := p.String() + // Resource keys are used as identifiers in plan paths and looked up + // via dyn.NewPathFromString, which splits on '.'. A variable reference + // in a resource key (e.g. resources.schemas.${var.schema}) cannot be + // resolved at this stage and would be split into multiple path + // components, causing a nil dereference downstream in PrepareState. + // Reject early with an actionable error. + for _, c := range p { + if dynvar.ContainsVariableReference(c.Key()) { + return v, fmt.Errorf("resource key %q cannot contain variable references; use a literal key and parameterize fields like 'name' instead", s) + } + } resourceType := config.GetResourceTypeFromKey(s) if resourceType == "" { return v, fmt.Errorf("cannot parse resource key: %q", s) diff --git a/bundle/direct/bundle_plan_test.go b/bundle/direct/bundle_plan_test.go index ccfb7cb517f..08cac223634 100644 --- a/bundle/direct/bundle_plan_test.go +++ b/bundle/direct/bundle_plan_test.go @@ -3,8 +3,13 @@ package direct import ( "testing" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/direct/dresources" + "github.com/databricks/cli/bundle/direct/dstate" "github.com/databricks/cli/libs/dyn" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDynPathToStructPath(t *testing.T) { @@ -35,3 +40,28 @@ func TestDynPathToStructPath(t *testing.T) { assert.Equal(t, tc.expected, node.String()) } } + +// TestMakePlanRejectsVariableInResourceKey verifies that a variable reference +// in a resource map key (e.g. resources.schemas.${var.schema}) is rejected +// with a clear error rather than panicking with a nil pointer dereference +// inside PrepareState. Regression test for issue #5098. +func TestMakePlanRejectsVariableInResourceKey(t *testing.T) { + rootCfg := config.Root{ + Resources: config.Resources{ + Schemas: map[string]*resources.Schema{ + "${var.schema}": {}, + }, + }, + } + require.NoError(t, rootCfg.Mutate(func(v dyn.Value) (dyn.Value, error) { return v, nil })) + + db := dstate.NewDatabase("", 0) + adapters, err := dresources.InitAll(nil) + require.NoError(t, err) + b := &DeploymentBundle{Adapters: adapters} + + _, err = b.makePlan(t.Context(), &rootCfg, &db) + require.Error(t, err) + assert.Contains(t, err.Error(), "${var.schema}") + assert.Contains(t, err.Error(), "cannot contain variable references") +}