Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more readability for errors #129

Merged
merged 33 commits into from
Jun 25, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c42bb01
Add resource wait retry for workspace create
nfx Jun 19, 2020
4186055
Made vscode integration testing simpler
nfx Jun 19, 2020
4dff4d1
added read support for username/password for config files
nfx Jun 19, 2020
1278b62
Made errors concise and explainable
nfx Jun 19, 2020
c580ad7
Fix formatting issue
nfx Jun 19, 2020
c42d4c4
cleaned up token request structs
nfx Jun 21, 2020
457ef82
make fmt
nfx Jun 21, 2020
0e235c1
Added some documentation
nfx Jun 21, 2020
3fd44f7
keying composite literals
nfx Jun 21, 2020
97361c6
Add resource wait retry for workspace create
nfx Jun 19, 2020
285995a
Made vscode integration testing simpler
nfx Jun 19, 2020
9b09084
added read support for username/password for config files
nfx Jun 19, 2020
4b931c9
Made errors concise and explainable
nfx Jun 19, 2020
0ea8c51
Fix formatting issue
nfx Jun 19, 2020
8b17019
cleaned up token request structs
nfx Jun 21, 2020
a64ba56
make fmt
nfx Jun 21, 2020
94ac0e3
Added some documentation
nfx Jun 21, 2020
0f68309
keying composite literals
nfx Jun 21, 2020
a076e0d
Add missing resource check in resourceClusterPolicyRead
nfx Jun 22, 2020
e631e3c
Apply review comments
nfx Jun 22, 2020
2c063c6
Merge branch 'fixes' of github.com:databrickslabs/terraform-provider-…
nfx Jun 22, 2020
bbb9a7e
More correct implementation of 404-check
nfx Jun 22, 2020
05d59ec
Make README more user-friendly
nfx Jun 22, 2020
7e8f0ff
More links
nfx Jun 22, 2020
3a2d754
added integration test to verify that all apis can either handle 404s…
stikkireddy Jun 25, 2020
a8747e0
added skip for testMissingWorkspaceResources to run only if TF_ACC is…
stikkireddy Jun 25, 2020
94580cb
Merge branches 'fixes' and 'master' of github.com:databrickslabs/terr…
stikkireddy Jun 25, 2020
7cdaa55
cleaned up makefile
stikkireddy Jun 25, 2020
742ded6
refactored tokenexpirytime to the api client config so the client is …
stikkireddy Jun 25, 2020
bbb4c75
added a int test missing cluster policy
stikkireddy Jun 25, 2020
2a69c2d
adjusted the headers on the index and added the id attribute for clus…
stikkireddy Jun 25, 2020
69ad2fd
corrected typos
stikkireddy Jun 25, 2020
0ecd736
fix another credentials typo
stikkireddy Jun 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 32 additions & 24 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch test function",
            "type": "go",
            "request": "launch",
            "mode": "test",
            "program": "${workspaceRoot}/databricks/resource_databricks_azure_adls_gen2_mount_test.go",
            "args": [
                "-test.v",
                "-test.run",
                "TestAccAzureAdlsGen2Mount_capture_error"
            ],
