From 1c8e499747165912b649ad26bd043eb13a55b32f Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Wed, 8 Apr 2026 13:51:34 +0200 Subject: [PATCH 1/3] Use filter option with service principal API when lookup variable used --- .../variable/resolve_service_principal.go | 19 +++++++++++-- .../resolve_service_principal_test.go | 28 ++++++++++++------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/bundle/config/variable/resolve_service_principal.go b/bundle/config/variable/resolve_service_principal.go index c7b299ccaa..480a92873d 100644 --- a/bundle/config/variable/resolve_service_principal.go +++ b/bundle/config/variable/resolve_service_principal.go @@ -2,8 +2,11 @@ package variable import ( "context" + "fmt" "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/listing" + "github.com/databricks/databricks-sdk-go/service/iam" ) type resolveServicePrincipal struct { @@ -12,11 +15,23 @@ type resolveServicePrincipal struct { func (l resolveServicePrincipal) Resolve(ctx context.Context, w *databricks.WorkspaceClient) (string, error) { //nolint:staticcheck // this API is deprecated but we still need use it as there is no replacement yet. - entity, err := w.ServicePrincipals.GetByDisplayName(ctx, l.name) + it := w.ServicePrincipalsV2.List(ctx, iam.ListServicePrincipalsRequest{ + Filter: fmt.Sprintf("display_name == '%s'", l.name), + }) + + servicePrincipals, err := listing.ToSliceN(ctx, it, 1) if err != nil { return "", err } - return entity.ApplicationId, nil + if len(servicePrincipals) == 0 { + return "", fmt.Errorf("service principal named %q does not exist", l.name) + } + + if len(servicePrincipals) > 1 { + return "", fmt.Errorf("multiple service principals found with display name %q", l.name) + } + + return servicePrincipals[0].ApplicationId, nil } func (l resolveServicePrincipal) String() string { diff --git a/bundle/config/variable/resolve_service_principal_test.go b/bundle/config/variable/resolve_service_principal_test.go index 0d062f282b..50ee342812 100644 --- a/bundle/config/variable/resolve_service_principal_test.go +++ b/bundle/config/variable/resolve_service_principal_test.go @@ -3,8 +3,8 @@ package variable import ( "testing" - "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/experimental/mocks" + "github.com/databricks/databricks-sdk-go/listing" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -14,12 +14,17 @@ import ( func TestResolveServicePrincipal_ResolveSuccess(t *testing.T) { m := mocks.NewMockWorkspaceClient(t) - api := m.GetMockServicePrincipalsAPI() - api.EXPECT(). - GetByDisplayName(mock.Anything, "service-principal"). - Return(&iam.ServicePrincipal{ + api := m.GetMockServicePrincipalsV2API() + iterator := listing.SliceIterator[iam.ServicePrincipal]([]iam.ServicePrincipal{ + { ApplicationId: "5678", - }, nil) + }, + }) + api.EXPECT(). + List(mock.Anything, iam.ListServicePrincipalsRequest{ + Filter: "display_name == 'service-principal'", + }). + Return(&iterator) ctx := t.Context() l := resolveServicePrincipal{name: "service-principal"} @@ -31,15 +36,18 @@ func TestResolveServicePrincipal_ResolveSuccess(t *testing.T) { func TestResolveServicePrincipal_ResolveNotFound(t *testing.T) { m := mocks.NewMockWorkspaceClient(t) - api := m.GetMockServicePrincipalsAPI() + api := m.GetMockServicePrincipalsV2API() + iterator := listing.SliceIterator[iam.ServicePrincipal]([]iam.ServicePrincipal{}) api.EXPECT(). - GetByDisplayName(mock.Anything, "service-principal"). - Return(nil, &apierr.APIError{StatusCode: 404}) + List(mock.Anything, iam.ListServicePrincipalsRequest{ + Filter: "display_name == 'service-principal'", + }). + Return(&iterator) ctx := t.Context() l := resolveServicePrincipal{name: "service-principal"} _, err := l.Resolve(ctx, m.WorkspaceClient) - require.ErrorIs(t, err, apierr.ErrNotFound) + require.ErrorContains(t, err, "service principal named \"service-principal\" does not exist") } func TestResolveServicePrincipal_String(t *testing.T) { From d09cb1737fc026b153024cc9571e023b19282acf Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Wed, 8 Apr 2026 15:52:59 +0200 Subject: [PATCH 2/3] fix tests --- .../mutator/resolve_lookup_variables_test.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/bundle/config/mutator/resolve_lookup_variables_test.go b/bundle/config/mutator/resolve_lookup_variables_test.go index 322d03e7d0..d517efb281 100644 --- a/bundle/config/mutator/resolve_lookup_variables_test.go +++ b/bundle/config/mutator/resolve_lookup_variables_test.go @@ -1,6 +1,7 @@ package mutator import ( + "fmt" "testing" "github.com/databricks/cli/bundle" @@ -11,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/databricks/databricks-sdk-go/experimental/mocks" + "github.com/databricks/databricks-sdk-go/listing" "github.com/databricks/databricks-sdk-go/service/compute" "github.com/databricks/databricks-sdk-go/service/iam" ) @@ -131,11 +133,18 @@ func TestResolveServicePrincipal(t *testing.T) { m := mocks.NewMockWorkspaceClient(t) b.SetWorkpaceClient(m.WorkspaceClient) - spApi := m.GetMockServicePrincipalsAPI() - spApi.EXPECT().GetByDisplayName(mock.Anything, spName).Return(&iam.ServicePrincipal{ - Id: "1234", - ApplicationId: "app-1234", - }, nil) + + api := m.GetMockServicePrincipalsV2API() + iterator := listing.SliceIterator[iam.ServicePrincipal]([]iam.ServicePrincipal{ + { + ApplicationId: "app-1234", + }, + }) + api.EXPECT(). + List(mock.Anything, iam.ListServicePrincipalsRequest{ + Filter: fmt.Sprintf("display_name == '%s'", spName), + }). + Return(&iterator) diags := bundle.Apply(t.Context(), b, ResolveLookupVariables()) require.NoError(t, diags.Error()) From fadcb1908d7141a6603b5fd75556388deb2f7cc3 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Wed, 8 Apr 2026 16:14:05 +0200 Subject: [PATCH 3/3] added cloud test + fixes --- .../issue_3039_lookup_with_ref/output.txt | 2 +- acceptance/bundle/variables/lookup/databricks.yml | 10 ++++++++++ acceptance/bundle/variables/lookup/out.test.toml | 5 +++++ acceptance/bundle/variables/lookup/output.txt | 14 ++++++++++++++ acceptance/bundle/variables/lookup/script | 1 + acceptance/bundle/variables/lookup/test.toml | 13 +++++++++++++ .../mutator/resolve_lookup_variables_test.go | 2 +- .../config/variable/resolve_service_principal.go | 2 +- .../variable/resolve_service_principal_test.go | 4 ++-- 9 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 acceptance/bundle/variables/lookup/databricks.yml create mode 100644 acceptance/bundle/variables/lookup/out.test.toml create mode 100644 acceptance/bundle/variables/lookup/output.txt create mode 100644 acceptance/bundle/variables/lookup/script create mode 100644 acceptance/bundle/variables/lookup/test.toml diff --git a/acceptance/bundle/variables/issue_3039_lookup_with_ref/output.txt b/acceptance/bundle/variables/issue_3039_lookup_with_ref/output.txt index 0f85f8131e..bcd8b131b4 100644 --- a/acceptance/bundle/variables/issue_3039_lookup_with_ref/output.txt +++ b/acceptance/bundle/variables/issue_3039_lookup_with_ref/output.txt @@ -1,4 +1,4 @@ -Error: failed to resolve service-principal: TIDALDBServAccount - usdev, err: ServicePrincipal named 'TIDALDBServAccount - usdev' does not exist +Error: failed to resolve service-principal: TIDALDBServAccount - usdev, err: service principal named "TIDALDBServAccount - usdev" does not exist Name: issue-3039 Target: personal diff --git a/acceptance/bundle/variables/lookup/databricks.yml b/acceptance/bundle/variables/lookup/databricks.yml new file mode 100644 index 0000000000..c7d1b4d47b --- /dev/null +++ b/acceptance/bundle/variables/lookup/databricks.yml @@ -0,0 +1,10 @@ +bundle: + name: test-bundle + +variables: + sp: + lookup: + service_principal: "deco-test-spn" + + result: + default: ${var.sp} diff --git a/acceptance/bundle/variables/lookup/out.test.toml b/acceptance/bundle/variables/lookup/out.test.toml new file mode 100644 index 0000000000..01ed6822af --- /dev/null +++ b/acceptance/bundle/variables/lookup/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = true + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/variables/lookup/output.txt b/acceptance/bundle/variables/lookup/output.txt new file mode 100644 index 0000000000..1cfc090441 --- /dev/null +++ b/acceptance/bundle/variables/lookup/output.txt @@ -0,0 +1,14 @@ + +>>> [CLI] bundle validate -o json +{ + "result": { + "default": "[UUID]", + "value": "[UUID]" + }, + "sp": { + "lookup": { + "service_principal": "deco-test-spn" + }, + "value": "[UUID]" + } +} diff --git a/acceptance/bundle/variables/lookup/script b/acceptance/bundle/variables/lookup/script new file mode 100644 index 0000000000..5a066dfada --- /dev/null +++ b/acceptance/bundle/variables/lookup/script @@ -0,0 +1 @@ +trace $CLI bundle validate -o json | jq '.variables' diff --git a/acceptance/bundle/variables/lookup/test.toml b/acceptance/bundle/variables/lookup/test.toml new file mode 100644 index 0000000000..485bf96ec5 --- /dev/null +++ b/acceptance/bundle/variables/lookup/test.toml @@ -0,0 +1,13 @@ +Local = true +Cloud = true + +[[Server]] +Pattern = "GET /api/2.0/preview/scim/v2/ServicePrincipals" +Response.Body = '''{ + "Resources": [ + { + "displayName": "deco-test-spn", + "applicationId": "123e4567-e89b-12d3-a456-426614174000" + } + ] +}''' diff --git a/bundle/config/mutator/resolve_lookup_variables_test.go b/bundle/config/mutator/resolve_lookup_variables_test.go index d517efb281..104a8ddd85 100644 --- a/bundle/config/mutator/resolve_lookup_variables_test.go +++ b/bundle/config/mutator/resolve_lookup_variables_test.go @@ -142,7 +142,7 @@ func TestResolveServicePrincipal(t *testing.T) { }) api.EXPECT(). List(mock.Anything, iam.ListServicePrincipalsRequest{ - Filter: fmt.Sprintf("display_name == '%s'", spName), + Filter: fmt.Sprintf("displayName eq '%s'", spName), }). Return(&iterator) diff --git a/bundle/config/variable/resolve_service_principal.go b/bundle/config/variable/resolve_service_principal.go index 480a92873d..e0ae7f879d 100644 --- a/bundle/config/variable/resolve_service_principal.go +++ b/bundle/config/variable/resolve_service_principal.go @@ -16,7 +16,7 @@ type resolveServicePrincipal struct { func (l resolveServicePrincipal) Resolve(ctx context.Context, w *databricks.WorkspaceClient) (string, error) { //nolint:staticcheck // this API is deprecated but we still need use it as there is no replacement yet. it := w.ServicePrincipalsV2.List(ctx, iam.ListServicePrincipalsRequest{ - Filter: fmt.Sprintf("display_name == '%s'", l.name), + Filter: fmt.Sprintf("displayName eq '%s'", l.name), }) servicePrincipals, err := listing.ToSliceN(ctx, it, 1) diff --git a/bundle/config/variable/resolve_service_principal_test.go b/bundle/config/variable/resolve_service_principal_test.go index 50ee342812..d146a3a9bf 100644 --- a/bundle/config/variable/resolve_service_principal_test.go +++ b/bundle/config/variable/resolve_service_principal_test.go @@ -22,7 +22,7 @@ func TestResolveServicePrincipal_ResolveSuccess(t *testing.T) { }) api.EXPECT(). List(mock.Anything, iam.ListServicePrincipalsRequest{ - Filter: "display_name == 'service-principal'", + Filter: "displayName eq 'service-principal'", }). Return(&iterator) @@ -40,7 +40,7 @@ func TestResolveServicePrincipal_ResolveNotFound(t *testing.T) { iterator := listing.SliceIterator[iam.ServicePrincipal]([]iam.ServicePrincipal{}) api.EXPECT(). List(mock.Anything, iam.ListServicePrincipalsRequest{ - Filter: "display_name == 'service-principal'", + Filter: "displayName eq 'service-principal'", }). Return(&iterator)