Skip to content

Commit 878f555

Browse files
authored
Added auth_type for multi-cloud states (#1000)
Added optional `auth_type` provider conf to enforce specific auth type to be used in very rare cases, where a single Terraform state manages Databricks workspaces on more than one cloud and `More than one authorization method configured` error is a false positive. Valid values are `pat`, `basic`, `azure-client-secret`, `azure-msi`, `azure-cli`, and `databricks-cli`.
1 parent ef33ea8 commit 878f555

File tree

8 files changed

+163
-101
lines changed

8 files changed

+163
-101
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
## 0.4.2
44

55
* Added `DBC` format support for `databricks_notebook` ([#989](https://github.com/databrickslabs/terraform-provider-databricks/pull/989)).
6+
* Added optional `auth_type` provider conf to enforce specific auth type to be used in very rare cases, where a single Terraform state manages Databricks workspaces on more than one cloud and `More than one authorization method configured` error is a false positive. Valid values are `pat`, `basic`, `azure-client-secret`, `azure-msi`, `azure-cli`, and `databricks-cli`.
7+
* Added automated documentation formatting with `make fmt-docs`, so that all HCL examples look consistent.
8+
* Increased codebase unit test coverage to 91% to improve stability.
9+
10+
Updated dependency versions:
11+
12+
* Bump github.com/hashicorp/terraform-plugin-sdk/v2 from 2.10.0 to 2.10.1
613

714
## 0.4.1
815

common/azure_auth_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ func TestGetJWTProperty_Authenticate_Fail(t *testing.T) {
374374
Host: "https://adb-1232.azuredatabricks.net",
375375
}
376376
_, err := client.GetAzureJwtProperty("tid")
377-
require.EqualError(t, err, "cannot configure Azure CLI auth: "+
377+
require.EqualError(t, err, "cannot configure azure-cli auth: "+
378378
"Invoking Azure CLI failed with the following error: "+
379379
"This is just a failing script.\n. "+
380380
"Please check https://registry.terraform.io/providers/"+

common/client.go

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ type DatabricksClient struct {
6161
AzureTenantID string `name:"azure_tenant_id" env:"ARM_TENANT_ID" auth:"azure"`
6262
AzurermEnvironment string `name:"azure_environment" env:"ARM_ENVIRONMENT"`
6363

64+
// When multiple auth attributes are available in the environment, use the auth type
65+
// specified by this argument. This argument also holds currently selected auth.
66+
AuthType string `name:"auth_type" auth:"-"`
67+
6468
// Azure Enviroment endpoints
6569
AzureEnvironment *azure.Environment
6670

@@ -233,28 +237,39 @@ func (c *DatabricksClient) Authenticate(ctx context.Context) error {
233237
name string
234238
}
235239
providers := []auth{
236-
{c.configureWithDirectParams, "direct"},
237-
{c.configureWithAzureClientSecret, "Azure Service Principal"},
238-
{c.configureWithAzureManagedIdentity, "Azure MSI"},
239-
{c.configureWithAzureCLI, "Azure CLI"},
240-
{c.configureWithGoogleForAccountsAPI, "Databricks Account on GCP"},
241-
{c.configureWithGoogleForWorkspace, "Databricks on GCP"},
242-
{c.configureWithDatabricksCfg, "Databricks CLI"},
240+
{c.configureWithPat, "pat"},
241+
{c.configureWithBasicAuth, "basic"},
242+
{c.configureWithAzureClientSecret, "azure-client-secret"},
243+
{c.configureWithAzureManagedIdentity, "azure-msi"},
244+
{c.configureWithAzureCLI, "azure-cli"},
245+
{c.configureWithGoogleForAccountsAPI, "google-accounts"},
246+
{c.configureWithGoogleForWorkspace, "google-workspace"},
247+
{c.configureWithDatabricksCfg, "databricks-cli"},
243248
}
244249
// try configuring authentication with different methods
245250
for _, auth := range providers {
251+
if c.AuthType != "" && auth.name != c.AuthType {
252+
// ignore other auth types if one is explicitly enforced
253+
log.Printf("[INFO] Ignoring %s auth, because %s is preferred", auth.name, c.AuthType)
254+
continue
255+
}
246256
authorizer, err := auth.configure(ctx)
247257
if err != nil {
248258
return c.niceAuthError(fmt.Sprintf("cannot configure %s auth: %s", auth.name, err))
249259
}
250260
if authorizer == nil {
251261
continue
252262
}
263+
// even though this may complain about clear text loggin, passwords are replaced with `***`
253264
log.Printf("[INFO] Configured %s auth: %s", auth.name, c.configDebugString()) // lgtm[go/clear-text-logging]
254265
c.authVisitor = authorizer
266+
c.AuthType = auth.name
255267
c.fixHost()
256268
return nil
257269
}
270+
if c.AuthType != "" {
271+
return c.niceAuthError(fmt.Sprintf("cannot configure %s auth", c.AuthType))
272+
}
258273
if c.Host == "" && IsData.GetOrUnknown(ctx) == "yes" {
259274
return c.niceAuthError("workspace is most likely not created yet, because the `host` " +
260275
"is empty. Please add `depends_on = [databricks_mws_workspaces.this]` or " +
@@ -310,25 +325,21 @@ func (c *DatabricksClient) fixHost() {
310325
}
311326
}
312327

313-
func (c *DatabricksClient) configureWithDirectParams(ctx context.Context) (func(*http.Request) error, error) {
314-
authType := "Bearer"
315-
var needsHostBecause string
316-
if c.Username != "" && c.Password != "" {
317-
authType = "Basic"
318-
needsHostBecause = "basic_auth"
319-
c.Token = c.encodeBasicAuth(c.Username, c.Password)
320-
log.Printf("[INFO] Using basic auth for user '%s'", c.Username)
321-
} else if c.Token != "" {
322-
needsHostBecause = "token"
323-
}
324-
if needsHostBecause != "" && c.Host == "" {
325-
return nil, fmt.Errorf("host is empty, but is required by %s", needsHostBecause)
328+
func (c *DatabricksClient) configureWithPat(ctx context.Context) (func(*http.Request) error, error) {
329+
if !(c.Token != "" && c.Host != "") {
330+
return nil, nil
326331
}
327-
if c.Token == "" || c.Host == "" {
332+
log.Printf("[INFO] Using directly configured PAT authentication")
333+
return c.authorizer("Bearer", c.Token), nil
334+
}
335+
336+
func (c *DatabricksClient) configureWithBasicAuth(ctx context.Context) (func(*http.Request) error, error) {
337+
if !(c.Username != "" && c.Password != "" && c.Host != "") {
328338
return nil, nil
329339
}
330-
log.Printf("[INFO] Using directly configured host+%s authentication", needsHostBecause)
331-
return c.authorizer(authType, c.Token), nil
340+
b64 := c.encodeBasicAuth(c.Username, c.Password)
341+
log.Printf("[INFO] Using directly configured basic authentication")
342+
return c.authorizer("Basic", b64), nil
332343
}
333344

334345
func (c *DatabricksClient) configureWithDatabricksCfg(ctx context.Context) (func(r *http.Request) error, error) {
@@ -342,8 +353,8 @@ func (c *DatabricksClient) configureWithDatabricksCfg(ctx context.Context) (func
342353
}
343354
_, err = os.Stat(configFile)
344355
if os.IsNotExist(err) {
345-
log.Printf("[INFO] ~/.databrickscfg not found on current host")
346356
// early return for non-configured machines
357+
log.Printf("[INFO] %s not found on current host", configFile)
347358
return nil, nil
348359
}
349360
cfg, err := ini.Load(configFile)

common/client_test.go

Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@ package common
22

33
import (
44
"context"
5+
"log"
56
"os"
7+
"reflect"
68
"strings"
79
"testing"
810

911
"github.com/stretchr/testify/assert"
1012
)
1113

12-
func AssertErrorStartsWith(t *testing.T, err error, message string) bool {
13-
return assert.True(t, strings.HasPrefix(err.Error(), message), err.Error())
14-
}
15-
1614
func configureAndAuthenticate(dc *DatabricksClient) (*DatabricksClient, error) {
1715
err := dc.Configure()
1816
if err != nil {
@@ -21,22 +19,29 @@ func configureAndAuthenticate(dc *DatabricksClient) (*DatabricksClient, error) {
2119
return dc, dc.Authenticate(context.Background())
2220
}
2321

22+
func failsToAuthenticateWith(t *testing.T, dc *DatabricksClient, message string) {
23+
_, err := configureAndAuthenticate(dc)
24+
if dc.AuthType != "" {
25+
log.Printf("[INFO] Auth is: %s", dc.AuthType)
26+
}
27+
if assert.NotNil(t, err, "expected to have error: %s", message) {
28+
assert.True(t, strings.HasPrefix(err.Error(), message), err.Error())
29+
}
30+
}
31+
2432
func TestDatabricksClientConfigure_Nothing(t *testing.T) {
2533
defer CleanupEnvironment()()
2634
os.Setenv("PATH", "testdata:/bin")
27-
28-
_, err := configureAndAuthenticate(&DatabricksClient{})
29-
AssertErrorStartsWith(t, err, "authentication is not configured for provider")
35+
failsToAuthenticateWith(t, &DatabricksClient{},
36+
"authentication is not configured for provider")
3037
}
3138

3239
func TestDatabricksClientConfigure_BasicAuth_NoHost(t *testing.T) {
33-
dc, err := configureAndAuthenticate(&DatabricksClient{
40+
defer CleanupEnvironment()()
41+
failsToAuthenticateWith(t, &DatabricksClient{
3442
Username: "foo",
3543
Password: "bar",
36-
})
37-
38-
AssertErrorStartsWith(t, err, "cannot configure direct auth: host is empty, but is required by basic_auth")
39-
assert.Equal(t, "Zm9vOmJhcg==", dc.Token)
44+
}, "authentication is not configured for provider.")
4045
}
4146

4247
func TestDatabricksClientConfigure_BasicAuth(t *testing.T) {
@@ -45,103 +50,101 @@ func TestDatabricksClientConfigure_BasicAuth(t *testing.T) {
4550
Username: "foo",
4651
Password: "bar",
4752
})
48-
49-
assert.Equal(t, "Zm9vOmJhcg==", dc.Token)
5053
assert.NoError(t, err)
54+
assert.Equal(t, "basic", dc.AuthType)
5155
}
5256

5357
func TestDatabricksClientConfigure_HostWithoutScheme(t *testing.T) {
5458
dc, err := configureAndAuthenticate(&DatabricksClient{
5559
Host: "localhost:443",
5660
Token: "...",
5761
})
58-
62+
assert.NoError(t, err)
63+
assert.Equal(t, "pat", dc.AuthType)
5964
assert.Equal(t, "...", dc.Token)
6065
assert.Equal(t, "https://localhost:443", dc.Host)
61-
assert.NoError(t, err)
6266
}
6367

6468
func TestDatabricksClientConfigure_Token_NoHost(t *testing.T) {
65-
dc, err := configureAndAuthenticate(&DatabricksClient{
69+
defer CleanupEnvironment()()
70+
failsToAuthenticateWith(t, &DatabricksClient{
6671
Token: "dapi345678",
67-
})
68-
69-
AssertErrorStartsWith(t, err, "cannot configure direct auth: host is empty, but is required by token")
70-
assert.Equal(t, "dapi345678", dc.Token)
72+
}, "authentication is not configured for provider.")
7173
}
7274

7375
func TestDatabricksClientConfigure_HostTokensTakePrecedence(t *testing.T) {
74-
_, err := configureAndAuthenticate(&DatabricksClient{
76+
dc, err := configureAndAuthenticate(&DatabricksClient{
7577
Host: "foo",
7678
Token: "connfigured",
7779
ConfigFile: "testdata/.databrickscfg",
7880
})
7981
assert.NoError(t, err)
82+
assert.Equal(t, "pat", dc.AuthType)
8083
}
8184

8285
func TestDatabricksClientConfigure_BasicAuthTakePrecedence(t *testing.T) {
8386
dc, err := configureAndAuthenticate(&DatabricksClient{
8487
Host: "foo",
85-
Token: "connfigured",
88+
Token: "configured",
8689
Username: "foo",
8790
Password: "bar",
8891
ConfigFile: "testdata/.databrickscfg",
8992
})
9093
assert.NoError(t, err)
91-
assert.Equal(t, "Zm9vOmJhcg==", dc.Token)
94+
assert.Equal(t, "pat", dc.AuthType)
95+
assert.Equal(t, "configured", dc.Token)
9296
}
9397

9498
func TestDatabricksClientConfigure_ConfigRead(t *testing.T) {
9599
dc, err := configureAndAuthenticate(&DatabricksClient{
96100
ConfigFile: "testdata/.databrickscfg",
97101
})
98102
assert.NoError(t, err)
103+
assert.Equal(t, "databricks-cli", dc.AuthType)
99104
assert.Equal(t, "PT0+IC9kZXYvdXJhbmRvbSA8PT0KYFZ", dc.Token)
100105
}
101106

102107
func TestDatabricksClientConfigure_NoHostGivesError(t *testing.T) {
103-
_, err := configureAndAuthenticate(&DatabricksClient{
108+
failsToAuthenticateWith(t, &DatabricksClient{
104109
Token: "connfigured",
105110
ConfigFile: "testdata/.databrickscfg",
106111
Profile: "nohost",
107-
})
108-
assert.Error(t, err)
112+
}, "cannot configure databricks-cli auth: config file "+
113+
"testdata/.databrickscfg is corrupt: cannot find host in nohost profile.")
109114
}
110115

111116
func TestDatabricksClientConfigure_NoTokenGivesError(t *testing.T) {
112-
_, err := configureAndAuthenticate(&DatabricksClient{
117+
failsToAuthenticateWith(t, &DatabricksClient{
113118
Token: "connfigured",
114119
ConfigFile: "testdata/.databrickscfg",
115120
Profile: "notoken",
116-
})
117-
assert.Error(t, err)
121+
}, "cannot configure databricks-cli auth: config file "+
122+
"testdata/.databrickscfg is corrupt: cannot find token in notoken profile.")
118123
}
119124

120125
func TestDatabricksClientConfigure_InvalidProfileGivesError(t *testing.T) {
121-
_, err := configureAndAuthenticate(&DatabricksClient{
126+
failsToAuthenticateWith(t, &DatabricksClient{
122127
Token: "connfigured",
123128
ConfigFile: "testdata/.databrickscfg",
124129
Profile: "invalidhost",
125-
})
126-
assert.Error(t, err)
130+
}, "cannot configure databricks-cli auth: testdata/.databrickscfg "+
131+
"has no invalidhost profile configured")
127132
}
128133

129134
func TestDatabricksClientConfigure_MissingFile(t *testing.T) {
130-
_, err := configureAndAuthenticate(&DatabricksClient{
135+
failsToAuthenticateWith(t, &DatabricksClient{
131136
Token: "connfigured",
132137
ConfigFile: "testdata/.invalid file",
133138
Profile: "invalidhost",
134-
})
135-
assert.Error(t, err)
139+
}, "authentication is not configured for provider.")
136140
}
137141

138142
func TestDatabricksClientConfigure_InvalidConfigFilePath(t *testing.T) {
139-
_, err := configureAndAuthenticate(&DatabricksClient{
143+
failsToAuthenticateWith(t, &DatabricksClient{
140144
Token: "connfigured",
141-
ConfigFile: "testdata/policy01.json",
145+
ConfigFile: "testdata/az",
142146
Profile: "invalidhost",
143-
})
144-
assert.Error(t, err)
147+
}, "cannot configure databricks-cli auth: cannot parse config file")
145148
}
146149

147150
func TestDatabricksClient_FormatURL(t *testing.T) {
@@ -151,7 +154,7 @@ func TestDatabricksClient_FormatURL(t *testing.T) {
151154

152155
func TestClientAttributes(t *testing.T) {
153156
ca := ClientAttributes()
154-
assert.Len(t, ca, 19)
157+
assert.Len(t, ca, 20)
155158
}
156159

157160
func TestDatabricksClient_Authenticate(t *testing.T) {
@@ -223,7 +226,10 @@ func TestClientForHostAuthError(t *testing.T) {
223226
Profile: "notoken",
224227
}
225228
_, err := c.ClientForHost(context.Background(), "https://e2-workspace.cloud.databricks.com/")
226-
AssertErrorStartsWith(t, err, "cannot authenticate parent client: cannot configure direct auth")
229+
if assert.NotNil(t, err) {
230+
assert.True(t, strings.HasPrefix(err.Error(),
231+
"cannot authenticate parent client: cannot configure databricks-cli auth"), err.Error())
232+
}
227233
}
228234

229235
func TestDatabricksCliCouldNotFindHomeDir(t *testing.T) {
@@ -237,7 +243,10 @@ func TestDatabricksCliCouldNotParseIni(t *testing.T) {
237243
_, err := (&DatabricksClient{
238244
ConfigFile: "testdata/az",
239245
}).configureWithDatabricksCfg(context.Background())
240-
AssertErrorStartsWith(t, err, "cannot parse config file: key-value delimiter not found")
246+
if assert.NotNil(t, err) {
247+
assert.True(t, strings.HasPrefix(err.Error(),
248+
"cannot parse config file: key-value delimiter not found"), err.Error())
249+
}
241250
}
242251

243252
func TestDatabricksCliWrongProfile(t *testing.T) {
@@ -274,3 +283,17 @@ func TestDatabricksBasicAuth(t *testing.T) {
274283
assert.Equal(t, "abc", c.Username)
275284
assert.Equal(t, "bcd", c.Password)
276285
}
286+
287+
func TestDatabricksClientConfigure_NonsenseAuth(t *testing.T) {
288+
defer CleanupEnvironment()()
289+
failsToAuthenticateWith(t, &DatabricksClient{
290+
AuthType: "nonsense",
291+
}, "cannot configure nonsense auth.")
292+
}
293+
294+
func TestConfigAttributeSetNonsense(t *testing.T) {
295+
err := (&ConfigAttribute{
296+
Kind: reflect.Chan,
297+
}).Set(&DatabricksClient{}, 1)
298+
assert.EqualError(t, err, "cannot set of unknown type Chan")
299+
}

common/resource.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func makeEmptyBlockSuppressFunc(name string) func(k, old, new string, d *schema.
161161
re := MustCompileKeyRE(name)
162162
return func(k, old, new string, d *schema.ResourceData) bool {
163163
if re.Match([]byte(name)) && old == "1" && new == "0" {
164-
log.Printf("[DEBUG] Suppressing diff for name=%s k=%#v patform=%#v config=%#v", name, k, old, new)
164+
log.Printf("[DEBUG] Suppressing diff for name=%s k=%#v platform=%#v config=%#v", name, k, old, new)
165165
return true
166166
}
167167
return false

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ Alternatively, you can provide this value as an environment variable `DATABRICKS
176176
* `profile` - (optional) Connection profile specified within ~/.databrickscfg. Please check [connection profiles section](https://docs.databricks.com/dev-tools/cli/index.html#connection-profiles) for more details. This field defaults to
177177
`DEFAULT`.
178178
* `account_id` - (optional) Account Id that could be found in the bottom left corner of [Accounts Console](https://accounts.cloud.databricks.com/). Alternatively, you can provide this value as an environment variable `DATABRICKS_ACCOUNT_ID`. Only has effect when `host = "https://accounts.cloud.databricks.com/"` and currently used to provision account admins via [databricks_user](resources/user.md). In the future releases of the provider this property will also be used specify account for `databricks_mws_*` resources as well.
179+
* `auth_type` - (optional) enforce specific auth type to be used in very rare cases, where a single Terraform state manages Databricks workspaces on more than one cloud and `More than one authorization method configured` error is a false positive. Valid values are `pat`, `basic`, `azure-client-secret`, `azure-msi`, `azure-cli`, and `databricks-cli`.
179180

180181
## Special configurations for Azure
181182

provider/provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func configureDatabricksClient(ctx context.Context, d *schema.ResourceData) (int
173173
authorizationMethodsUsed = append(authorizationMethodsUsed, name)
174174
}
175175
}
176-
if len(authorizationMethodsUsed) > 1 {
176+
if pc.AuthType == "" && len(authorizationMethodsUsed) > 1 {
177177
sort.Strings(authorizationMethodsUsed)
178178
return nil, diag.Errorf("More than one authorization method configured: %s",
179179
strings.Join(authorizationMethodsUsed, " and "))

0 commit comments

Comments
 (0)