Skip to content

Commit 6bce373

Browse files
authored
Merge pull request #27 from databrickslabs/issue-21
Issue #21 Fix client_secret special characters
2 parents 7552f91 + 2d21363 commit 6bce373

File tree

140 files changed

+14408
-3060
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

140 files changed

+14408
-3060
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ override.tf.json
297297
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
298298
pyvenv.cfg
299299
.env
300+
.aws.env
301+
.azure.env
300302
.venv
301303
env/
302304
venv/

Makefile

+11-5
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ build: lint test fmt
3232

3333
lint:
3434
@echo "==> Linting source code with golangci-lint..."
35-
@golangci-lint run --skip-dirs-use-default --timeout 5m
35+
@golangci-lint run --skip-dirs-use-default --timeout 5m --build-tags=azure
36+
@golangci-lint run --skip-dirs-use-default --timeout 5m --build-tags=aws
3637

3738
fmt: lint
3839
@echo "==> Formatting source code with gofmt..."
@@ -61,10 +62,15 @@ vendor:
6162
@echo "==> Filling vendor folder with library code..."
6263
@go mod vendor
6364

64-
# INTEGRATION TESTING WITH TERRAFORM EXAMPLES
65-
terraform-acc: fmt build
66-
@echo "==> Running Terraform Acceptance Tests..."
67-
@TF_ACC=1 gotestsum --format short-verbose --raw-command go test -v -json -short -coverprofile=coverage.out ./...
65+
# INTEGRATION TESTING WITH AZURE
66+
terraform-acc-azure: fmt
67+
@echo "==> Running Terraform Acceptance Tests for Azure..."
68+
@CLOUD_ENV="azure" TF_ACC=1 gotestsum --format short-verbose --raw-command go test -v -json -tags=azure -short -coverprofile=coverage.out ./...
69+
70+
# INTEGRATION TESTING WITH AWS
71+
terraform-acc-aws: fmt
72+
@echo "==> Running Terraform Acceptance Tests for AWS..."
73+
@CLOUD_ENV="aws" TF_ACC=1 gotestsum --format short-verbose --raw-command go test -v -json -tags=aws -short -coverprofile=coverage.out ./...
6874

6975
terraform-setup: build
7076
@echo "==> Initializing Terraform..."

README.md