"env": {
"TF_ACC" : "1"
// "TEST_RESOURCE_GROUP" : "${env:TEST_RESOURCE_GROUP}"
},
            "showLog": true
        }
    ]
}
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch test function",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${file}",
"args": [
"-test.v",
"-test.run",
"${selectedText}"
],
"dlvLoadConfig": {
"followPointers": true,
"maxVariableRecurse": 1,
"maxStringLen": 64,
"maxArrayValues": 64,
"maxStructFields": -1
},
"env": {
"TF_ACC": "1",
"DATABRICKS_CONFIG_PROFILE": "sandbox"
// "TEST_RESOURCE_GROUP" : "${env:TEST_RESOURCE_GROUP}"
},
"showLog": true
}
]
}
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"go.testFlags": ["-v"],
"go.delveConfig": {
"dlvLoadConfig": {
"followPointers": true,
Expand Down
6 changes: 6 additions & 0 deletions client/model/token.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package model

// TokenRequest asks for a token
type TokenRequest struct {
LifetimeSeconds int32 `json:"lifetime_seconds,omitempty"`
Comment string `json:"comment,omitempty"`
}

// TokenResponse is a struct that contains information about token that is created from the create tokens api
type TokenResponse struct {
TokenValue string `json:"token_value,omitempty"`
Expand Down
66 changes: 52 additions & 14 deletions client/service/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"reflect"
"regexp"
"strings"
"sync"
"time"
Expand All @@ -28,25 +29,40 @@ const (
Azure CloudServiceProvider = "Azure"
)

// DBApiErrorBody is a struct for a custom api error for all the services on databrickss.
type DBApiErrorBody struct {
type apiErrorBody struct {
ErrorCode string `json:"error_code,omitempty"`
Message string `json:"message,omitempty"`
// The following two are for scim api only for RFC 7644 Section 3.7.3 https://tools.ietf.org/html/rfc7644#section-3.7.3
ScimDetail string `json:"detail,omitempty"`
ScimStatus string `json:"status,omitempty"`
}

// DBApiError is a generic struct for an api error on databricks
type DBApiError struct {
ErrorBody *DBApiErrorBody
// APIError is a generic struct for an api error on databricks
type APIError struct {
ErrorCode string
Message string
Resource string
StatusCode int
Err error
}

// Error is a interface implementation of the error interface.
func (r DBApiError) Error() string {
return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
// Error returns error message string instead of
func (apiError APIError) Error() string {
docs := apiError.DocumentationURL()
if docs == "" {
return fmt.Sprintf("%s\n(%d on %s)", apiError.Message, apiError.StatusCode, apiError.Resource)
}
return fmt.Sprintf("%s\nPlease consult API docs at %s for details.", apiError.Message, docs)
}

// DocumentationURL guesses doc link
func (apiError APIError) DocumentationURL() string {
endpointRE := regexp.MustCompile(`/api/2.0/([^/]+)/([^/]+)$`)
endpointMatches := endpointRE.FindStringSubmatch(apiError.Resource)
if len(endpointMatches) < 3 {
return ""
}
return fmt.Sprintf("https://docs.databricks.com/dev-tools/api/latest/%s.html#%s",
endpointMatches[1], endpointMatches[2])
}

// AuthType is a custom type for a type of authentication allowed on Databricks
Expand Down Expand Up @@ -128,15 +144,37 @@ func checkHTTPRetry(ctx context.Context, resp *http.Response, err error) (bool,
if err != nil {
return false, err
}
var errorBody DBApiErrorBody
var errorBody apiErrorBody
err = json.Unmarshal(body, &errorBody)
if err != nil {
return false, fmt.Errorf("Response from server (%d) %s: %v", resp.StatusCode, string(body), err)
// this is most likely HTML...
stringBody := string(body)
messageRE := regexp.MustCompile(`<pre>(.*)</pre>`)
messageMatches := messageRE.FindStringSubmatch(stringBody)
if len(messageMatches) < 2 {
return false, fmt.Errorf("Response from server (%d) %s: %v", resp.StatusCode, stringBody, err)
}
errorBody.Message = strings.Trim(messageMatches[1], " .")
statusParts := strings.SplitN(resp.Status, " ", 2)
if len(statusParts) < 2 {
errorBody.ErrorCode = "UNKNOWN"
} else {
errorBody.ErrorCode = strings.ReplaceAll(strings.ToUpper(strings.Trim(statusParts[1], " .")), " ", "_")
}
}
dbAPIError := DBApiError{
ErrorBody: &errorBody,
dbAPIError := APIError{
Message: errorBody.Message,
ErrorCode: errorBody.ErrorCode,
StatusCode: resp.StatusCode,
Err: fmt.Errorf("Response from server %s", string(body)),
Resource: resp.Request.URL.Path,
}
if dbAPIError.Message == "" && errorBody.ScimDetail != "" {
if errorBody.ScimDetail == "null" {
dbAPIError.Message = "SCIM API Internal Error"
} else {
dbAPIError.Message = errorBody.ScimDetail
}
dbAPIError.ErrorCode = fmt.Sprintf("SCIM_%s", errorBody.ScimStatus)
}
for _, substring := range transientErrorStringMatches {
if strings.Contains(errorBody.Message, substring) {
Expand Down
9 changes: 3 additions & 6 deletions client/service/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@ type TokensAPI struct {
func (a TokensAPI) Create(lifeTimeSeconds int32, comment string) (model.TokenResponse, error) {
var tokenData model.TokenResponse

tokenCreateRequest := struct {
LifetimeSeconds int32 `json:"lifetime_seconds,omitempty"`
Comment string `json:"comment,omitempty"`
}{
lifeTimeSeconds,
comment,
tokenCreateRequest := model.TokenRequest{
LifetimeSeconds: lifeTimeSeconds,
Comment: comment,
}

tokenCreateResponse, err := a.Client.performQuery(http.MethodPost, "/token/create", "2.0", nil, tokenCreateRequest, nil)
Expand Down
26 changes: 13 additions & 13 deletions databricks/azure_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/databrickslabs/databricks-terraform/client/model"
"github.com/databrickslabs/databricks-terraform/client/service"
)

Expand All @@ -24,6 +25,9 @@ type AzureAuth struct {
AdbWorkspaceResourceID string
AdbAccessToken string
AdbPlatformToken string
// new token should be requested from
// the workspace before this time comes
PatExpiryTime int64
}

// TokenPayload contains all the auth information for azure sp authentication
Expand Down Expand Up @@ -133,16 +137,7 @@ func (a *AzureAuth) getADBPlatformToken() error {

func (a *AzureAuth) getWorkspaceAccessToken(config *service.DBApiClientConfig) error {
log.Println("[DEBUG] Creating workspace token")
apiLifeTimeInSeconds := int32(600)
comment := "Secret made via SP"
url := "https://" + a.TokenPayload.AzureRegion + ".azuredatabricks.net/api/2.0/token/create"
payload := struct {
LifetimeSeconds int32 `json:"lifetime_seconds,omitempty"`
Comment string `json:"comment,omitempty"`
}{
apiLifeTimeInSeconds,
comment,
}
headers := map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
"X-Databricks-Azure-Workspace-Resource-Id": a.AdbWorkspaceResourceID,
Expand All @@ -151,16 +146,21 @@ func (a *AzureAuth) getWorkspaceAccessToken(config *service.DBApiClientConfig) e
"Authorization": "Bearer " + a.AdbPlatformToken,
}

var responseMap map[string]interface{}
resp, err := service.PerformQuery(config, http.MethodPost, url, "2.0", headers, true, true, payload, nil)
var tokenResponse model.TokenResponse
resp, err := service.PerformQuery(config, http.MethodPost, url, "2.0",
headers, true, true, model.TokenRequest{
LifetimeSeconds: int32(600),
Comment: "Secret made via SP",
}, nil)
if err != nil {
return err
}
err = json.Unmarshal(resp, &responseMap)
err = json.Unmarshal(resp, &tokenResponse)
if err != nil {
return err
}
a.AdbAccessToken = responseMap["token_value"].(string)
a.AdbAccessToken = tokenResponse.TokenValue
a.PatExpiryTime = tokenResponse.TokenInfo.ExpiryTime
return nil
}

Expand Down
31 changes: 18 additions & 13 deletions databricks/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ func Provider(version string) terraform.ResourceProvider {
"file credetials will only be used when host/token are not provided.",
},
"profile": {
Type: schema.TypeString,
Optional: true,
Default: "DEFAULT",
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("DATABRICKS_CONFIG_PROFILE", "DEFAULT"),
Description: "Connection profile specified within ~/.databrickscfg. Please check\n" +
"https://docs.databricks.com/dev-tools/cli/index.html#connection-profiles for documentation.",
},
Expand Down Expand Up @@ -275,20 +275,25 @@ func tryDatabricksCliConfigFile(d *schema.ResourceData, config *service.DBApiCli
"Please check https://docs.databricks.com/dev-tools/cli/index.html#set-up-authentication for details", configFile)
}
if profile, ok := d.GetOk("profile"); ok {
dbcliConfig := cfg.Section(profile.(string))
token := dbcliConfig.Key("token").String()
if "" == token {
return fmt.Errorf("config file %s is corrupt: cannot find token in %s profile",
dbcli := cfg.Section(profile.(string))
config.Host = dbcli.Key("host").String()
if config.Host == "" {
return fmt.Errorf("config file %s is corrupt: cannot find host in %s profile",
configFile, profile)
}
config.Token = token

host := dbcliConfig.Key("host").String()
if "" == host {
return fmt.Errorf("config file %s is corrupt: cannot find host in %s profile",
if dbcli.HasKey("username") && dbcli.HasKey("password") {
username := dbcli.Key("username").String()
password := dbcli.Key("password").String()
tokenUnB64 := fmt.Sprintf("%s:%s", username, password)
config.Token = base64.StdEncoding.EncodeToString([]byte(tokenUnB64))
config.AuthType = service.BasicAuth
} else {
config.Token = dbcli.Key("token").String()
}
if config.Token == "" {
return fmt.Errorf("config file %s is corrupt: cannot find token in %s profile",
configFile, profile)
}
config.Host = host
}

return nil
Expand Down
24 changes: 24 additions & 0 deletions databricks/resource_databricks_mws_workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (

"github.com/databrickslabs/databricks-terraform/client/model"
"github.com/databrickslabs/databricks-terraform/client/service"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"

"log"
"net"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -124,6 +126,21 @@ func resourceMWSWorkspaces() *schema.Resource {
}
}

func waitForWorkspaceURLResolution(workspace model.MWSWorkspace, timeoutDurationMinutes time.Duration) error {
hostAndPort := fmt.Sprintf("%s.cloud.databricks.com:443", workspace.DeploymentName)
url := fmt.Sprintf("https://%s.cloud.databricks.com", workspace.DeploymentName)
return resource.Retry(timeoutDurationMinutes, func() *resource.RetryError {
conn, err := net.DialTimeout("tcp", hostAndPort, 1*time.Minute)
if err != nil {
log.Printf("Cannot yet reach %s", url)
return resource.RetryableError(err)
}
log.Printf("Workspace %s is ready to use", url)
defer conn.Close()
return nil
})
}

func resourceMWSWorkspacesCreate(d *schema.ResourceData, m interface{}) error {
client := m.(*service.DBApiClient)
mwsAcctID := d.Get("account_id").(string)
Expand Down Expand Up @@ -162,6 +179,13 @@ func resourceMWSWorkspacesCreate(d *schema.ResourceData, m interface{}) error {
}
return err
}
// wait maximum 5 minute for DNS caches to refresh, as
// sometimes we cannot make API calls to new workspaces
// immediately after it's created
err = waitForWorkspaceURLResolution(workspace, 5*time.Minute)
if err != nil {
return err
}
return resourceMWSWorkspacesRead(d, m)
}

Expand Down
Loading