+45-5
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,51 @@ $ docker run -it -v $(pwd):/workpace -w /workpace databricks-terraform plan
147147
$ docker run -it -v $(pwd):/workpace -w /workpace databricks-terraform apply
148148
```
149149

150+
## Testing
151+
152+
* [ ] Integration tests should be run at a client level against both azure and aws to maintain sdk parity against both apis **(currently only on one cloud)**
153+
* [x] Terraform acceptance tests should be run against both aws and azure to maintain parity of provider between both cloud services **(currently only on one cloud)**
154+
155+
### Linting
156+
157+
Please use makefile for linting. If you run `golangci-lint` by itself it will fail due to different tags containing same functions.
158+
So please run `make lint` instead.
159+
160+
### Integration Testing
161+
162+
Currently Databricks supports two cloud providers `azure` and `aws` thus integration testing with the correct cloud service provider is
163+
crucial for making sure that the provider behaves as expected on all supported clouds. This type of testing separation is being managed via build tags
164+
to allow for duplicate method names and environment variables to configure clients.
165+
166+
The current integration test implementation uses `CLOUD_ENV` environment variable and can use the value of `azure` or `aws`.
167+
You can execute the acceptance with the following make commands `make terraform-acc-azure`, and `make terraform-acc-aws` for
168+
azure and aws respectively.
169+
170+
This involves bootstrapping the provider via a .env configuration file. Without these files in the root directory the tests
171+
will fail as the provider will not have a authorized token and host.
172+
173+
The configuration file for `aws` should be like the following and be named `.aws.env`:
174+
```.env
175+
DATABRICKS_HOST=<host>
176+
DATABRICKS_TOKEN=<token>
177+
```
178+
179+
The configuration file for `azure` should be like the following and be named `.azure.env`:
180+
```.env
181+
DATABRICKS_AZURE_CLIENT_ID=<enterprise app client id>
182+
DATABRICKS_AZURE_CLIENT_SECRET=<enterprise app client secret>
183+
DATABRICKS_AZURE_TENANT_ID=<azure ad tenant id>
184+
DATABRICKS_AZURE_SUBSCRIPTION_ID=<azure subscription id>
185+
DATABRICKS_AZURE_RESOURCE_GROUP=<resource group where the workspace is>
186+
AZURE_REGION=<region where the workspace is>
187+
DATABRICKS_AZURE_MANAGED_RESOURCE_GROUP=<azure databricks managed resource group for workspace>
188+
DATABRICKS_AZURE_WORKSPACE_NAME=<azure databricks workspace name>
189+
```
190+
191+
Note that azure integration tests will use service principal based auth. Even though it is using a service principal,
192+
it will still be generating a personal access token to perform creation of resources.
193+
194+
150195
## Project Components
151196

152197
### Databricks Terraform Provider Resources State
@@ -192,11 +237,6 @@ $ docker run -it -v $(pwd):/workpace -w /workpace databricks-terraform apply
192237
| databricks_table | :white_large_square: | :white_large_square: | :white_large_square: | :white_large_square: |
193238

194239

195-
## Testing
196-
197-
* [ ] Integration tests should be run at a client level against both azure and aws to maintain sdk parity against both apis **(currently only on one cloud)**
198-
* [ ] Terraform acceptance tests should be run against both aws and azure to maintain parity of provider between both cloud services **(currently only on one cloud)**
199-
200240
## Project Support
201241
Please note that all projects in the /databrickslabs github account are provided for your exploration only, and are not formally supported by Databricks with Service Level Agreements (SLAs). They are provided AS-IS and we do not make any guarantees of any kind. Please do not submit a support ticket relating to any issues arising from the use of these projects.
202242

client/service/client.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ type DBApiClientConfig struct {
6868
// Setup initializes the client
6969
func (c *DBApiClientConfig) Setup() {
7070
if c.TimeoutSeconds == 0 {
71-
c.TimeoutSeconds = 10
71+
c.TimeoutSeconds = 60
7272
}
7373
c.client = http.Client{
7474
Timeout: time.Duration(time.Duration(c.TimeoutSeconds) * time.Second),

databricks/azure_ws_init.go renamed to databricks/azure_auth.go

+111-31
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ package databricks
22

33
import (
44
"encoding/json"
5+
"fmt"
6+
"github.com/Azure/go-autorest/autorest/adal"
7+
"github.com/Azure/go-autorest/autorest/azure"
58
"github.com/databrickslabs/databricks-terraform/client/service"
69
"log"
710
"net/http"
11+
urlParse "net/url"
12+
"strings"
13+
"time"
814
)
915

1016
// List of management information
1117
const (
12-
ManagementResourceEndpoint string = "https://management.core.windows.net/"
13-
ADBResourceID string = "2ff814a6-3304-4ab8-85cb-cd0e6f879c1d"
18+
ADBResourceID string = "2ff814a6-3304-4ab8-85cb-cd0e6f879c1d"
1419
)
1520

1621
// AzureAuth is a struct that contains information about the azure sp authentication
@@ -48,31 +53,36 @@ type WorkspaceRequest struct {
4853

4954
func (a *AzureAuth) getManagementToken(config *service.DBApiClientConfig) error {
5055
log.Println("[DEBUG] Creating Azure Databricks management OAuth token.")
51-
url := "https://login.microsoftonline.com/" + a.TokenPayload.TenantID + "/oauth2/token"
52-
payload := "grant_type=client_credentials&client_id=" + a.TokenPayload.ClientID + "&client_secret=" + a.TokenPayload.ClientSecret + "&resource=" + ManagementResourceEndpoint
53-
headers := map[string]string{
54-
"Content-Type": "application/x-www-form-urlencoded",
55-
"cache-control": "no-cache",
56+
mgmtTokenOAuthCfg, err := adal.NewOAuthConfigWithAPIVersion(azure.PublicCloud.ActiveDirectoryEndpoint,
57+
a.TokenPayload.TenantID,
58+
nil)
59+
if err != nil {
60+
return err
5661
}
57-
58-
var responseMap map[string]interface{}
59-
resp, err := service.PerformQuery(config, http.MethodPost, url, "2.0", headers, false, true, payload, nil)
62+
mgmtToken, err := adal.NewServicePrincipalToken(
63+
*mgmtTokenOAuthCfg,
64+
a.TokenPayload.ClientID,
65+
a.TokenPayload.ClientSecret,
66+
azure.PublicCloud.ServiceManagementEndpoint)
6067
if err != nil {
6168
return err
6269
}
63-
err = json.Unmarshal(resp, &responseMap)
70+
err = mgmtToken.Refresh()
6471
if err != nil {
6572
return err
6673
}
67-
a.ManagementToken = responseMap["access_token"].(string)
74+
a.ManagementToken = mgmtToken.OAuthToken()
6875
return nil
6976
}
7077

7178
func (a *AzureAuth) getWorkspaceID(config *service.DBApiClientConfig) error {
7279
log.Println("[DEBUG] Getting Workspace ID via management token.")
73-
url := "https://management.azure.com/subscriptions/" + a.TokenPayload.SubscriptionID + "/resourceGroups/" + a.TokenPayload.ResourceGroup + "/providers/Microsoft.Databricks/workspaces/" + a.TokenPayload.WorkspaceName + "" +
74-
"?api-version=2018-04-01"
75-
80+
// Escape all the ids
81+
url := fmt.Sprintf("https://management.azure.com/subscriptions/%s/resourceGroups/%s"+
82+
"/providers/Microsoft.Databricks/workspaces/%s?api-version=2018-04-01",
83+
urlParse.PathEscape(a.TokenPayload.SubscriptionID),
84+
urlParse.PathEscape(a.TokenPayload.ResourceGroup),
85+
urlParse.PathEscape(a.TokenPayload.WorkspaceName))
7686
payload := &WorkspaceRequest{
7787
Properties: &WsProps{ManagedResourceGroupID: "/subscriptions/" + a.TokenPayload.SubscriptionID + "/resourceGroups/" + a.TokenPayload.ManagedResourceGroup},
7888
Name: a.TokenPayload.WorkspaceName,
@@ -89,36 +99,37 @@ func (a *AzureAuth) getWorkspaceID(config *service.DBApiClientConfig) error {
8999
if err != nil {
90100
return err
91101
}
102+
92103
err = json.Unmarshal(resp, &responseMap)
93104
if err != nil {
94105
return err
95106
}
96-
log.Println(responseMap)
97107
a.AdbWorkspaceResourceID = responseMap["id"].(string)
98108
return err
99109
}
100110

101111
func (a *AzureAuth) getADBPlatformToken(clientConfig *service.DBApiClientConfig) error {
102-
log.Println("[DEBUG] Creating Azure Databricks platform OAuth token.")
103-
url := "https://login.microsoftonline.com/" + a.TokenPayload.TenantID + "/oauth2/token"
104-
payload := "grant_type=client_credentials&client_id=" + a.TokenPayload.ClientID + "&client_secret=" + a.TokenPayload.ClientSecret + "&resource=" + ADBResourceID
105-
headers := map[string]string{
106-
"Content-Type": "application/x-www-form-urlencoded",
107-
"cache-control": "no-cache",
112+
log.Println("[DEBUG] Creating Azure Databricks management OAuth token.")
113+
platformTokenOAuthCfg, err := adal.NewOAuthConfigWithAPIVersion(azure.PublicCloud.ActiveDirectoryEndpoint,
114+
a.TokenPayload.TenantID,
115+
nil)
116+
if err != nil {
117+
return err
108118
}
109-
110-
var responseMap map[string]interface{}
111-
resp, err := service.PerformQuery(clientConfig, http.MethodPost, url, "2.0", headers, false, true, payload, &service.SecretsMask{
112-
Secrets: []string{a.TokenPayload.ClientID, a.TokenPayload.TenantID, a.TokenPayload.ClientSecret},
113-
})
119+
platformToken, err := adal.NewServicePrincipalToken(
120+
*platformTokenOAuthCfg,
121+
a.TokenPayload.ClientID,
122+
a.TokenPayload.ClientSecret,
123+
ADBResourceID)
114124
if err != nil {
115125
return err
116126
}
117-
err = json.Unmarshal(resp, &responseMap)
127+
128+
err = platformToken.Refresh()
118129
if err != nil {
119130
return err
120131
}
121-
a.AdbPlatformToken = responseMap["access_token"].(string)
132+
a.AdbPlatformToken = platformToken.OAuthToken()
122133
return nil
123134
}
124135

@@ -155,31 +166,100 @@ func (a *AzureAuth) getWorkspaceAccessToken(config *service.DBApiClientConfig) e
155166
return nil
156167
}
157168

169+
// Main function call that gets made and it follows 4 steps at the moment:
170+
// 1. Get Management OAuth Token using management endpoint
171+
// 2. Get Workspace ID
172+
// 3. Get Azure Databricks Platform OAuth Token using Databricks resource id
173+
// 4. Get Azure Databricks Workspace Personal Access Token for the SP (60 min duration)
158174
func (a *AzureAuth) initWorkspaceAndGetClient(config *service.DBApiClientConfig) (service.DBApiClient, error) {
159175
var dbClient service.DBApiClient
176+
177+
// Get management token
160178
err := a.getManagementToken(config)
161179
if err != nil {
162180
return dbClient, err
163181
}
164182

183+
// Get workspace access token
165184
err = a.getWorkspaceID(config)
166185
if err != nil {
167186
return dbClient, err
168187
}
188+
189+
// Get platform token
169190
err = a.getADBPlatformToken(config)
170191
if err != nil {
171192
return dbClient, err
172193
}
194+
195+
// Get workspace personal access token
173196
err = a.getWorkspaceAccessToken(config)
174197
if err != nil {
175198
return dbClient, err
176199
}
177200

178201
var newOption service.DBApiClientConfig
179-
newOption.Token = a.AdbAccessToken
202+
203+
// TODO: Eventually change this to include new Databricks domain names. May have to add new vars and/or deprecate existing args.
180204
newOption.Host = "https://" + a.TokenPayload.AzureRegion + ".azuredatabricks.net"
205+
newOption.Token = a.AdbAccessToken
206+
207+
// Headers to use aad tokens, hidden till tokens support secrets, scopes and acls
208+
//newOption.DefaultHeaders = map[string]string{
209+
// //"Content-Type": "application/x-www-form-urlencoded",
210+
// "X-Databricks-Azure-Workspace-Resource-Id": a.AdbWorkspaceResourceID,
211+
// "X-Databricks-Azure-SP-Management-Token": a.ManagementToken,
212+
// "cache-control": "no-cache",
213+
//}
181214
dbClient.SetConfig(&newOption)
182215

183-
_, err = dbClient.Clusters().ListNodeTypes()
216+
// So when the workspace is initially created sometimes it fails to perform api calls so this is a simple test
217+
// to verify that the workspace has been created successfully. The retry is intentional as sometimes after workspace
218+
// creation the API's will not work correctly. This may also
219+
err = validateWorkspaceApis(10, 30, func(attempt int) error {
220+
_, err = dbClient.Clusters().ListNodeTypes()
221+
return err
222+
})
223+
184224
return dbClient, err
185225
}
226+
227+
func validateWorkspaceApis(sleepDurationSeconds time.Duration, timeoutDurationMinutes time.Duration, do func(attempt int) error) error {
228+
errChan := make(chan error, 1)
229+
var timeoutBool = false
230+
var attempts int
231+
var expectedError error
232+
apisAreNotYetReadErr := "com.databricks.backend.manager.util.UnknownWorkerEnvironmentException: Unknown worker environment WorkerEnvId"
233+
go func(attempts *int, expectedError *error, timeout *bool) {
234+
for {
235+
err := do(*attempts)
236+
// Timeout and terminate go routine so it does not leak
237+
if *timeout {
238+
errChan <- err
239+
return
240+
}
241+
if err == nil {
242+
errChan <- err
243+
return
244+
}
245+
if !strings.Contains(err.Error(), apisAreNotYetReadErr) {
246+
errChan <- err
247+
return
248+
}
249+
log.Println(fmt.Sprintf("Waiting for cluster apis to not throw error: %v", err))
250+
*attempts++
251+
*expectedError = err
252+
time.Sleep(sleepDurationSeconds * time.Second)
253+
}
254+
}(&attempts, &expectedError, &timeoutBool)
255+
select {
256+
case err := <-errChan:
257+
if err == nil {
258+
log.Printf("Returned nil error after %v attempts\n", attempts)
259+
}
260+
return err
261+
case <-time.After(timeoutDurationMinutes * time.Minute):
262+
timeoutBool = true
263+
return fmt.Errorf("timed out waiting for ready state after %v attempts with error %v", attempts, expectedError)
264+
}
265+
}

0 commit comments

Comments
 (0)