diff --git a/access/acceptance/sql_permissions_test.go b/access/acceptance/sql_permissions_test.go index 0c6c61a027..0ad73764bb 100644 --- a/access/acceptance/sql_permissions_test.go +++ b/access/acceptance/sql_permissions_test.go @@ -6,10 +6,8 @@ import ( "os" "testing" - "github.com/databricks/terraform-provider-databricks/commands" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go" "github.com/databricks/terraform-provider-databricks/internal/acceptance" - "github.com/databricks/terraform-provider-databricks/internal/compute" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,23 +19,20 @@ func TestAccTableACL(t *testing.T) { t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") } t.Parallel() - client := common.CommonEnvironmentClient() - client.WithCommandExecutor(func(ctx context.Context, - dc *common.DatabricksClient) common.CommandExecutor { - return commands.NewCommandsAPI(ctx, dc) - }) - shell := client.CommandExecutor(context.Background()) - clusterInfo := compute.NewTinyClusterInCommonPoolPossiblyReused() - talbeName := qa.RandomName("table_acl_") + ctx := context.Background() + w := databricks.Must(databricks.NewWorkspaceClient()) + info, err := w.Clusters.GetOrCreateRunningCluster(ctx, "tf-dummy") + require.NoError(t, err) - cr := shell.Execute(clusterInfo.ClusterID, "python", + talbeName := qa.RandomName("table_acl_") + cr := w.CommandExecutor.Execute(ctx, info.ClusterId, "python", fmt.Sprintf("spark.range(10).write.saveAsTable('%s')", talbeName)) require.False(t, cr.Failed(), cr.Error()) os.Setenv("TABLE_ACL_TEST_TABLE", talbeName) defer func() { - cr := shell.Execute(clusterInfo.ClusterID, "sql", + cr := w.CommandExecutor.Execute(ctx, info.ClusterId, "sql", fmt.Sprintf("DROP TABLE %s", talbeName)) assert.False(t, cr.Failed(), cr.Error()) }() @@ -47,7 +42,7 @@ func TestAccTableACL(t *testing.T) { Template: ` resource "databricks_sql_permissions" "this" { table = "{env.TABLE_ACL_TEST_TABLE}" - + privilege_assignments { principal = "users" privileges = ["SELECT"] diff --git a/access/resource_ip_access_list_test.go b/access/resource_ip_access_list_test.go index 3af4a39271..305a7f7d14 100644 --- a/access/resource_ip_access_list_test.go +++ b/access/resource_ip_access_list_test.go @@ -5,10 +5,9 @@ package access import ( "context" "net/http" - "os" "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -25,37 +24,6 @@ var ( TestingIPAddressesState = []any{"1.2.3.4", "1.2.4.0/24"} ) -func TestAccIPACL(t *testing.T) { - cloud := os.Getenv("CLOUD_ENV") - if cloud == "" { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - client := common.NewClientFromEnvironment() - ctx := context.Background() - ipAccessListsAPI := NewIPAccessListsAPI(ctx, client) - res, err := ipAccessListsAPI.Create(createIPAccessListRequest{ - Label: qa.RandomName("ipacl-"), - ListType: "BLOCK", - IPAddresses: TestingIPAddresses, - }) - require.NoError(t, err) - defer func() { - err = ipAccessListsAPI.Delete(res.ListID) - require.NoError(t, err) - }() - err = ipAccessListsAPI.Update(res.ListID, ipAccessListUpdateRequest{ - Label: res.Label, - ListType: res.ListType, - Enabled: true, - IPAddresses: []string{"4.3.2.1"}, - }) - require.NoError(t, err) - updated, err := ipAccessListsAPI.Read(res.ListID) - require.NoError(t, err) - assert.Equal(t, "4.3.2.1", updated.IPAddresses[0]) -} - func TestIPACLCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -123,7 +91,7 @@ func TestAPIACLCreate_Error(t *testing.T) { { Method: http.MethodPost, Resource: "/api/2.0/ip-access-lists", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_ALREADY_EXISTS", Message: "IP access list with type (" + TestingListTypeString + ") and label (" + TestingLabel + ") already exists", }, @@ -206,7 +174,7 @@ func TestIPACLUpdate_Error(t *testing.T) { { Method: http.MethodPut, Resource: "/api/2.0/ip-access-lists/" + TestingID, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "SERVER_ERROR", Message: "Something unexpected happened", }, @@ -261,7 +229,7 @@ func TestIPACLRead_NotFound(t *testing.T) { { Method: http.MethodGet, Resource: "/api/2.0/ip-access-lists/" + TestingID, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "Can't find an IP access list with id: " + TestingID + ".", }, @@ -281,7 +249,7 @@ func TestIPACLRead_Error(t *testing.T) { { Method: http.MethodGet, Resource: "/api/2.0/ip-access-lists/" + TestingID, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "SERVER_ERROR", Message: "Something unexpected happened", }, @@ -319,7 +287,7 @@ func TestIPACLDelete_Error(t *testing.T) { { Method: http.MethodDelete, Resource: "/api/2.0/ip-access-lists/" + TestingID, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_STATE", Message: "Something went wrong", }, diff --git a/access/resource_permission_assignment.go b/access/resource_permission_assignment.go index 84d3f0c351..0ac595e403 100644 --- a/access/resource_permission_assignment.go +++ b/access/resource_permission_assignment.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -56,7 +57,7 @@ func (l PermissionAssignmentList) ForPrincipal(principalId int64) (res Permissio } return Permissions{v.Permissions}, nil } - return res, common.NotFound(fmt.Sprintf("%d not found", principalId)) + return res, apierr.NotFound(fmt.Sprintf("%d not found", principalId)) } func (a PermissionAssignmentAPI) List() (list PermissionAssignmentList, err error) { diff --git a/access/resource_sql_permissions.go b/access/resource_sql_permissions.go index e6695b77e0..6989e94806 100644 --- a/access/resource_sql_permissions.go +++ b/access/resource_sql_permissions.go @@ -6,6 +6,7 @@ import ( "log" "strings" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" @@ -120,7 +121,7 @@ func (ta *SqlPermissions) read() error { failure := currentGrantsOnThis.Error() if strings.Contains(failure, "does not exist") || strings.Contains(failure, "RESOURCE_DOES_NOT_EXIST") { - return common.NotFound(failure) + return apierr.NotFound(failure) } return fmt.Errorf("cannot read current grants: %s", failure) } @@ -249,7 +250,7 @@ func (ta *SqlPermissions) initCluster(ctx context.Context, d *schema.ResourceDat } } clusterInfo, err := clustersAPI.StartAndGetInfo(ta.ClusterID) - if common.IsMissing(err) { + if apierr.IsMissing(err) { // cluster that was previously in a tfstate was deleted ta.ClusterID, err = ta.getOrCreateCluster(clustersAPI) if err != nil { diff --git a/access/resource_sql_permissions_test.go b/access/resource_sql_permissions_test.go index 8794b50929..0c2e2811a4 100644 --- a/access/resource_sql_permissions_test.go +++ b/access/resource_sql_permissions_test.go @@ -90,15 +90,15 @@ func TestTableACLGrants(t *testing.T) { } func TestDatabaseACLGrants(t *testing.T) { - ta := SqlPermissions{ Database: "default", - exec: mockData{ - "SHOW GRANT ON DATABASE `default`": { - // principal, actionType, objType, objectKey - // Test with and without backticks - {"users", "SELECT", "database", "default"}, - {"users", "USAGE", "database", "`default`"}, - }, - }} + ta := SqlPermissions{Database: "default", + exec: mockData{ + "SHOW GRANT ON DATABASE `default`": { + // principal, actionType, objType, objectKey + // Test with and without backticks + {"users", "SELECT", "database", "default"}, + {"users", "USAGE", "database", "`default`"}, + }, + }} err := ta.read() assert.NoError(t, err) assert.Len(t, ta.PrivilegeAssignments, 1) diff --git a/aws/acceptance/instance_profile_test.go b/aws/acceptance/instance_profile_test.go index 40f1e608cc..cfb308b290 100644 --- a/aws/acceptance/instance_profile_test.go +++ b/aws/acceptance/instance_profile_test.go @@ -3,6 +3,8 @@ package acceptance import ( "context" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/aws" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/internal/acceptance" @@ -17,9 +19,14 @@ func TestAccAwsGroupInstanceProfileResource(t *testing.T) { t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") } ctx := context.WithValue(context.Background(), common.Current, t.Name()) - client := common.CommonEnvironmentClient() arn := qa.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE") - instanceProfilesAPI := aws.NewInstanceProfilesAPI(ctx, client) + client, err := client.New(&config.Config{}) + if err != nil { + t.Fatal(err) + } + instanceProfilesAPI := aws.NewInstanceProfilesAPI(ctx, &common.DatabricksClient{ + DatabricksClient: client, + }) instanceProfilesAPI.Synchronized(arn, func() bool { if instanceProfilesAPI.IsRegistered(arn) { return false diff --git a/aws/resource_group_instance_profile.go b/aws/resource_group_instance_profile.go index 3c2f7e6fbc..08338c37c6 100644 --- a/aws/resource_group_instance_profile.go +++ b/aws/resource_group_instance_profile.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/scim" @@ -21,7 +22,7 @@ func ResourceGroupInstanceProfile() *schema.Resource { group, err := scim.NewGroupsAPI(ctx, c).Read(groupID) hasRole := scim.ComplexValues(group.Roles).HasValue(roleARN) if err == nil && !hasRole { - return common.NotFound("Group has no instance profile") + return apierr.NotFound("Group has no instance profile") } return err }, diff --git a/aws/resource_group_instance_profile_test.go b/aws/resource_group_instance_profile_test.go index 3c8b374110..83cd4ee5b8 100644 --- a/aws/resource_group_instance_profile_test.go +++ b/aws/resource_group_instance_profile_test.go @@ -3,7 +3,7 @@ package aws import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/scim" "github.com/databricks/terraform-provider-databricks/qa" @@ -56,7 +56,7 @@ func TestResourceGroupInstanceProfileCreate_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -80,7 +80,7 @@ func TestResourceGroupInstanceProfileCreate_Error_InvalidARN(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -103,7 +103,7 @@ func TestResourceGroupInstanceProfileCreate_Error_OtherARN(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -152,7 +152,7 @@ func TestResourceGroupInstanceProfileRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -192,7 +192,7 @@ func TestResourceGroupInstanceProfileRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -233,7 +233,7 @@ func TestResourceGroupInstanceProfileDelete_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/aws/resource_instance_profile.go b/aws/resource_instance_profile.go index bb6b362715..fe92528785 100644 --- a/aws/resource_instance_profile.go +++ b/aws/resource_instance_profile.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "log" - "net/http" "strings" "time" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/go-cty/cty" @@ -60,13 +60,8 @@ func (a InstanceProfilesAPI) Read(instanceProfileARN string) (result InstancePro return } } - err = common.APIError{ - ErrorCode: "NOT_FOUND", - Message: fmt.Sprintf("Instance profile with name: %s not found in "+ - "list of instance profiles in the workspace!", instanceProfileARN), - Resource: "/api/2.0/instance-profiles/list", - StatusCode: http.StatusNotFound, - } + err = apierr.NotFound(fmt.Sprintf("Instance profile with name: %s not found in "+ + "list of instance profiles in the workspace!", instanceProfileARN)) return } diff --git a/aws/resource_instance_profile_test.go b/aws/resource_instance_profile_test.go index 7f872943c5..d6c4d8ddd2 100644 --- a/aws/resource_instance_profile_test.go +++ b/aws/resource_instance_profile_test.go @@ -1,10 +1,9 @@ package aws import ( - "context" "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -115,7 +114,7 @@ func TestResourceInstanceProfileCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/instance-profiles/add", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -250,7 +249,7 @@ func TestResourceInstanceProfileRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/instance-profiles/list", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -290,7 +289,7 @@ func TestResourceInstanceProfileDelete_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/instance-profiles/remove", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -357,7 +356,7 @@ func TestResourceInstanceProfileUpdate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/instance-profiles/edit", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -376,51 +375,3 @@ func TestResourceInstanceProfileUpdate_Error(t *testing.T) { assert.Equal(t, "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile", d.Id()) } -func TestAccAwsInstanceProfiles(t *testing.T) { - arn := qa.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE") - client := common.NewClientFromEnvironment() - ctx := context.WithValue(context.Background(), common.Current, t.Name()) - instanceProfilesAPI := NewInstanceProfilesAPI(ctx, client) - instanceProfilesAPI.Synchronized(arn, func() bool { - err := instanceProfilesAPI.Create(InstanceProfileInfo{ - InstanceProfileArn: arn, - }) - if err != nil { - return false - } - defer func() { - err := instanceProfilesAPI.Delete(arn) - assert.NoError(t, err) - }() - - arnSearch, err := instanceProfilesAPI.Read(arn) - assert.NoError(t, err) - assert.True(t, len(arnSearch.InstanceProfileArn) > 0) - return true - }) -} - -func TestAccAwsInstanceProfilesSkippingValidation(t *testing.T) { - arn := qa.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE") - client := common.NewClientFromEnvironment() - ctx := context.WithValue(context.Background(), common.Current, t.Name()) - instanceProfilesAPI := NewInstanceProfilesAPI(ctx, client) - instanceProfilesAPI.Synchronized(arn, func() bool { - err := instanceProfilesAPI.Create(InstanceProfileInfo{ - InstanceProfileArn: arn, - SkipValidation: true, - }) - if err != nil { - return false - } - defer func() { - err := instanceProfilesAPI.Delete(arn) - assert.NoError(t, err) - }() - - arnSearch, err := instanceProfilesAPI.Read(arn) - assert.NoError(t, err) - assert.True(t, len(arnSearch.InstanceProfileArn) > 0) - return true - }) -} diff --git a/aws/resource_service_principal_role.go b/aws/resource_service_principal_role.go index bf07b8f1aa..9d39ede107 100644 --- a/aws/resource_service_principal_role.go +++ b/aws/resource_service_principal_role.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/scim" @@ -20,7 +21,7 @@ func ResourceServicePrincipalRole() *schema.Resource { servicePrincipal, err := scim.NewServicePrincipalsAPI(ctx, c).Read(servicePrincipalID) hasRole := scim.ComplexValues(servicePrincipal.Roles).HasValue(roleARN) if err == nil && !hasRole { - return common.NotFound("Service Principal has no role") + return apierr.NotFound("Service Principal has no role") } return err }, diff --git a/aws/resource_service_principal_role_test.go b/aws/resource_service_principal_role_test.go index 6585dd3332..cdf8994805 100644 --- a/aws/resource_service_principal_role_test.go +++ b/aws/resource_service_principal_role_test.go @@ -3,8 +3,7 @@ package aws import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/scim" "github.com/databricks/terraform-provider-databricks/qa" @@ -54,7 +53,7 @@ func TestResourceServicePrincipalRoleCreate_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -120,7 +119,7 @@ func TestResourceServicePrincipalRoleRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, diff --git a/aws/resource_user_instance_profile.go b/aws/resource_user_instance_profile.go index cf956492e0..2bff25281f 100644 --- a/aws/resource_user_instance_profile.go +++ b/aws/resource_user_instance_profile.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/scim" @@ -24,7 +25,7 @@ func ResourceUserInstanceProfile() *schema.Resource { user, err := scim.NewUsersAPI(ctx, c).Read(userID) hasRole := scim.ComplexValues(user.Roles).HasValue(roleARN) if err == nil && !hasRole { - return common.NotFound("User has no role") + return apierr.NotFound("User has no role") } return err }, diff --git a/aws/resource_user_instance_profile_test.go b/aws/resource_user_instance_profile_test.go index fcd16df08d..7eee930907 100644 --- a/aws/resource_user_instance_profile_test.go +++ b/aws/resource_user_instance_profile_test.go @@ -3,7 +3,7 @@ package aws import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/scim" "github.com/databricks/terraform-provider-databricks/qa" @@ -68,7 +68,7 @@ func TestResourceUserInstanceProfileCreate_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Users/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -138,7 +138,7 @@ func TestResourceUserInstanceProfileRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Users/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -158,7 +158,7 @@ func TestResourceUserInstanceProfileRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Users/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -199,7 +199,7 @@ func TestResourceUserInstanceProfileDelete_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Users/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/aws/resource_user_role.go b/aws/resource_user_role.go index 0bf9529724..1b67f62f0f 100644 --- a/aws/resource_user_role.go +++ b/aws/resource_user_role.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/scim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -18,7 +19,7 @@ func ResourceUserRole() *schema.Resource { user, err := scim.NewUsersAPI(ctx, c).Read(userID) hasRole := scim.ComplexValues(user.Roles).HasValue(roleARN) if err == nil && !hasRole { - return common.NotFound("User has no role") + return apierr.NotFound("User has no role") } return err }, diff --git a/catalog/resource_catalog_test.go b/catalog/resource_catalog_test.go index 3894d49144..864f10545d 100644 --- a/catalog/resource_catalog_test.go +++ b/catalog/resource_catalog_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -137,7 +137,7 @@ func TestCatalogCreateCannotDeleteDefaultSchema(t *testing.T) { Method: "DELETE", Resource: "/api/2.1/unity-catalog/schemas/a.default", Status: 400, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ScimDetail: "Something", ScimStatus: "Else", }, diff --git a/catalog/resource_grants.go b/catalog/resource_grants.go index 1762812e15..8d244fb647 100644 --- a/catalog/resource_grants.go +++ b/catalog/resource_grants.go @@ -6,6 +6,7 @@ import ( "sort" "strings" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -310,7 +311,7 @@ func ResourceGrants() *schema.Resource { return err } if len(grants.Assignments) == 0 { - return common.NotFound("got empty permissions list") + return apierr.NotFound("got empty permissions list") } return common.StructToData(grants, s, d) }, diff --git a/catalog/resource_share_test.go b/catalog/resource_share_test.go index 5cc30e871e..e778ac8f32 100644 --- a/catalog/resource_share_test.go +++ b/catalog/resource_share_test.go @@ -3,9 +3,9 @@ package catalog import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" "github.com/stretchr/testify/assert" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" ) @@ -376,7 +376,7 @@ func TestCreateShare_ThrowError(t *testing.T) { }, }, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -451,7 +451,7 @@ func TestCreateShareButPatchFails(t *testing.T) { }, }, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/clusters/acceptance/clusters_api_test.go b/clusters/acceptance/clusters_api_test.go deleted file mode 100644 index 24db598207..0000000000 --- a/clusters/acceptance/clusters_api_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package acceptance - -import ( - "context" - "reflect" - "testing" - - "github.com/databricks/terraform-provider-databricks/clusters" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/qa" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAccListClustersIntegration(t *testing.T) { - qa.RequireAnyCloudEnv(t) - t.Parallel() - client := common.CommonEnvironmentClient() - ctx := context.Background() - clustersAPI := clusters.NewClustersAPI(ctx, client) - randomName := qa.RandomName() - - cluster := clusters.Cluster{ - NumWorkers: 1, - ClusterName: "Terraform Integration Test " + randomName, - SparkVersion: clustersAPI.LatestSparkVersionOrDefault( - clusters.SparkVersionRequest{ - Latest: true, - }), - InstancePoolID: qa.GetEnvOrSkipTest(t, "TEST_INSTANCE_POOL_ID"), - IdempotencyToken: "acc-list-" + randomName, - AutoterminationMinutes: 15, - } - clusterReadInfo, err := clustersAPI.Create(cluster) - require.NoError(t, err) - assert.True(t, clusterReadInfo.NumWorkers == cluster.NumWorkers) - assert.True(t, clusterReadInfo.ClusterName == cluster.ClusterName) - assert.True(t, reflect.DeepEqual(clusterReadInfo.SparkEnvVars, cluster.SparkEnvVars)) - assert.True(t, clusterReadInfo.SparkVersion == cluster.SparkVersion) - assert.True(t, clusterReadInfo.AutoterminationMinutes == cluster.AutoterminationMinutes) - assert.True(t, clusterReadInfo.State == clusters.ClusterStateRunning) - - defer func() { - err = clustersAPI.Terminate(clusterReadInfo.ClusterID) - assert.NoError(t, err) - - clusterReadInfo, err = clustersAPI.Get(clusterReadInfo.ClusterID) - assert.NoError(t, err) - assert.True(t, clusterReadInfo.State == clusters.ClusterStateTerminated) - - err = clustersAPI.Unpin(clusterReadInfo.ClusterID) - assert.NoError(t, err) - - err = clustersAPI.PermanentDelete(clusterReadInfo.ClusterID) - assert.NoError(t, err) - }() - - err = clustersAPI.Pin(clusterReadInfo.ClusterID) - assert.NoError(t, err) - - clusterReadInfo, err = clustersAPI.Get(clusterReadInfo.ClusterID) - assert.NoError(t, err) - assert.True(t, clusterReadInfo.State == clusters.ClusterStateRunning) -} - -func TestAccListClustersResizeIntegrationTest(t *testing.T) { - qa.RequireAnyCloudEnv(t) - t.Parallel() - client := common.CommonEnvironmentClient() - ctx := context.Background() - clustersAPI := clusters.NewClustersAPI(ctx, client) - randomName := qa.RandomName() - - cluster := clusters.Cluster{ - NumWorkers: 1, - ClusterName: "Terraform Integration Test " + randomName, - SparkVersion: clustersAPI.LatestSparkVersionOrDefault( - clusters.SparkVersionRequest{ - Latest: true, - }), - InstancePoolID: qa.GetEnvOrSkipTest(t, "TEST_INSTANCE_POOL_ID"), - IdempotencyToken: "acc-list-" + randomName, - AutoterminationMinutes: 15, - } - - clusterReadInfo, err := clustersAPI.Create(cluster) - require.NoError(t, err) - assert.True(t, clusterReadInfo.NumWorkers == cluster.NumWorkers) - assert.True(t, clusterReadInfo.ClusterName == cluster.ClusterName) - assert.True(t, reflect.DeepEqual(clusterReadInfo.SparkEnvVars, cluster.SparkEnvVars)) - assert.True(t, clusterReadInfo.SparkVersion == cluster.SparkVersion) - assert.True(t, clusterReadInfo.AutoterminationMinutes == cluster.AutoterminationMinutes) - assert.True(t, clusterReadInfo.State == clusters.ClusterStateRunning) - - // Resize num workers - clusterReadInfo, err = clustersAPI.Resize(clusters.ResizeRequest{ClusterID: clusterReadInfo.ClusterID, NumWorkers: 2}) - require.NoError(t, err) - assert.True(t, clusterReadInfo.NumWorkers == 2) - assert.True(t, clusterReadInfo.State == clusters.ClusterStateRunning) - - // Resize cluster to become autoscaling - clusterReadInfo, err = clustersAPI.Resize(clusters.ResizeRequest{ClusterID: clusterReadInfo.ClusterID, AutoScale: &clusters.AutoScale{ - MinWorkers: 1, - MaxWorkers: 2, - }}) - require.NoError(t, err) - assert.True(t, clusterReadInfo.AutoScale.MinWorkers == 1) - assert.True(t, clusterReadInfo.AutoScale.MaxWorkers == 2) - assert.True(t, clusterReadInfo.State == clusters.ClusterStateRunning) - - // cleanup - err = clustersAPI.Terminate(clusterReadInfo.ClusterID) - assert.NoError(t, err) - - clusterReadInfo, err = clustersAPI.Get(clusterReadInfo.ClusterID) - assert.NoError(t, err) - assert.True(t, clusterReadInfo.State == clusters.ClusterStateTerminated) - - err = clustersAPI.Unpin(clusterReadInfo.ClusterID) - assert.NoError(t, err) - - err = clustersAPI.PermanentDelete(clusterReadInfo.ClusterID) - assert.NoError(t, err) -} diff --git a/clusters/clusters_api.go b/clusters/clusters_api.go index 207151478d..5b3dfb0296 100644 --- a/clusters/clusters_api.go +++ b/clusters/clusters_api.go @@ -2,12 +2,14 @@ package clusters import ( "context" + "errors" "fmt" "log" "strings" "sync" "time" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -666,12 +668,13 @@ func (a ClustersAPI) StartAndGetInfo(clusterID string) (ClusterInfo, error) { } // make common/resource.go#ToResource read behavior consistent with "normal" resources +// TODO: https://github.com/databricks/terraform-provider-databricks/issues/2021 func wrapMissingClusterError(err error, id string) error { if err == nil { return nil } - apiErr, ok := err.(common.APIError) - if !ok { + var apiErr *apierr.APIError + if !errors.As(err, &apiErr) { return err } if apiErr.IsMissing() { @@ -699,7 +702,7 @@ func (a ClustersAPI) waitForClusterStatus(clusterID string, desired ClusterState // nolint should be a bigger context-aware refactor return result, resource.RetryContext(a.context, a.defaultTimeout(), func() *resource.RetryError { clusterInfo, err := a.Get(clusterID) - if common.IsMissing(err) { + if apierr.IsMissing(err) { log.Printf("[INFO] Cluster %s not found. Retrying", clusterID) return resource.RetryableError(err) } diff --git a/clusters/clusters_api_test.go b/clusters/clusters_api_test.go index 6a5c32d3b0..74f874e1b1 100644 --- a/clusters/clusters_api_test.go +++ b/clusters/clusters_api_test.go @@ -2,12 +2,14 @@ package clusters import ( "context" + "errors" "fmt" // "reflect" "strings" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -104,7 +106,7 @@ func TestGetOrCreateRunningCluster_AzureAuth(t *testing.T) { defer server.Close() require.NoError(t, err) - client.AzureResourceID = "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c" + client.Config.AzureResourceID = "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c" ctx := context.Background() clusterInfo, err := NewClustersAPI(ctx, client).GetOrCreateRunningCluster("mount") @@ -157,7 +159,7 @@ func TestGetOrCreateRunningCluster_Existing_AzureAuth(t *testing.T) { defer server.Close() require.NoError(t, err) - client.AzureResourceID = "/a/b/c" + client.Config.AzureResourceID = "/a/b/c" ctx := context.Background() clusterInfo, err := NewClustersAPI(ctx, client).GetOrCreateRunningCluster("mount") @@ -171,7 +173,7 @@ func TestWaitForClusterStatus_RetryOnNotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ Message: "Nope", }, Status: 404, @@ -187,7 +189,7 @@ func TestWaitForClusterStatus_RetryOnNotFound(t *testing.T) { defer server.Close() require.NoError(t, err) - client.AzureResourceID = "/a/b/c" + client.Config.AzureResourceID = "/a/b/c" ctx := context.Background() clusterInfo, err := NewClustersAPI(ctx, client).waitForClusterStatus("abc", ClusterStateRunning) @@ -201,7 +203,7 @@ func TestWaitForClusterStatus_StopRetryingEarly(t *testing.T) { { Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ Message: "I am a teapot", }, Status: 418, @@ -232,7 +234,7 @@ func TestWaitForClusterStatus_NotReachable(t *testing.T) { defer server.Close() require.NoError(t, err) - client.AzureResourceID = "/a/b/c" + client.Config.AzureResourceID = "/a/b/c" ctx := context.Background() _, err = NewClustersAPI(ctx, client).waitForClusterStatus("abc", ClusterStateRunning) @@ -591,7 +593,7 @@ func TestStartAndGetInfo_StartingError(t *testing.T) { ExpectedRequest: ClusterID{ ClusterID: "abc", }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ Message: "I am a teapot!", }, Status: 418, @@ -628,7 +630,7 @@ func TestPermanentDelete_Pinned(t *testing.T) { ExpectedRequest: ClusterID{ ClusterID: "abc", }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ Message: "unpin the cluster first", }, Status: 400, @@ -656,16 +658,6 @@ func TestPermanentDelete_Pinned(t *testing.T) { require.NoError(t, err) } -func TestAccAwsSmallestNodeType(t *testing.T) { - qa.RequireCloudEnv(t, "aws") - client := common.CommonEnvironmentClient() - ctx := context.Background() - nodeType := NewClustersAPI(ctx, client).GetSmallestNodeType(NodeTypeRequest{ - LocalDisk: true, - }) - assert.Equal(t, "m5d.large", nodeType) -} - func TestEventsSinglePage(t *testing.T) { client, server, err := qa.HttpFixtureClient(t, []qa.HTTPFixture{ { @@ -1239,7 +1231,7 @@ func TestFailureOfPermanentDeleteOnCreateFailure(t *testing.T) { Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=abc", Status: 418, - Response: common.APIError{ + Response: apierr.APIError{ ErrorCode: "TEST", Message: "nothing", }, @@ -1259,7 +1251,7 @@ func TestFailureOfPermanentDeleteOnCreateFailure(t *testing.T) { Method: "POST", Resource: "/api/2.0/clusters/permanent-delete", Status: 418, - Response: common.APIError{ + Response: apierr.APIError{ ErrorCode: "TEST", Message: "You should unpin the cluster first", }, @@ -1268,7 +1260,7 @@ func TestFailureOfPermanentDeleteOnCreateFailure(t *testing.T) { Method: "POST", Resource: "/api/2.0/clusters/unpin", Status: 418, - Response: common.NotFound("missing"), + Response: apierr.NotFound("missing"), }, }, func(ctx context.Context, client *common.DatabricksClient) { a := NewClustersAPI(ctx, client) @@ -1279,16 +1271,17 @@ func TestFailureOfPermanentDeleteOnCreateFailure(t *testing.T) { func TestWrapMissingClusterError(t *testing.T) { assert.EqualError(t, wrapMissingClusterError(fmt.Errorf("x"), "abc"), "x") - assert.EqualError(t, wrapMissingClusterError(common.APIError{ + assert.EqualError(t, wrapMissingClusterError(&apierr.APIError{ Message: "Cluster abc does not exist", }, "abc"), "Cluster abc does not exist") } func TestExpiredClusterAssumedAsRemoved(t *testing.T) { - err := wrapMissingClusterError(common.APIError{ + err := wrapMissingClusterError(&apierr.APIError{ ErrorCode: "INVALID_STATE", Message: "Cannot access cluster X that was terminated or unpinned more than Y days ago.", }, "X") - ae, _ := err.(common.APIError) + var ae *apierr.APIError + assert.True(t, errors.As(err, &ae)) assert.Equal(t, 404, ae.StatusCode) } diff --git a/clusters/data_clusters_test.go b/clusters/data_clusters_test.go index 023e6a81c3..aea5485863 100644 --- a/clusters/data_clusters_test.go +++ b/clusters/data_clusters_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -70,8 +72,13 @@ func TestClustersDataSourceContainsName(t *testing.T) { } func TestClustersDataSourceErrorsOut(t *testing.T) { + client, _ := client.New(&config.Config{ + Host: ".", + Token: ".", + }) diag := DataSourceClusters().ReadContext(context.Background(), nil, &common.DatabricksClient{ - Host: ".", Token: "."}) + DatabricksClient: client, + }) assert.NotNil(t, diag) assert.True(t, diag.HasError()) } diff --git a/clusters/data_node_type_test.go b/clusters/data_node_type_test.go index 43b9924168..1703a91cb8 100644 --- a/clusters/data_node_type_test.go +++ b/clusters/data_node_type_test.go @@ -3,6 +3,8 @@ package clusters import ( "testing" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -227,19 +229,31 @@ func TestNodeTypeVCPU(t *testing.T) { func TestSmallestNodeTypeClouds(t *testing.T) { assert.Equal(t, "Standard_D3_v2", ClustersAPI{ client: &common.DatabricksClient{ - Host: "foo.azuredatabricks.net", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "foo.azuredatabricks.net", + }, + }, }, }.defaultSmallestNodeType()) assert.Equal(t, "n1-standard-4", ClustersAPI{ client: &common.DatabricksClient{ - Host: "foo.gcp.databricks.com", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "foo.gcp.databricks.com", + }, + }, }, }.defaultSmallestNodeType()) assert.Equal(t, "i3.xlarge", ClustersAPI{ client: &common.DatabricksClient{ - Host: "foo.cloud.databricks.com", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "foo.cloud.databricks.com", + }, + }, }, }.defaultSmallestNodeType()) } diff --git a/clusters/data_zones_test.go b/clusters/data_zones_test.go index 23e7eb3d9c..958e62c27f 100644 --- a/clusters/data_zones_test.go +++ b/clusters/data_zones_test.go @@ -3,7 +3,7 @@ package clusters import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) @@ -37,7 +37,7 @@ func TestZones_404(t *testing.T) { Method: "GET", Resource: "/api/2.0/clusters/list-zones", Status: 404, - Response: common.NotFound("missing"), + Response: apierr.NotFound("missing"), }, }, Read: true, diff --git a/clusters/resource_cluster_test.go b/clusters/resource_cluster_test.go index 458626ed33..1a254e0f8d 100644 --- a/clusters/resource_cluster_test.go +++ b/clusters/resource_cluster_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/libraries" "github.com/databricks/terraform-provider-databricks/qa" @@ -437,7 +437,7 @@ func TestResourceClusterCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/clusters/create", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -517,11 +517,13 @@ func TestResourceClusterRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=abc", - Response: common.APIErrorBody{ - ErrorCode: "NOT_FOUND", - Message: "Item not found", + Response: apierr.APIErrorBody{ + // clusters API is not fully restful, so let's test for that + // TODO: https://github.com/databricks/terraform-provider-databricks/issues/2021 + ErrorCode: "INVALID_STATE", + Message: "Cluster abc does not exist", }, - Status: 404, + Status: 400, }, }, Resource: ResourceCluster(), @@ -537,7 +539,7 @@ func TestResourceClusterRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -1221,7 +1223,7 @@ func TestResourceClusterUpdate_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -1377,7 +1379,7 @@ func TestResourceClusterDelete_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/clusters/delete", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/clusters/resource_library.go b/clusters/resource_library.go index 0a28a420a3..ef83c66089 100644 --- a/clusters/resource_library.go +++ b/clusters/resource_library.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/libraries" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -72,7 +73,7 @@ func ResourceLibrary() *schema.Resource { return nil } } - return common.NotFound(fmt.Sprintf("cannot find %s on %s", libraryRep, clusterID)) + return apierr.NotFound(fmt.Sprintf("cannot find %s on %s", libraryRep, clusterID)) }, Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { clusterID, libraryRep := parseId(d.Id()) @@ -94,7 +95,7 @@ func ResourceLibrary() *schema.Resource { Libraries: []libraries.Library{*v.Library}, }) } - return common.NotFound(fmt.Sprintf("cannot find %s on %s", libraryRep, clusterID)) + return apierr.NotFound(fmt.Sprintf("cannot find %s on %s", libraryRep, clusterID)) }, }.ToResource() } diff --git a/commands/acceptance/commands_test.go b/commands/acceptance/commands_test.go deleted file mode 100644 index 615d162311..0000000000 --- a/commands/acceptance/commands_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package acceptance - -import ( - "context" - "os" - "testing" - - "github.com/databricks/terraform-provider-databricks/commands" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/internal/compute" - "github.com/databricks/terraform-provider-databricks/qa" - "github.com/stretchr/testify/assert" -) - -func TestAccContext(t *testing.T) { - cloud := os.Getenv("CLOUD_ENV") - if cloud == "" { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - client := common.CommonEnvironmentClient() - clusterInfo := compute.NewTinyClusterInCommonPoolPossiblyReused() - clusterID := clusterInfo.ClusterID - ctx := context.Background() - c := commands.NewCommandsAPI(ctx, client) - - result := c.Execute(clusterID, "python", `print('hello world')`) - assert.Equal(t, "hello world", result.Text()) - - // exceptions are regexed away for readability - result = c.Execute(clusterID, "python", `raise Exception("Not Found")`) - qa.AssertErrorStartsWith(t, result.Err(), "Not Found") - - // but errors are not - result = c.Execute(clusterID, "python", `raise KeyError("foo")`) - qa.AssertErrorStartsWith(t, result.Err(), "KeyError: 'foo'") - - // so it is more clear to read and debug - result = c.Execute(clusterID, "python", `return 'hello world'`) - qa.AssertErrorStartsWith(t, result.Err(), "SyntaxError: 'return' outside function") - - result = c.Execute(clusterID, "python", `"Hello World!"`) - assert.Equal(t, "'Hello World!'", result.Text()) - - result = c.Execute(clusterID, "python", ` - print("Hello World!") - dbutils.notebook.exit("success")`) - assert.Equal(t, "success", result.Text()) -} diff --git a/commands/commands_test.go b/commands/commands_test.go index ce3c3239bc..1fc726dcbb 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" @@ -194,7 +195,7 @@ func TestCommandsAPIExecute_FailGettingCluster(t *testing.T) { Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=abc", Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "Does not compute", }, }, @@ -234,7 +235,7 @@ func TestCommandsAPIExecute_FailToCreateContext(t *testing.T) { Method: "POST", Resource: "/api/1.2/contexts/create", Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "Does not compute", }, }, @@ -265,7 +266,7 @@ func TestCommandsAPIExecute_FailToWaitForContext(t *testing.T) { Method: "GET", Resource: "/api/1.2/contexts/status?clusterId=abc&contextId=abc", Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "Does not compute", }, }, @@ -303,7 +304,7 @@ func TestCommandsAPIExecute_FailToCreateCommand(t *testing.T) { Method: "POST", Resource: "/api/1.2/commands/execute", Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "Does not compute", }, }, @@ -348,7 +349,7 @@ func TestCommandsAPIExecute_FailToWaitForCommand(t *testing.T) { Method: "GET", Resource: "/api/1.2/commands/status?clusterId=abc&commandId=abc&contextId=abc", Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "Does not compute", }, }, @@ -400,7 +401,7 @@ func TestCommandsAPIExecute_FailToGetCommand(t *testing.T) { Method: "GET", Resource: "/api/1.2/commands/status?clusterId=abc&commandId=abc&contextId=abc", Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "Does not compute", }, }, @@ -453,7 +454,7 @@ func TestCommandsAPIExecute_FailToDeleteContext(t *testing.T) { Method: "POST", Resource: "/api/1.2/contexts/destroy", Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "Does not compute", }, }, diff --git a/common/azure_auth.go b/common/azure_auth.go deleted file mode 100644 index f7e7d9b54b..0000000000 --- a/common/azure_auth.go +++ /dev/null @@ -1,289 +0,0 @@ -package common - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "strings" - "time" - - "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/adal" - "github.com/Azure/go-autorest/autorest/azure" - "github.com/Azure/go-autorest/autorest/azure/auth" - "github.com/golang-jwt/jwt/v4" -) - -// List of management information -const azureDatabricksProdLoginAppID string = "2ff814a6-3304-4ab8-85cb-cd0e6f879c1d" - -func (aa *DatabricksClient) GetAzureDatabricksLoginAppId() string { - if aa.AzureDatabricksLoginAppId != "" { - return aa.AzureDatabricksLoginAppId - } - return azureDatabricksProdLoginAppID -} - -func (aa *DatabricksClient) GetAzureJwtProperty(key string) (any, error) { - if !aa.IsAzure() { - return "", fmt.Errorf("can't get Azure JWT token in non-Azure environment") - } - if key == "tid" && aa.AzureTenantID != "" { - return aa.AzureTenantID, nil - } - err := aa.Authenticate(context.TODO()) - if err != nil { - return nil, err - } - request, err := http.NewRequest("GET", aa.Host, nil) - if err != nil { - return nil, err - } - if err = aa.authVisitor(request); err != nil { - return nil, err - } - header := request.Header.Get("Authorization") - var stoken string - if len(header) > 0 && strings.HasPrefix(string(header), "Bearer ") { - log.Printf("[DEBUG] Got Bearer token") - stoken = strings.TrimSpace(strings.TrimPrefix(string(header), "Bearer ")) - } - if stoken == "" { - return nil, fmt.Errorf("can't obtain Azure JWT token") - } - if strings.HasPrefix(stoken, "dapi") { - return nil, fmt.Errorf("can't use Databricks PAT") - } - parser := jwt.Parser{SkipClaimsValidation: true} - token, _, err := parser.ParseUnverified(stoken, jwt.MapClaims{}) - if err != nil { - return nil, err - } - claims := token.Claims.(jwt.MapClaims) - v, ok := claims[key] - if !ok { - return nil, fmt.Errorf("can't find field '%s' in parsed JWT", key) - } - return v, nil -} - -func (aa *DatabricksClient) getAzureEnvironment() (azure.Environment, error) { - if aa.AzureEnvironment != nil { - // used for testing purposes - return *aa.AzureEnvironment, nil - } - if aa.AzurermEnvironment == "" { - return azure.PublicCloud, nil - } - envName := fmt.Sprintf("AZURE%sCLOUD", strings.ToUpper(aa.AzurermEnvironment)) - return azure.EnvironmentFromName(envName) -} - -// IsAzureClientSecretSet returns true if client id/secret and tenand id are supplied -func (aa *DatabricksClient) IsAzureClientSecretSet() bool { - return aa.AzureClientID != "" && aa.AzureClientSecret != "" && aa.AzureTenantID != "" -} - -func (aa *DatabricksClient) configureWithAzureClientSecret(ctx context.Context) (func(*http.Request) error, error) { - if !aa.IsAzure() { - return nil, nil - } - if !aa.IsAzureClientSecretSet() { - return nil, nil - } - log.Printf("[INFO] Generating AAD token for Azure Service Principal") - return aa.simpleAADRequestVisitor(ctx, aa.getClientSecretAuthorizer, aa.addSpManagementTokenVisitor) -} - -// variable, so that we can mock it in tests -var msiAvailabilityChecker = adal.MSIAvailable - -func (aa *DatabricksClient) configureWithAzureManagedIdentity(ctx context.Context) (func(*http.Request) error, error) { - if !aa.IsAzure() { - return nil, nil - } - if !aa.AzureUseMSI { - return nil, nil - } - if !msiAvailabilityChecker(ctx, aa.httpClient.HTTPClient) { - return nil, fmt.Errorf("managed identity is not available") - } - log.Printf("[INFO] Using Azure Managed Identity authentication") - return aa.simpleAADRequestVisitor(ctx, func(resource string) (autorest.Authorizer, error) { - return auth.MSIConfig{ - Resource: resource, - }.Authorizer() - }, aa.addSpManagementTokenVisitor) -} - -func (aa *DatabricksClient) addSpManagementTokenVisitor(r *http.Request, management autorest.Authorizer) error { - log.Printf("[DEBUG] Setting 'X-Databricks-Azure-SP-Management-Token' header") - ba, ok := management.(*autorest.BearerAuthorizer) - if !ok { - return fmt.Errorf("supposed to get BearerAuthorizer, but got %#v", management) - } - tokenProvider := ba.TokenProvider() - if tokenProvider == nil { - return fmt.Errorf("token provider is nil") - } - if rf, ok := tokenProvider.(adal.RefresherWithContext); ok { - err := rf.EnsureFreshWithContext(r.Context()) - if err != nil { - return fmt.Errorf("cannot refresh AAD token: %w", err) - } - } - accessToken := tokenProvider.OAuthToken() - r.Header.Set("X-Databricks-Azure-SP-Management-Token", accessToken) - return nil -} - -// go nolint -func (aa *DatabricksClient) simpleAADRequestVisitor( - ctx context.Context, - authorizerFactory func(resource string) (autorest.Authorizer, error), - visitors ...func(r *http.Request, ma autorest.Authorizer) error) (func(r *http.Request) error, error) { - managementAuthorizer, err := authorizerFactory(aa.AzureEnvironment.ServiceManagementEndpoint) - if err != nil { - return nil, fmt.Errorf("cannot authorize management: %w", err) - } - err = aa.ensureWorkspaceURL(ctx, managementAuthorizer) - if err != nil { - return nil, fmt.Errorf("cannot get workspace: %w", err) - } - armDatabricksResourceID := aa.GetAzureDatabricksLoginAppId() - platformAuthorizer, err := authorizerFactory(armDatabricksResourceID) - if err != nil { - return nil, fmt.Errorf("cannot authorize databricks: %w", err) - } - return func(r *http.Request) error { - if len(visitors) > 0 { - err = visitors[0](r, managementAuthorizer) - if err != nil { - return err - } - } - if aa.AzureResourceID != "" { - r.Header.Set("X-Databricks-Azure-Workspace-Resource-Id", aa.AzureResourceID) - } - _, err = autorest.Prepare(r, platformAuthorizer.WithAuthorization()) - if err != nil { - return fmt.Errorf("cannot prepare request: %w", err) - } - return nil - }, nil -} - -func maybeExtendAuthzError(err error) error { - fmtString := "Azure authorization error. Does your SPN have Contributor access to Databricks workspace? %v" - if e, ok := err.(APIError); ok && e.StatusCode == 403 { - return fmt.Errorf(fmtString, err) - } else if strings.Contains(err.Error(), "does not have authorization to perform action") { - return fmt.Errorf(fmtString, err) - } - return err -} - -func (aa *DatabricksClient) ensureWorkspaceURL(ctx context.Context, - managementAuthorizer autorest.Authorizer) error { - if aa.Host != "" { - return nil - } - resourceID := aa.AzureResourceID - if resourceID == "" { - return fmt.Errorf("please set `azure_workspace_resource_id` provider argument") - } - log.Println("[DEBUG] Getting Workspace ID via management token.") - // All azure endpoints typically end with a trailing slash removing it because resourceID starts with slash - managementResourceURL := strings.TrimSuffix(aa.AzureEnvironment.ResourceManagerEndpoint, "/") + resourceID - var workspace azureDatabricksWorkspace - resp, err := aa.genericQuery(ctx, http.MethodGet, - managementResourceURL, - map[string]string{ - "api-version": "2018-04-01", - }, func(r *http.Request) error { - _, err := autorest.Prepare(r, managementAuthorizer.WithAuthorization()) - if err != nil { - return maybeExtendAuthzError(err) - } - return nil - }) - if err != nil { - return maybeExtendAuthzError(err) - } - err = json.Unmarshal(resp, &workspace) - if err != nil { - return err - } - aa.Host = fmt.Sprintf("https://%s/", workspace.Properties.WorkspaceURL) - return nil -} - -func (aa *DatabricksClient) getClientSecretAuthorizer(resource string) (autorest.Authorizer, error) { - if aa.azureAuthorizer != nil { - return aa.azureAuthorizer, nil - } - armDatabricksResourceID := aa.GetAzureDatabricksLoginAppId() - if resource != armDatabricksResourceID { - es := auth.EnvironmentSettings{ - Values: map[string]string{ - auth.ClientID: aa.AzureClientID, - auth.ClientSecret: aa.AzureClientSecret, - auth.TenantID: aa.AzureTenantID, - auth.Resource: resource, - }, - Environment: *aa.AzureEnvironment, - } - return es.GetAuthorizer() - } - platformTokenOAuthCfg, err := adal.NewOAuthConfigWithAPIVersion( - aa.AzureEnvironment.ActiveDirectoryEndpoint, - aa.AzureTenantID, - nil) - if err != nil { - return nil, maybeExtendAuthzError(err) - } - spt, err := adal.NewServicePrincipalToken( - *platformTokenOAuthCfg, - aa.AzureClientID, - aa.AzureClientSecret, - armDatabricksResourceID) - if err != nil { - return nil, maybeExtendAuthzError(err) - } - return autorest.NewBearerAuthorizer(spt), nil -} - -type azureDatabricksWorkspace struct { - Name string `json:"name"` - ID string `json:"id"` - Type string `json:"type"` - Sku struct { - Name string `json:"name"` - } `json:"sku"` - Location string `json:"location"` - Properties struct { - ManagedResourceGroupID string `json:"managedResourceGroupId"` - Parameters any `json:"parameters"` - ProvisioningState string `json:"provisioningState"` - UIDefinitionURI string `json:"uiDefinitionUri"` - Authorizations []struct { - PrincipalID string `json:"principalId"` - RoleDefinitionID string `json:"roleDefinitionId"` - } `json:"authorizations"` - CreatedBy struct { - Oid string `json:"oid"` - Puid string `json:"puid"` - ApplicationID string `json:"applicationId"` - } `json:"createdBy"` - UpdatedBy struct { - Oid string `json:"oid"` - Puid string `json:"puid"` - ApplicationID string `json:"applicationId"` - } `json:"updatedBy"` - CreatedDateTime time.Time `json:"createdDateTime"` - WorkspaceID string `json:"workspaceId"` - WorkspaceURL string `json:"workspaceUrl"` - } `json:"properties"` -} diff --git a/common/azure_auth_test.go b/common/azure_auth_test.go deleted file mode 100644 index fb5fc4c888..0000000000 --- a/common/azure_auth_test.go +++ /dev/null @@ -1,586 +0,0 @@ -package common - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "sync" - "testing" - - "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/adal" - "github.com/Azure/go-autorest/autorest/azure" - "github.com/golang-jwt/jwt/v4" - "github.com/hashicorp/go-retryablehttp" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAddSpManagementTokenVisitor(t *testing.T) { - aa := DatabricksClient{} - r := httptest.NewRequest("GET", "/a/b/c", http.NoBody) - err := aa.addSpManagementTokenVisitor(r, &autorest.BearerAuthorizer{}) - assert.EqualError(t, err, "token provider is nil") -} - -func TestAddSpManagementTokenVisitor_Refreshed(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - - aa := DatabricksClient{} - r := httptest.NewRequest("GET", "/a/b/c", http.NoBody) - rct := refreshableCliToken{ - lock: &sync.RWMutex{}, - resource: "x", - refreshMinutes: 6, - } - err := aa.addSpManagementTokenVisitor(r, autorest.NewBearerAuthorizer(&rct)) - require.NoError(t, err) - assert.Equal(t, "...", r.Header[http.CanonicalHeaderKey("X-Databricks-Azure-SP-Management-Token")][0]) -} - -func TestAddSpManagementTokenVisitor_RefreshedError(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("FAIL", "yes") - - aa := DatabricksClient{} - r := httptest.NewRequest("GET", "/a/b/c", http.NoBody) - rct := refreshableCliToken{ - lock: &sync.RWMutex{}, - resource: "x", - refreshMinutes: 6, - } - err := aa.addSpManagementTokenVisitor(r, autorest.NewBearerAuthorizer(&rct)) - require.EqualError(t, err, "cannot refresh AAD token: cannot get access token: This is just a failing script.\n") - - err = aa.addSpManagementTokenVisitor(r, autorest.NewBasicAuthorizer("a", "b")) - require.Error(t, err) -} - -func TestGetClientSecretAuthorizer(t *testing.T) { - aa := DatabricksClient{} - env, err := aa.getAzureEnvironment() - require.NoError(t, err) - aa.AzureEnvironment = &env - auth, err := aa.getClientSecretAuthorizer(azureDatabricksProdLoginAppID) - require.Nil(t, auth) - require.EqualError(t, err, "parameter 'clientID' cannot be empty") - - aa.AzureTenantID = "a" - aa.AzureClientID = "b" - aa.AzureClientSecret = "c" - auth, err = aa.getClientSecretAuthorizer(azureDatabricksProdLoginAppID) - require.NotNil(t, auth) - require.NoError(t, err) - - auth, err = aa.getClientSecretAuthorizer(env.ServiceManagementEndpoint) - require.NotNil(t, auth) - require.NoError(t, err) -} - -func TestEnsureWorkspaceURL_CornerCases(t *testing.T) { - aa := DatabricksClient{} - env, err := aa.getAzureEnvironment() - require.NoError(t, err) - aa.AzureEnvironment = &env - - err = aa.ensureWorkspaceURL(context.Background(), nil) - assert.EqualError(t, err, "please set `azure_workspace_resource_id` provider argument") -} - -func TestDatabricksClient_ensureWorkspaceURL(t *testing.T) { - aa := DatabricksClient{InsecureSkipVerify: true} - aa.configureHTTPCLient() - env, err := aa.getAzureEnvironment() - require.NoError(t, err) - aa.AzureEnvironment = &env - - cnt := []int{0} - var serverURL string - server := httptest.NewUnstartedServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - if req.RequestURI == - "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c?api-version=2018-04-01" { - _, err := rw.Write([]byte(fmt.Sprintf(`{"properties": {"workspaceUrl": "%s"}}`, - strings.ReplaceAll(serverURL, "https://", "")))) - assert.NoError(t, err) - cnt[0]++ - return - } - assert.Fail(t, fmt.Sprintf("Received unexpected call: %s %s", - req.Method, req.RequestURI)) - })) - server.StartTLS() - serverURL = server.URL - defer server.Close() - - aa.AzureResourceID = "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c" - // resource management endpoints end with a trailing slash in url - aa.AzureEnvironment = &azure.Environment{ - ResourceManagerEndpoint: fmt.Sprintf("%s/", server.URL), - } - - token := &adal.Token{ - AccessToken: "TestToken", - Resource: "https://azure.microsoft.com/", - Type: "Bearer", - } - authorizer := autorest.NewBearerAuthorizer(token) - err = aa.ensureWorkspaceURL(context.Background(), authorizer) - assert.NoError(t, err) - - err = aa.ensureWorkspaceURL(context.Background(), authorizer) - assert.NoError(t, err) - assert.Equal(t, 1, cnt[0], - "Calls to Azure Management API must be done only once") -} - -func TestDatabricksClient_configureWithClientSecretAAD(t *testing.T) { - client := DatabricksClient{InsecureSkipVerify: true} - client.AzureResourceID = "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c" - - token := &adal.Token{ - AccessToken: "TestToken", - Resource: "https://azure.microsoft.com/", - Type: "Bearer", - } - client.azureAuthorizer = autorest.NewBearerAuthorizer(token) - - var serverURL string - server := httptest.NewUnstartedServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - if req.RequestURI == - "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c?api-version=2018-04-01" { - _, err := rw.Write([]byte(fmt.Sprintf(`{"properties": {"workspaceUrl": "%s"}}`, - strings.ReplaceAll(serverURL, "https://", "")))) - assert.NoError(t, err) - return - } - if req.RequestURI == "/api/2.0/clusters/list-zones" { - assert.Equal(t, token.AccessToken, req.Header.Get("X-Databricks-Azure-SP-Management-Token")) - assert.Equal(t, "Bearer "+token.AccessToken, req.Header.Get("Authorization")) - assert.Equal(t, client.AzureResourceID, req.Header.Get("X-Databricks-Azure-Workspace-Resource-Id")) - _, err := rw.Write([]byte(`{"zones": ["a", "b", "c"]}`)) - assert.NoError(t, err) - return - } - assert.Fail(t, fmt.Sprintf("Received unexpected call: %s %s", - req.Method, req.RequestURI)) - })) - server.StartTLS() - serverURL = server.URL - defer server.Close() - - client.AzureClientID = "a" - client.AzureClientSecret = "b" - client.AzureTenantID = "c" - // resource management endpoints end with a trailing slash in url - client.AzureEnvironment = &azure.Environment{ - ResourceManagerEndpoint: fmt.Sprintf("%s/", server.URL), - } - client.configureHTTPCLient() - ctx := context.Background() - auth, err := client.configureWithAzureClientSecret(ctx) - assert.NoError(t, err) - - client.authVisitor = auth - client.configureHTTPCLient() - - type ZonesInfo struct { - Zones []string `json:"zones,omitempty"` - DefaultZone string `json:"default_zone,omitempty"` - } - var zi ZonesInfo - err = client.Get(context.Background(), "/clusters/list-zones", nil, &zi) - assert.NotNil(t, zi) - assert.NoError(t, err) - assert.Len(t, zi.Zones, 3) -} - -func TestAzureEnvironment_WithAzureManagementEndpoint(t *testing.T) { - fakeEndpoint := "http://google.com" - aa := DatabricksClient{ - AzureEnvironment: &azure.Environment{ - ResourceManagerEndpoint: fakeEndpoint, - }, - } - env, err := aa.getAzureEnvironment() - assert.Nil(t, err) - // This value should be populated with azureManagementEndpoint for testing - assert.Equal(t, env.ResourceManagerEndpoint, fakeEndpoint) - // The rest should be nill - assert.Equal(t, env.ActiveDirectoryEndpoint, "") - - // Making the azureManagementEndpoint empty should yield PublicCloud - aa.AzureEnvironment = nil - env, err = aa.getAzureEnvironment() - assert.Nil(t, err) - assert.Equal(t, azure.PublicCloud, env) -} - -func TestAzureEnvironment(t *testing.T) { - aa := DatabricksClient{} - env, err := aa.getAzureEnvironment() - - assert.Nil(t, err) - assert.Equal(t, azure.PublicCloud, env) - - aa.AzurermEnvironment = "public" - env, err = aa.getAzureEnvironment() - assert.Nil(t, err) - assert.Equal(t, azure.PublicCloud, env) - - aa.AzurermEnvironment = "china" - env, err = aa.getAzureEnvironment() - assert.Nil(t, err) - assert.Equal(t, azure.ChinaCloud, env) - - aa.AzurermEnvironment = "german" - env, err = aa.getAzureEnvironment() - assert.Nil(t, err) - assert.Equal(t, azure.GermanCloud, env) - - aa.AzurermEnvironment = "usgovernment" - env, err = aa.getAzureEnvironment() - assert.Nil(t, err) - assert.Equal(t, azure.USGovernmentCloud, env) - - aa.AzurermEnvironment = "xyzdummy" - _, err = aa.getAzureEnvironment() - assert.NotNil(t, err) -} - -func TestMaybeExtendError(t *testing.T) { - err := fmt.Errorf("Some test") - err2 := maybeExtendAuthzError(err) - - assert.EqualError(t, err2, err.Error()) - - msg := "Azure authorization error. Does your SPN" - - err = fmt.Errorf("something does not have authorization to perform action abc") - err2 = maybeExtendAuthzError(err) - assert.True(t, strings.HasPrefix(err2.Error(), msg), err2.Error()) - - err = APIError{StatusCode: 403} - err2 = maybeExtendAuthzError(err) - assert.True(t, strings.HasPrefix(err2.Error(), msg), err2.Error()) -} - -func TestGetJWTProperty_AzureCLI_SP(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - - aa := DatabricksClient{ - AzureClientID: "a", - AzureClientSecret: "b", - AzureTenantID: "c", - Host: "https://adb-1232.azuredatabricks.net", - } - tid, err := aa.GetAzureJwtProperty("tid") - assert.NoError(t, err) - assert.Equal(t, "c", tid) -} - -func TestGetJWTProperty_NonAzure(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - - aa := DatabricksClient{ - Host: "https://abc.cloud.databricks.com", - Token: "abc", - } - _, err := aa.GetAzureJwtProperty("tid") - require.EqualError(t, err, "can't get Azure JWT token in non-Azure environment") -} - -func TestGetJWTProperty_AzureCli_Error(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - - // token without expiry in this case - client, server := singleRequestServer(t, "POST", "/api/2.0/token/create", `{ - "token_value": "abc" - }`) - defer server.Close() - - client.Host = "https://adb-1232.azuredatabricks.net" - - _, err := client.GetAzureJwtProperty("tid") - require.EqualError(t, err, "unexpected end of JSON input") -} - -func setupJwtTestClient() (*httptest.Server, *DatabricksClient) { - client := DatabricksClient{InsecureSkipVerify: true, - Host: "https://adb-1232.azuredatabricks.net", - } - ctx := context.Background() - - server := httptest.NewUnstartedServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - })) - server.StartTLS() - - // resource management endpoints end with a trailing slash in url - client.AzureEnvironment = &azure.Environment{ - ResourceManagerEndpoint: fmt.Sprintf("%s/", server.URL), - } - auth, err := client.configureWithAzureCLI(ctx) - if err != nil || auth == nil { - return nil, nil - } - - client.authVisitor = auth - client.configureHTTPCLient() - - return server, &client -} - -func newTestJwt(t *testing.T, claims jwt.MapClaims) string { - token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims) - result, err := token.SignedString([]byte("_")) - assert.NoError(t, err) - return result -} - -func TestGetJWTProperty_AzureCli(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("TF_AAD_TOKEN", newTestJwt(t, jwt.MapClaims{ - "tid": "some-tenant", - })) - - srv, client := setupJwtTestClient() - assert.NotNil(t, srv) - defer srv.Close() - assert.NotNil(t, client) - - tid, err := client.GetAzureJwtProperty("tid") - require.NoError(t, err) - assert.Equal(t, "some-tenant", tid.(string)) -} - -func TestGetJWTProperty_Authenticate_Fail(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("FAIL", "yes") - - client := &DatabricksClient{ - Host: "https://adb-1232.azuredatabricks.net", - } - _, err := client.GetAzureJwtProperty("tid") - require.EqualError(t, err, "cannot configure azure-cli auth: "+ - "Invoking Azure CLI failed with the following error: "+ - "This is just a failing script.\n. "+ - "Please check https://registry.terraform.io/providers/"+ - "databricks/databricks/latest/docs#authentication for details") -} - -func TestGetJWTProperty_makeGetRequest_Fail(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("TF_AAD_TOKEN", newTestJwt(t, jwt.MapClaims{ - "tid": "some-tenant", - })) - - srv, client := setupJwtTestClient() - assert.NotNil(t, srv) - defer srv.Close() - assert.NotNil(t, client) - client.Host = "%🙀.azuredatabricks.net" - - _, err := client.GetAzureJwtProperty("tid") - require.EqualError(t, err, `parse "%🙀.azuredatabricks.net": invalid URL escape "%\xf0\x9f"`) -} - -func TestGetJWTProperty_authVisitor_Fail(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - - client := &DatabricksClient{ - Host: "https://adb-1232.azuredatabricks.net", - authVisitor: func(r *http.Request) error { - return fmt.Errorf("fails for the test") - }, - } - _, err := client.GetAzureJwtProperty("tid") - require.EqualError(t, err, "fails for the test") -} - -func TestGetJWTProperty_AzureCli_Error_DB_PAT(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("TF_AAD_TOKEN", "dapi123") - - srv, client := setupJwtTestClient() - assert.NotNil(t, srv) - defer srv.Close() - assert.NotNil(t, client) - - _, err := client.GetAzureJwtProperty("tid") - require.EqualError(t, err, "can't use Databricks PAT") -} - -func TestGetJWTProperty_AzureCli_Error_No_TenantID(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("TF_AAD_TOKEN", newTestJwt(t, jwt.MapClaims{ - "something": "else", - })) - - srv, client := setupJwtTestClient() - assert.NotNil(t, srv) - defer srv.Close() - assert.NotNil(t, client) - - _, err := client.GetAzureJwtProperty("tid") - require.EqualError(t, err, "can't find field 'tid' in parsed JWT") -} - -func TestGetJWTProperty_AzureCli_Error_EmptyToken(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("TF_AAD_TOKEN", " ") - - srv, client := setupJwtTestClient() - assert.NotNil(t, srv) - defer srv.Close() - assert.NotNil(t, client) - - _, err := client.GetAzureJwtProperty("tid") - require.EqualError(t, err, "can't obtain Azure JWT token") -} - -func TestNoMsiAvailable(t *testing.T) { - msiAvailabilityChecker = func(ctx context.Context, s adal.Sender) bool { - // without this shim test will fail on GitHub, as it runs on Azure - return false - } - _, err := (&DatabricksClient{ - AzureResourceID: "/a/b/c", - AzureUseMSI: true, - httpClient: &retryablehttp.Client{ - HTTPClient: http.DefaultClient, - RetryMax: 0, - CheckRetry: func(ctx context.Context, resp *http.Response, err error) (bool, error) { - return false, err - }, - }, - }).configureWithAzureManagedIdentity(context.Background()) - assert.EqualError(t, err, "managed identity is not available") -} - -func TestMsiWorks(t *testing.T) { - msiAvailabilityChecker = func(ctx context.Context, s adal.Sender) bool { - return true - } - auth, err := (&DatabricksClient{ - AzureResourceID: "/a/b/c", - AzureUseMSI: true, - Host: "abc.azuredatabricks.net", - AzureEnvironment: &azure.Environment{ - ResourceManagerEndpoint: fmt.Sprintf("%s/", "http://localhost"), - ServiceManagementEndpoint: "sm", - }, - httpClient: &retryablehttp.Client{ - HTTPClient: http.DefaultClient, - RetryMax: 0, - }, - }).configureWithAzureManagedIdentity(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, auth) -} - -func TestSimpleAADRequestVisitor_FailManagement(t *testing.T) { - _, err := (&DatabricksClient{ - AzureEnvironment: &azure.Environment{ - ServiceManagementEndpoint: "x", - }, - }).simpleAADRequestVisitor(context.Background(), - func(resource string) (autorest.Authorizer, error) { - return nil, fmt.Errorf("🤨") - }) - assert.EqualError(t, err, "cannot authorize management: 🤨") -} - -func TestSimpleAADRequestVisitor_FailWorkspaceUrl(t *testing.T) { - _, err := (&DatabricksClient{ - AzureEnvironment: &azure.Environment{ - ServiceManagementEndpoint: "x", - }, - }).simpleAADRequestVisitor(context.Background(), - func(resource string) (autorest.Authorizer, error) { - return autorest.NullAuthorizer{}, nil - }) - assert.EqualError(t, err, "cannot get workspace: please set `azure_workspace_resource_id` provider argument") -} - -func TestSimpleAADRequestVisitor_FailPlatformAuth(t *testing.T) { - _, err := (&DatabricksClient{ - Host: "abc.azuredatabricks.net", - AzureEnvironment: &azure.Environment{ - ServiceManagementEndpoint: "x", - }, - }).simpleAADRequestVisitor(context.Background(), - func(resource string) (autorest.Authorizer, error) { - if resource == azureDatabricksProdLoginAppID { - return nil, fmt.Errorf("🤨") - } - return autorest.NullAuthorizer{}, nil - }) - assert.EqualError(t, err, "cannot authorize databricks: 🤨") -} - -func TestSimpleAADRequestVisitor_ProdLoginAppId(t *testing.T) { - aa := DatabricksClient{ - Host: "abc.azuredatabricks.net", - AzureEnvironment: &azure.Environment{ - ServiceManagementEndpoint: "x", - }, - } - _, err := aa.simpleAADRequestVisitor(context.Background(), - func(resource string) (autorest.Authorizer, error) { - if resource == "x" { - return autorest.NullAuthorizer{}, nil - } - assert.Equal(t, azureDatabricksProdLoginAppID, resource) - return autorest.NullAuthorizer{}, nil - }) - assert.Nil(t, err) -} - -func TestSimpleAADRequestVisitor_LoginAppIdOverride(t *testing.T) { - _, err := (&DatabricksClient{ - Host: "abc.azuredatabricks.net", - AzureEnvironment: &azure.Environment{ - ServiceManagementEndpoint: "x", - }, - AzureDatabricksLoginAppId: "y", - }).simpleAADRequestVisitor(context.Background(), - func(resource string) (autorest.Authorizer, error) { - if resource == "x" { - return autorest.NullAuthorizer{}, nil - } - assert.Equal(t, "y", resource) - return autorest.NullAuthorizer{}, nil - }) - assert.Nil(t, err) -} diff --git a/common/azure_cli_auth.go b/common/azure_cli_auth.go deleted file mode 100644 index 4c818513b9..0000000000 --- a/common/azure_cli_auth.go +++ /dev/null @@ -1,116 +0,0 @@ -package common - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "os/exec" - "strings" - "sync" - "time" - - "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/adal" - "github.com/Azure/go-autorest/autorest/azure/cli" -) - -type refreshableCliToken struct { - resource string - token *adal.Token - lock *sync.RWMutex - refreshMinutes int -} - -// OAuthToken implements adal.OAuthTokenProvider -func (rct *refreshableCliToken) OAuthToken() string { - if rct.token == nil { - return "" - } - return rct.token.OAuthToken() -} - -// EnsureFreshWithContext implements adal.RefresherWithContext -func (rct *refreshableCliToken) EnsureFreshWithContext(ctx context.Context) error { - refreshInterval := time.Duration(rct.refreshMinutes) * time.Minute - if rct.token != nil && !rct.token.WillExpireIn(refreshInterval) { - return nil - } - rct.lock.Lock() - defer rct.lock.Unlock() - if rct.token != nil && !rct.token.WillExpireIn(refreshInterval) { - return nil - } - return rct.refreshInternal(rct.resource) -} - -// RefreshWithContext implements adal.RefresherWithContext -func (rct *refreshableCliToken) RefreshWithContext(ctx context.Context) error { - rct.lock.Lock() - defer rct.lock.Unlock() - return rct.refreshInternal(rct.resource) -} - -// RefreshExchangeWithContext implements adal.RefresherWithContext -func (rct *refreshableCliToken) RefreshExchangeWithContext(ctx context.Context, resource string) error { - rct.lock.Lock() - defer rct.lock.Unlock() - return rct.refreshInternal(rct.resource) -} - -func (rct *refreshableCliToken) refreshInternal(resource string) error { - out, err := exec.Command("az", "account", "get-access-token", "--resource", resource, "--output", "json").Output() - if ee, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("cannot get access token: %s", string(ee.Stderr)) - } - if err != nil { - return fmt.Errorf("cannot get access token: %v", err) - } - var cliToken cli.Token - err = json.Unmarshal(out, &cliToken) - if err != nil { - return fmt.Errorf("cannot unmarshal CLI result: %w", err) - } - token, err := cliToken.ToADALToken() - if err != nil { - return fmt.Errorf("cannot convert to ADAL token: %w", err) - } - log.Printf("[INFO] Refreshed OAuth token for %s from Azure CLI, which expires on %s", resource, cliToken.ExpiresOn) - rct.token = &token - return nil -} - -func (aa *DatabricksClient) cliAuthorizer(resource string) (autorest.Authorizer, error) { - rct := refreshableCliToken{ - lock: &sync.RWMutex{}, - resource: resource, - refreshMinutes: 6, - } - err := rct.refreshInternal(resource) - if err != nil { - return nil, fmt.Errorf("cannot refresh: %w", err) - } - return autorest.NewBearerAuthorizer(&rct), nil -} - -func (aa *DatabricksClient) configureWithAzureCLI(ctx context.Context) (func(*http.Request) error, error) { - if !aa.IsAzure() { - return nil, nil - } - if aa.IsAzureClientSecretSet() { - return nil, nil - } - // verify that Azure CLI is authenticated - armDatabricksResourceID := aa.GetAzureDatabricksLoginAppId() - _, err := cli.GetTokenFromCLI(armDatabricksResourceID) - if err != nil { - if strings.Contains(err.Error(), "executable file not found") { - return nil, fmt.Errorf("most likely Azure CLI is not installed. " + - "See https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest for details") - } - return nil, err - } - log.Printf("[INFO] Using Azure CLI authentication with AAD tokens") - return aa.simpleAADRequestVisitor(ctx, aa.cliAuthorizer) -} diff --git a/common/azure_cli_auth_test.go b/common/azure_cli_auth_test.go deleted file mode 100644 index 5e80450f2b..0000000000 --- a/common/azure_cli_auth_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package common - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "sync" - "testing" - - "github.com/Azure/go-autorest/autorest/adal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAzureCliAuth(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - // fake expiration date for az mock cli - os.Setenv("EXPIRE", "15M") - - cnt := []int{0} - server := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - cnt[0]++ - if req.RequestURI == "/api/2.0/clusters/list-zones" { - assert.Equal(t, "Bearer ...", req.Header.Get("Authorization")) - _, err := rw.Write([]byte(`{"zones": ["a", "b", "c"]}`)) - assert.NoError(t, err) - return - } - assert.Fail(t, fmt.Sprintf("Received unexpected call: %s %s", - req.Method, req.RequestURI)) - })) - defer server.Close() - - client := DatabricksClient{ - Host: server.URL, - AzureResourceID: "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c", - InsecureSkipVerify: true, - } - err := client.Configure() - assert.NoError(t, err) - - type ZonesInfo struct { - Zones []string `json:"zones,omitempty"` - DefaultZone string `json:"default_zone,omitempty"` - } - var zi ZonesInfo - err = client.Get(context.Background(), "/clusters/list-zones", nil, &zi) - assert.NoError(t, err) - assert.NotNil(t, zi) - assert.Len(t, zi.Zones, 3) - - err = client.Get(context.Background(), "/clusters/list-zones", nil, &zi) - assert.NoError(t, err) - - assert.Equal(t, 2, cnt[0], "There should be only one HTTP call") -} - -func TestOAuthToken_CornerCases(t *testing.T) { - rct := refreshableCliToken{} - assert.Empty(t, rct.OAuthToken()) -} - -func TestEnsureFreshWithContext(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - - rct := refreshableCliToken{ - token: &adal.Token{ - ExpiresIn: "10", - }, - lock: &sync.RWMutex{}, - } - err := rct.EnsureFreshWithContext(context.Background()) - assert.NoError(t, err) -} - -func TestRefreshWithContext(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - - rct := refreshableCliToken{ - token: &adal.Token{ - ExpiresIn: "10", - }, - lock: &sync.RWMutex{}, - } - err := rct.RefreshWithContext(context.Background()) - assert.NoError(t, err) -} - -func TestRefreshExchangeWithContext(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - - rct := refreshableCliToken{ - token: &adal.Token{ - ExpiresIn: "10", - }, - lock: &sync.RWMutex{}, - } - err := rct.RefreshExchangeWithContext(context.Background(), "a") - assert.NoError(t, err) -} - -func TestInternalRefresh_ExitError(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("FAIL", "yes") - - rct := refreshableCliToken{ - token: &adal.Token{ - ExpiresIn: "10", - }, - lock: &sync.RWMutex{}, - } - err := rct.refreshInternal("a") - assert.EqualError(t, err, "cannot get access token: This is just a failing script.\n") -} - -func TestInternalRefresh_OtherError(t *testing.T) { - defer CleanupEnvironment()() - os.Setenv("PATH", "whatever") - - rct := refreshableCliToken{ - token: &adal.Token{ - ExpiresIn: "10", - }, - lock: &sync.RWMutex{}, - } - err := rct.refreshInternal("a") - assert.EqualError(t, err, "cannot get access token: exec: \"az\": executable file not found in $PATH") -} - -func TestInternalRefresh_Corrupt(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("FAIL", "corrupt") - - rct := refreshableCliToken{ - token: &adal.Token{ - ExpiresIn: "10", - }, - lock: &sync.RWMutex{}, - } - err := rct.refreshInternal("a") - assert.EqualError(t, err, "cannot unmarshal CLI result: invalid character 'a' looking for beginning of object key string") -} - -func TestInternalRefresh_CorruptExpire(t *testing.T) { - defer CleanupEnvironment()() - p, _ := filepath.Abs("./testdata") - os.Setenv("PATH", p+":/bin") - os.Setenv("EXPIRE", "corrupt") - - rct := refreshableCliToken{ - token: &adal.Token{ - ExpiresIn: "10", - }, - lock: &sync.RWMutex{}, - } - err := rct.refreshInternal("a") - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "cannot convert to ADAL token: Error parsing Token Expiration Date"), - "Actual message: %s", err.Error()) -} - -func TestConfigureWithAzureCLI_SP(t *testing.T) { - aa := DatabricksClient{ - AzureClientID: "a", - AzureClientSecret: "b", - AzureTenantID: "c", - AzureResourceID: "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c", - } - ctx := context.Background() - auth, err := aa.configureWithAzureCLI(ctx) - assert.NoError(t, err) - assert.Nil(t, auth) -} - -func TestCliAuthorizer_Error(t *testing.T) { - defer CleanupEnvironment()() - os.Setenv("PATH", "whatever") - aa := DatabricksClient{} - _, err := aa.cliAuthorizer("x") - require.Error(t, err) - require.EqualError(t, err, "cannot refresh: cannot get access token: exec: \"az\": executable file not found in $PATH") -} diff --git a/common/client.go b/common/client.go index 66301448b4..6d6f18e41a 100644 --- a/common/client.go +++ b/common/client.go @@ -2,34 +2,15 @@ package common import ( "context" - "crypto/tls" - "encoding/base64" "fmt" "log" "net/http" - "net/url" - "os" - "reflect" "strings" - "sync" - "time" - "golang.org/x/time/rate" - "google.golang.org/api/option" - - "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/azure" - "github.com/hashicorp/go-retryablehttp" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/mitchellh/go-homedir" - "gopkg.in/ini.v1" -) - -// Default settings -const ( - DefaultTruncateBytes = 96 - DefaultRateLimitPerSecond = 15 - DefaultHTTPTimeoutSeconds = 60 + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" + "github.com/golang-jwt/jwt/v4" ) // DatabricksClient holds properties needed for authentication and HTTP client setup @@ -37,477 +18,97 @@ const ( // can hold one or more coma-separated env variable names to find value, if not specified // directly. `auth` struct tag describes the type of conflicting authentication used. type DatabricksClient struct { - Host string `name:"host" env:"DATABRICKS_HOST"` - Token string `name:"token" env:"DATABRICKS_TOKEN" auth:"token,sensitive"` - Username string `name:"username" env:"DATABRICKS_USERNAME" auth:"password"` - Password string `name:"password" env:"DATABRICKS_PASSWORD" auth:"password,sensitive"` - - ClientID string `name:"client_id" env:"DATABRICKS_CLIENT_ID" auth:"oauth"` - ClientSecret string `name:"client_secret" env:"DATABRICKS_CLIENT_SECRET" auth:"oauth,sensitive"` - TokenEndpoint string `name:"token_endpoint" env:"DATABRICKS_TOKEN_ENDPOINT" auth:"oauth"` - - // Databricks Account ID for Accounts API. This field is used in dependencies. - AccountID string `name:"account_id" env:"DATABRICKS_ACCOUNT_ID"` - - // Connection profile specified within ~/.databrickscfg. - Profile string `name:"profile" env:"DATABRICKS_CONFIG_PROFILE" auth:"config profile"` - - // Location of the Databricks CLI credentials file, that is created - // by `databricks configure --token` command. By default, it is located - // in ~/.databrickscfg. - ConfigFile string `name:"config_file" env:"DATABRICKS_CONFIG_FILE"` - - GoogleServiceAccount string `name:"google_service_account" env:"DATABRICKS_GOOGLE_SERVICE_ACCOUNT" auth:"google"` - GoogleCredentials string `name:"google_credentials" env:"GOOGLE_CREDENTIALS" auth:"google,sensitive"` - - AzureResourceID string `name:"azure_workspace_resource_id" env:"DATABRICKS_AZURE_RESOURCE_ID" auth:"azure"` - AzureUseMSI bool `name:"azure_use_msi" env:"ARM_USE_MSI" auth:"azure"` - AzureClientSecret string `name:"azure_client_secret" env:"ARM_CLIENT_SECRET" auth:"azure,sensitive"` - AzureClientID string `name:"azure_client_id" env:"ARM_CLIENT_ID" auth:"azure"` - AzureTenantID string `name:"azure_tenant_id" env:"ARM_TENANT_ID" auth:"azure"` - AzurermEnvironment string `name:"azure_environment" env:"ARM_ENVIRONMENT"` - AzureDatabricksLoginAppId string `name:"azure_login_app_id" env:"DATABRICKS_AZURE_LOGIN_APP_ID" auth:"azure"` - - // When multiple auth attributes are available in the environment, use the auth type - // specified by this argument. This argument also holds currently selected auth. - AuthType string `name:"auth_type" auth:"-"` - - // Azure Environment endpoints - AzureEnvironment *azure.Environment - - // Skip SSL certificate verification for HTTP calls. - // Use at your own risk or for unit testing purposes. - InsecureSkipVerify bool `name:"skip_verify" auth:"-"` - HTTPTimeoutSeconds int `name:"http_timeout_seconds" auth:"-"` - - // Truncate JSON fields in JSON above this limit. Default is 96. - DebugTruncateBytes int `name:"debug_truncate_bytes" env:"DATABRICKS_DEBUG_TRUNCATE_BYTES" auth:"-"` - - // Debug HTTP headers of requests made by the provider. Default is false. - DebugHeaders bool `name:"debug_headers" env:"DATABRICKS_DEBUG_HEADERS" auth:"-"` - - // Maximum number of requests per second made to Databricks REST API. - RateLimitPerSecond int `name:"rate_limit" env:"DATABRICKS_RATE_LIMIT" auth:"-"` - - // OAuth token refreshers for Azure to be used within `authVisitor` - azureAuthorizer autorest.Authorizer - - // options used to enable unit testing mode for OIDC - googleAuthOptions []option.ClientOption - - // Mutex used by Authenticate method to guard `authVisitor`, which - // has to be lazily created on the first request to Databricks API. - // It is done because databricks host and token may often be available - // only in the middle of Terraform DAG execution. - authMutex sync.Mutex - - // HTTP request interceptor, that assigns Authorization header - authVisitor func(r *http.Request) error - - // Databricks REST API rate limiter - rateLimiter *rate.Limiter - - // Terraform provider instance to include Terraform binary version in - // User-Agent header - Provider *schema.Provider - - // retryalble HTTP client - httpClient *retryablehttp.Client - - // configuration attributes that were used to initialise client. - configAttributesUsed []string + *client.DatabricksClient // callback used to create API1.2 call wrapper, which simplifies unit tessting commandFactory func(context.Context, *DatabricksClient) CommandExecutor } -type ConfigAttribute struct { - Name string - Kind reflect.Kind - EnvVars []string - Auth string - Sensitive bool - Internal bool - num int +func (c *DatabricksClient) WorkspaceClient() (*databricks.WorkspaceClient, error) { + return databricks.NewWorkspaceClient((*databricks.Config)(c.DatabricksClient.Config)) } -func (ca *ConfigAttribute) Set(client *DatabricksClient, i any) error { - rv := reflect.ValueOf(client) - field := rv.Elem().Field(ca.num) - switch ca.Kind { - case reflect.String: - field.SetString(i.(string)) - case reflect.Bool: - field.SetBool(i.(bool)) - case reflect.Int: - field.SetInt(int64(i.(int))) - default: - // must extensively test with providerFixture to avoid this one - return fmt.Errorf("cannot set %s of unknown type %s", ca.Name, reflectKind(ca.Kind)) - } - return nil +// Get on path +func (c *DatabricksClient) Get(ctx context.Context, path string, request any, response any) error { + return c.Do(ctx, http.MethodGet, path, request, response, c.addApiPrefix) } -func (ca *ConfigAttribute) GetString(client *DatabricksClient) string { - rv := reflect.ValueOf(client) - field := rv.Elem().Field(ca.num) - return fmt.Sprintf("%v", field.Interface()) +// Post on path +func (c *DatabricksClient) Post(ctx context.Context, path string, request any, response any) error { + return c.Do(ctx, http.MethodPost, path, request, response, c.addApiPrefix) } -// ClientAttributes returns meta-representation of DatabricksClient configuration options -func ClientAttributes() (attrs []ConfigAttribute) { - t := reflect.TypeOf(DatabricksClient{}) - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - nameTag := field.Tag.Get("name") - if nameTag == "" { - continue - } - sensitive := false - auth := field.Tag.Get("auth") - authSplit := strings.Split(auth, ",") - if len(authSplit) == 2 { - auth = authSplit[0] - sensitive = authSplit[1] == "sensitive" - } - // internal config fields are skipped in debugging - internal := false - if auth == "-" { - auth = "" - internal = true - } - attr := ConfigAttribute{ - Name: nameTag, - Auth: auth, - Kind: field.Type.Kind(), - Sensitive: sensitive, - Internal: internal, - num: i, - } - envTag := field.Tag.Get("env") - if envTag != "" { - attr.EnvVars = strings.Split(envTag, ",") - } - attrs = append(attrs, attr) - } - return +// Delete on path +func (c *DatabricksClient) Delete(ctx context.Context, path string, request any) error { + return c.Do(ctx, http.MethodDelete, path, request, nil, c.addApiPrefix) } -// Configure client to work, optionally specifying configuration attributes used -func (c *DatabricksClient) Configure(attrsUsed ...string) error { - c.configAttributesUsed = attrsUsed - c.configureHTTPCLient() - if c.DebugTruncateBytes == 0 { - c.DebugTruncateBytes = DefaultTruncateBytes - } - // AzureEnvironment could be used in the different contexts, not only for Auzre Authentication - // lack of this lead to crash (see issue #831) - azureEnvironment, err := c.getAzureEnvironment() - if err != nil { - return fmt.Errorf("cannot get azure environment: %w", err) - } - c.AzureEnvironment = &azureEnvironment - - return nil +// Patch on path +func (c *DatabricksClient) Patch(ctx context.Context, path string, request any) error { + return c.Do(ctx, http.MethodPatch, path, request, nil, c.addApiPrefix) } -func (c *DatabricksClient) configDebugString() string { - debug := []string{} - for _, attr := range ClientAttributes() { - if attr.Internal && !c.DebugHeaders { - continue - } - value := attr.GetString(c) - if value == "" { - continue - } - if attr.Name == "azure_use_msi" && value == "false" { - // include Azure MSI info only when it's relevant - continue - } - if attr.Sensitive { - value = "***REDACTED***" - } - debug = append(debug, fmt.Sprintf("%s=%v", attr.Name, value)) - } - return strings.Join(debug, ", ") // lgtm[go/clear-text-logging] +// Put on path +func (c *DatabricksClient) Put(ctx context.Context, path string, request any) error { + return c.Do(ctx, http.MethodPut, path, request, nil, c.addApiPrefix) } -// Authenticate lazily authenticates across authorizers or returns error -func (c *DatabricksClient) Authenticate(ctx context.Context) error { - if c.authVisitor != nil { - return nil - } - c.authMutex.Lock() - defer c.authMutex.Unlock() - if c.authVisitor != nil { - return nil - } - // Fix host prior to auth, because it may be used in the OIDC flow as "audience" field. - // If necessary, this function adds a scheme and strips a trailing slash. - if err := c.fixHost(); err != nil { - return err - } - type auth struct { - configure func(context.Context) (func(*http.Request) error, error) - name string - } - providers := []auth{ - {c.configureWithPat, "pat"}, - {c.configureWithBasicAuth, "basic"}, - {c.configureWithOAuthM2M, "oauth-m2m"}, - {c.configureWithAzureClientSecret, "azure-client-secret"}, - {c.configureWithAzureManagedIdentity, "azure-msi"}, - {c.configureWithAzureCLI, "azure-cli"}, - {c.configureWithGoogleCrendentials, "google-creds"}, - {c.configureWithGoogleForAccountsAPI, "google-accounts"}, - {c.configureWithGoogleForWorkspace, "google-workspace"}, - {c.configureWithDatabricksCfg, "databricks-cli"}, - } - // try configuring authentication with different methods - for _, auth := range providers { - if c.AuthType != "" && auth.name != c.AuthType { - // ignore other auth types if one is explicitly enforced - log.Printf("[INFO] Ignoring %s auth, because %s is preferred", auth.name, c.AuthType) - continue - } - authorizer, err := auth.configure(ctx) - if err != nil { - return c.niceAuthError(fmt.Sprintf("cannot configure %s auth: %s", auth.name, err)) - } - if authorizer == nil { - // try the next method. - continue - } - // even though this may complain about clear text logging, passwords are replaced with `***` - log.Printf("[INFO] Configured %s auth: %s", auth.name, c.configDebugString()) // lgtm[go/clear-text-logging] - c.authVisitor = authorizer - c.AuthType = auth.name - c.fixHost() - return nil - } - if c.AuthType != "" { - return c.niceAuthError(fmt.Sprintf("cannot configure %s auth", c.AuthType)) - } - if c.Host == "" && IsData.GetOrUnknown(ctx) == "yes" { - return c.niceAuthError("workspace is most likely not created yet, because the `host` " + - "is empty. Please add `depends_on = [databricks_mws_workspaces.this]` or " + - "`depends_on = [azurerm_databricks_workspace.this]` to every data resource. See " + - "https://www.terraform.io/docs/language/resources/behavior.html more info") - } - return c.niceAuthError("authentication is not configured for provider.") -} - -func (c *DatabricksClient) niceAuthError(message string) error { - info := "" - if len(c.configAttributesUsed) > 0 { - envs := []string{} - attrs := []string{} - usedAsEnv := map[string]bool{} - for _, attr := range ClientAttributes() { - if len(attr.EnvVars) == 0 { - continue - } - for _, envVar := range attr.EnvVars { - value := os.Getenv(envVar) - if value == "" { - continue - } - usedAsEnv[attr.Name] = true - envs = append(envs, envVar) - } - } - for _, attr := range c.configAttributesUsed { - if usedAsEnv[attr] { - continue - } - attrs = append(attrs, attr) - } - infos := []string{} - if len(attrs) > 0 { - infos = append(infos, fmt.Sprintf("Attributes used: %s", strings.Join(attrs, ", "))) - } - if len(envs) > 0 { - infos = append(infos, fmt.Sprintf("Environment variables used: %s", strings.Join(envs, ", "))) - } - info = ". " + strings.Join(infos, ". ") - } - info = strings.TrimSuffix(info, ".") - message = strings.TrimSuffix(message, ".") - docUrl := "https://registry.terraform.io/providers/databricks/databricks/latest/docs#authentication" - return fmt.Errorf("%s%s. Please check %s for details", message, info, docUrl) -} +type ApiVersion string -func (c *DatabricksClient) fixHost() error { - // Nothing to fix if the host isn't set. - if c.Host == "" { - return nil - } - - u, err := url.Parse(c.Host) - if err != nil { - return err - } +const ( + API_1_2 ApiVersion = "1.2" + API_2_0 ApiVersion = "2.0" + API_2_1 ApiVersion = "2.1" +) - // If the host is empty, assume the scheme wasn't included. - if u.Host == "" { - u, err = url.Parse("https://" + c.Host) - if err != nil { - return err - } +func (c *DatabricksClient) addApiPrefix(r *http.Request) error { + if r.URL == nil { + return fmt.Errorf("no URL found in request") } - - // Create new instance to ensure other fields are initialized as empty. - u = &url.URL{ - Scheme: u.Scheme, - Host: u.Host, + ctx := r.Context() + av, ok := ctx.Value(Api).(ApiVersion) + if !ok { + av = API_2_0 } - - // Store sanitized version of c.Host. - c.Host = u.String() + r.URL.Path = fmt.Sprintf("/api/%s%s", av, r.URL.Path) return nil } -func (c *DatabricksClient) configureWithPat(ctx context.Context) (func(*http.Request) error, error) { - if !(c.Token != "" && c.Host != "") { - return nil, nil - } - log.Printf("[INFO] Using directly configured PAT authentication") - return c.authorizer("Bearer", c.Token), nil -} - -func (c *DatabricksClient) configureWithBasicAuth(ctx context.Context) (func(*http.Request) error, error) { - if !(c.Username != "" && c.Password != "" && c.Host != "") { - return nil, nil - } - b64 := c.encodeBasicAuth(c.Username, c.Password) - log.Printf("[INFO] Using directly configured basic authentication") - return c.authorizer("Basic", b64), nil -} - -func (c *DatabricksClient) configureWithDatabricksCfg(ctx context.Context) (func(r *http.Request) error, error) { - configFile := c.ConfigFile - if configFile == "" { - configFile = "~/.databrickscfg" - } - configFile, err := homedir.Expand(configFile) - if err != nil { - return nil, fmt.Errorf("cannot find homedir: %w", err) - } - _, err = os.Stat(configFile) - if os.IsNotExist(err) { - // early return for non-configured machines - log.Printf("[DEBUG] %s not found on current host", configFile) - return nil, nil - } - cfg, err := ini.Load(configFile) - if err != nil { - return nil, fmt.Errorf("cannot parse config file: %w", err) - } - if c.Profile == "" { - log.Printf("[INFO] Using DEFAULT profile from %s", configFile) - c.Profile = "DEFAULT" - } - dbcli := cfg.Section(c.Profile) - if len(dbcli.Keys()) == 0 { - // here we meet a heavy user of Databricks CLI - return nil, fmt.Errorf("%s has no %s profile configured", configFile, c.Profile) - } - c.Host = dbcli.Key("host").String() - if c.Host == "" { - return nil, fmt.Errorf("config file %s is corrupt: cannot find host in %s profile", - configFile, c.Profile) - } - token := "" - authType := "Bearer" - if dbcli.HasKey("username") && dbcli.HasKey("password") { - c.Username = dbcli.Key("username").String() - c.Password = dbcli.Key("password").String() - token = c.encodeBasicAuth(c.Username, c.Password) - authType = "Basic" - } else { - c.Token = dbcli.Key("token").String() - token = c.Token - } - if token == "" { - return nil, fmt.Errorf("config file %s is corrupt: cannot find token in %s profile", - configFile, c.Profile) - } - log.Printf("[INFO] Using %s authentication from ~/.databrickscfg", authType) - return c.authorizer(authType, token), nil -} - -func (c *DatabricksClient) authorizer(authType, token string) func(r *http.Request) error { - return func(r *http.Request) error { - r.Header.Set("Authorization", fmt.Sprintf("%s %s", authType, token)) - return nil +// scimVisitor is a separate method for the sake of unit tests +func (c *DatabricksClient) scimVisitor(r *http.Request) error { + r.Header.Set("Content-Type", "application/scim+json; charset=utf-8") + if c.Config.IsAccountClient() && c.Config.AccountID != "" { + // until `/preview` is there for workspace scim, + // `/api/2.0` is added by completeUrl visitor + r.URL.Path = strings.ReplaceAll(r.URL.Path, "/api/2.0/preview", + fmt.Sprintf("/api/2.0/accounts/%s", c.Config.AccountID)) } + return nil } -func (c *DatabricksClient) encodeBasicAuth(username, password string) string { - tokenUnB64 := fmt.Sprintf("%s:%s", username, password) - return base64.StdEncoding.EncodeToString([]byte(tokenUnB64)) -} - -func (c *DatabricksClient) configureHTTPCLient() { - if c.HTTPTimeoutSeconds == 0 { - c.HTTPTimeoutSeconds = DefaultHTTPTimeoutSeconds - } - if c.RateLimitPerSecond == 0 { - c.RateLimitPerSecond = DefaultRateLimitPerSecond - } - c.rateLimiter = rate.NewLimiter(rate.Limit(c.RateLimitPerSecond), 1) - // Set up a retryable HTTP Client to handle cases where the service returns - // a transient error on initial creation - retryDelayDuration := 10 * time.Second - retryMaximumDuration := 5 * time.Minute - defaultTransport := http.DefaultTransport.(*http.Transport) - c.httpClient = &retryablehttp.Client{ - HTTPClient: &http.Client{ - Timeout: time.Duration(c.HTTPTimeoutSeconds) * time.Second, - Transport: &http.Transport{ - Proxy: defaultTransport.Proxy, - DialContext: defaultTransport.DialContext, - MaxIdleConns: defaultTransport.MaxIdleConns, - IdleConnTimeout: defaultTransport.IdleConnTimeout * 3, - TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout * 3, - ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: c.InsecureSkipVerify, - }, - }, - }, - CheckRetry: c.checkHTTPRetry, - // Using a linear retry rather than the default exponential retry - // as the creation condition is normally passed after 30-40 seconds - // Setting the retry interval to 10 seconds. Setting RetryWaitMin and RetryWaitMax - // to the same value removes jitter (which would be useful in a high-volume traffic scenario - // but wouldn't add much here) - Backoff: retryablehttp.LinearJitterBackoff, - RetryWaitMin: retryDelayDuration, - RetryWaitMax: retryDelayDuration, - RetryMax: int(retryMaximumDuration / retryDelayDuration), - } +// Scim sets SCIM headers +func (c *DatabricksClient) Scim(ctx context.Context, method, path string, request any, response any) error { + return c.Do(ctx, method, path, request, response, c.addApiPrefix, c.scimVisitor) } // IsAzure returns true if client is configured for Azure Databricks - either by using AAD auth or with host+token combination func (c *DatabricksClient) IsAzure() bool { - return c.AzureResourceID != "" || c.AzureClientID != "" || c.AzureUseMSI || strings.Contains(c.Host, ".azuredatabricks.net") + return c.Config.IsAzure() } // IsAws returns true if client is configured for AWS func (c *DatabricksClient) IsAws() bool { - return !c.IsAzure() && !c.IsGcp() + return !c.IsGcp() && !c.IsAzure() } // IsGcp returns true if client is configured for GCP func (c *DatabricksClient) IsGcp() bool { - return c.GoogleServiceAccount != "" || strings.Contains(c.Host, ".gcp.databricks.com") + return c.Config.GoogleServiceAccount != "" || c.Config.IsGcp() } // FormatURL creates URL from the client Host and additional strings func (c *DatabricksClient) FormatURL(strs ...string) string { - host := c.Host + host := c.Config.Host if !strings.HasSuffix(host, "/") { host += "/" } @@ -519,32 +120,85 @@ func (c *DatabricksClient) FormatURL(strs ...string) string { // but for the given host. Authentication has to be reinitialized, as Google OIDC has // different authorizers, depending if it's workspace or Accounts API we're talking to. func (c *DatabricksClient) ClientForHost(ctx context.Context, url string) (*DatabricksClient, error) { - log.Printf("[INFO] Creating client for host %s based on %s", url, c.configDebugString()) // lgtm[go/clear-text-logging] + // create dummy http request + req, _ := http.NewRequestWithContext(ctx, "GET", "/", nil) // Ensure that client is authenticated - err := c.Authenticate(ctx) + err := c.DatabricksClient.Config.Authenticate(req) if err != nil { return nil, fmt.Errorf("cannot authenticate parent client: %w", err) } + cfg := &config.Config{ + Host: url, + Username: c.Config.Username, + Password: c.Config.Password, + Token: c.Config.Token, + ClientID: c.Config.ClientID, + ClientSecret: c.Config.ClientSecret, + GoogleServiceAccount: c.Config.GoogleServiceAccount, + GoogleCredentials: c.Config.GoogleCredentials, + InsecureSkipVerify: c.Config.InsecureSkipVerify, + HTTPTimeoutSeconds: c.Config.HTTPTimeoutSeconds, + DebugTruncateBytes: c.Config.DebugTruncateBytes, + DebugHeaders: c.Config.DebugHeaders, + RateLimitPerSecond: c.Config.RateLimitPerSecond, + } + client, err := client.New(cfg) + if err != nil { + return nil, fmt.Errorf("cannot configure new client: %w", err) + } // copy all client configuration options except Databricks CLI profile return &DatabricksClient{ - Host: url, - Username: c.Username, - Password: c.Password, - Token: c.Token, - ClientID: c.ClientID, - ClientSecret: c.ClientSecret, - GoogleServiceAccount: c.GoogleServiceAccount, - GoogleCredentials: c.GoogleCredentials, - AzurermEnvironment: c.AzurermEnvironment, - InsecureSkipVerify: c.InsecureSkipVerify, - HTTPTimeoutSeconds: c.HTTPTimeoutSeconds, - DebugTruncateBytes: c.DebugTruncateBytes, - DebugHeaders: c.DebugHeaders, - RateLimitPerSecond: c.RateLimitPerSecond, - Provider: c.Provider, - rateLimiter: c.rateLimiter, - httpClient: c.httpClient, - configAttributesUsed: c.configAttributesUsed, - commandFactory: c.commandFactory, + DatabricksClient: client, + commandFactory: c.commandFactory, }, nil } + +func (aa *DatabricksClient) GetAzureJwtProperty(key string) (any, error) { + if !aa.IsAzure() { + return "", fmt.Errorf("can't get Azure JWT token in non-Azure environment") + } + if key == "tid" && aa.Config.AzureTenantID != "" { + return aa.Config.AzureTenantID, nil + } + request, err := http.NewRequest("GET", aa.Config.Host, nil) + if err != nil { + return nil, err + } + err = aa.Config.Authenticate(request) + if err != nil { + return nil, err + } + header := request.Header.Get("Authorization") + var stoken string + if len(header) > 0 && strings.HasPrefix(string(header), "Bearer ") { + log.Printf("[DEBUG] Got Bearer token") + stoken = strings.TrimSpace(strings.TrimPrefix(string(header), "Bearer ")) + } + if stoken == "" { + return nil, fmt.Errorf("can't obtain Azure JWT token") + } + if strings.HasPrefix(stoken, "dapi") { + return nil, fmt.Errorf("can't use Databricks PAT") + } + parser := jwt.Parser{SkipClaimsValidation: true} + token, _, err := parser.ParseUnverified(stoken, jwt.MapClaims{}) + if err != nil { + return nil, err + } + claims := token.Claims.(jwt.MapClaims) + v, ok := claims[key] + if !ok { + return nil, fmt.Errorf("can't find field '%s' in parsed JWT", key) + } + return v, nil +} + +func CommonEnvironmentClient() *DatabricksClient { + c, err := client.New(&config.Config{}) + if err != nil { + panic(err) + } + return &DatabricksClient{ + DatabricksClient: c, + } +} diff --git a/common/client_test.go b/common/client_test.go index 1ad8929297..8897a92bc0 100644 --- a/common/client_test.go +++ b/common/client_test.go @@ -3,26 +3,29 @@ package common import ( "context" "log" - "os" - "reflect" + "net/http" + "path/filepath" "strings" "testing" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func configureAndAuthenticate(dc *DatabricksClient) (*DatabricksClient, error) { - err := dc.Configure() + req, err := http.NewRequest("GET", dc.Config.Host, nil) if err != nil { return dc, err } - return dc, dc.Authenticate(context.Background()) + return dc, dc.Config.Authenticate(req) } func failsToAuthenticateWith(t *testing.T, dc *DatabricksClient, message string) { _, err := configureAndAuthenticate(dc) - if dc.AuthType != "" { - log.Printf("[INFO] Auth is: %s", dc.AuthType) + if dc.Config.AuthType != "" { + log.Printf("[INFO] Auth is: %s", dc.Config.AuthType) } if assert.NotNil(t, err, "expected to have error: %s", message) { assert.True(t, strings.HasPrefix(err.Error(), message), err.Error()) @@ -31,165 +34,176 @@ func failsToAuthenticateWith(t *testing.T, dc *DatabricksClient, message string) func TestDatabricksClientConfigure_Nothing(t *testing.T) { defer CleanupEnvironment()() - os.Setenv("PATH", "testdata:/bin") - failsToAuthenticateWith(t, &DatabricksClient{}, - "authentication is not configured for provider") + t.Setenv("PATH", "testdata:/bin") + failsToAuthenticateWith(t, &DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{}, + }, + }, "default auth: cannot configure default credentials") } func TestDatabricksClientConfigure_BasicAuth_NoHost(t *testing.T) { defer CleanupEnvironment()() failsToAuthenticateWith(t, &DatabricksClient{ - Username: "foo", - Password: "bar", - }, "authentication is not configured for provider.") + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Username: "foo", + Password: "bar", + }, + }, + }, "default auth: cannot configure default credentials") } func TestDatabricksClientConfigure_BasicAuth(t *testing.T) { dc, err := configureAndAuthenticate(&DatabricksClient{ - Host: "https://localhost:443", - Username: "foo", - Password: "bar", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://localhost:443", + Username: "foo", + Password: "bar", + }, + }, }) assert.NoError(t, err) - assert.Equal(t, "basic", dc.AuthType) + assert.Equal(t, "basic", dc.Config.AuthType) } func TestDatabricksClientConfigure_HostWithoutScheme(t *testing.T) { dc, err := configureAndAuthenticate(&DatabricksClient{ - Host: "localhost:443", - Token: "...", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "localhost:443", + Token: "...", + }, + }, }) assert.NoError(t, err) - assert.Equal(t, "pat", dc.AuthType) - assert.Equal(t, "...", dc.Token) - assert.Equal(t, "https://localhost:443", dc.Host) + assert.Equal(t, "pat", dc.Config.AuthType) + assert.Equal(t, "...", dc.Config.Token) + assert.Equal(t, "https://localhost:443", dc.Config.Host) } func TestDatabricksClientConfigure_Token_NoHost(t *testing.T) { defer CleanupEnvironment()() failsToAuthenticateWith(t, &DatabricksClient{ - Token: "dapi345678", - }, "authentication is not configured for provider.") + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Token: "dapi345678", + }, + }, + }, "default auth: cannot configure default credentials") } func TestDatabricksClientConfigure_HostTokensTakePrecedence(t *testing.T) { dc, err := configureAndAuthenticate(&DatabricksClient{ - Host: "foo", - Token: "connfigured", - ConfigFile: "testdata/.databrickscfg", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "foo", + Token: "connfigured", + ConfigFile: "testdata/.databrickscfg", + }, + }, }) assert.NoError(t, err) - assert.Equal(t, "pat", dc.AuthType) + assert.Equal(t, "pat", dc.Config.AuthType) } -func TestDatabricksClientConfigure_BasicAuthTakePrecedence(t *testing.T) { - dc, err := configureAndAuthenticate(&DatabricksClient{ - Host: "foo", - Token: "configured", - Username: "foo", - Password: "bar", - ConfigFile: "testdata/.databrickscfg", - }) - assert.NoError(t, err) - assert.Equal(t, "pat", dc.AuthType) - assert.Equal(t, "configured", dc.Token) +func TestDatabricksClientConfigure_BasicAuthDoesNotTakePrecedence(t *testing.T) { + failsToAuthenticateWith(t, &DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "foo", + Token: "configured", + Username: "foo", + Password: "bar", + ConfigFile: "testdata/.databrickscfg", + }, + }, + }, "validate: more than one authorization method configured: basic and pat.") } func TestDatabricksClientConfigure_ConfigRead(t *testing.T) { dc, err := configureAndAuthenticate(&DatabricksClient{ - ConfigFile: "testdata/.databrickscfg", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + ConfigFile: "testdata/.databrickscfg", + }, + }, }) assert.NoError(t, err) - assert.Equal(t, "databricks-cli", dc.AuthType) - assert.Equal(t, "PT0+IC9kZXYvdXJhbmRvbSA8PT0KYFZ", dc.Token) + assert.Equal(t, "pat", dc.Config.AuthType) + assert.Equal(t, "PT0+IC9kZXYvdXJhbmRvbSA8PT0KYFZ", dc.Config.Token) } func TestDatabricksClientConfigure_NoHostGivesError(t *testing.T) { failsToAuthenticateWith(t, &DatabricksClient{ - Token: "connfigured", - ConfigFile: "testdata/.databrickscfg", - Profile: "nohost", - }, "cannot configure databricks-cli auth: config file "+ - "testdata/.databrickscfg is corrupt: cannot find host in nohost profile.") -} - -func TestDatabricksClientConfigure_NoTokenGivesError(t *testing.T) { - failsToAuthenticateWith(t, &DatabricksClient{ - Token: "connfigured", - ConfigFile: "testdata/.databrickscfg", - Profile: "notoken", - }, "cannot configure databricks-cli auth: config file "+ - "testdata/.databrickscfg is corrupt: cannot find token in notoken profile.") + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Token: "connfigured", + ConfigFile: "testdata/.databrickscfg", + Profile: "nohost", + }, + }, + }, "default auth: cannot configure default credentials. "+ + "Config: token=***, profile=nohost, config_file=testdata/.databrickscfg") } func TestDatabricksClientConfigure_InvalidProfileGivesError(t *testing.T) { failsToAuthenticateWith(t, &DatabricksClient{ - Token: "connfigured", - ConfigFile: "testdata/.databrickscfg", - Profile: "invalidhost", - }, "cannot configure databricks-cli auth: testdata/.databrickscfg "+ - "has no invalidhost profile configured") + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Token: "connfigured", + ConfigFile: "testdata/.databrickscfg", + Profile: "invalidhost", + }, + }, + }, "resolve: testdata/.databrickscfg has no invalidhost profile configured. "+ + "Config: token=***, profile=invalidhost, config_file=testdata/.databrickscfg") } func TestDatabricksClientConfigure_MissingFile(t *testing.T) { failsToAuthenticateWith(t, &DatabricksClient{ - Token: "connfigured", - ConfigFile: "testdata/.invalid file", - Profile: "invalidhost", - }, "authentication is not configured for provider.") + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Token: "connfigured", + ConfigFile: "testdata/.invalid file", + Profile: "invalidhost", + }, + }, + }, "default auth: cannot configure default credentials.") } func TestDatabricksClientConfigure_InvalidConfigFilePath(t *testing.T) { failsToAuthenticateWith(t, &DatabricksClient{ - Token: "connfigured", - ConfigFile: "testdata/az", - Profile: "invalidhost", - }, "cannot configure databricks-cli auth: cannot parse config file") + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Token: "connfigured", + ConfigFile: "testdata/az", + Profile: "invalidhost", + }, + }, + }, `resolve: cannot parse config file`) } func TestDatabricksClient_FormatURL(t *testing.T) { - client := DatabricksClient{Host: "https://some.host"} + client := DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://some.host", + }, + }, + } assert.Equal(t, "https://some.host/#job/123", client.FormatURL("#job/123")) } -func TestClientAttributes(t *testing.T) { - ca := ClientAttributes() - assert.Len(t, ca, 25) -} - -func TestDatabricksClient_Authenticate(t *testing.T) { - defer CleanupEnvironment()() - dc := DatabricksClient{} - err := dc.Configure("account_id", "username", "password") - os.Setenv("DATABRICKS_PASSWORD", ".") - assert.NoError(t, err) - err = dc.Authenticate(context.WithValue(context.Background(), IsData, "yes")) - assert.EqualError(t, err, "workspace is most likely not created yet, because the `host` is empty. "+ - "Please add `depends_on = [databricks_mws_workspaces.this]` or `depends_on = [azurerm_databricks"+ - "_workspace.this]` to every data resource. See https://www.terraform.io/docs/language/resources/behavior.html more info. "+ - "Attributes used: account_id, username. Environment variables used: DATABRICKS_PASSWORD. "+ - "Please check https://registry.terraform.io/providers/databricks/databricks/latest/docs#authentication for details") -} - -func TestDatabricksClient_AuthenticateAzure(t *testing.T) { - defer CleanupEnvironment()() - os.Setenv("ARM_CLIENT_SECRET", ".") - os.Setenv("ARM_CLIENT_ID", ".") - dc := DatabricksClient{} - err := dc.Configure("azure_client_id", "azure_client_secret") - assert.NoError(t, err) - err = dc.Authenticate(context.WithValue(context.Background(), IsData, "yes")) - assert.EqualError(t, err, "workspace is most likely not created yet, because the `host` is empty. "+ - "Please add `depends_on = [databricks_mws_workspaces.this]` or `depends_on = [azurerm_databricks"+ - "_workspace.this]` to every data resource. See https://www.terraform.io/docs/language/resources/"+ - "behavior.html more info. Environment variables used: ARM_CLIENT_SECRET, ARM_CLIENT_ID. "+ - "Please check https://registry.terraform.io/providers/databricks/databricks/latest/docs#authentication for details") -} - func TestDatabricksIsGcp(t *testing.T) { dc, err := configureAndAuthenticate(&DatabricksClient{ - Host: "https://demo.gcp.databricks.com/", - Token: "dapi123", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://demo.gcp.databricks.com/", + Token: "dapi123", + }, + }, }) assert.NoError(t, err) assert.Equal(t, true, dc.IsGcp()) @@ -197,149 +211,113 @@ func TestDatabricksIsGcp(t *testing.T) { func TestIsAzure_Error(t *testing.T) { dc := &DatabricksClient{ - Token: "connfigured", - ConfigFile: "testdata/.databrickscfg", - Profile: "notoken", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Token: "connfigured", + ConfigFile: "testdata/.databrickscfg", + Profile: "notoken", + }, + }, } assert.Equal(t, false, dc.IsAzure()) } func TestClientForHost(t *testing.T) { dc, err := configureAndAuthenticate(&DatabricksClient{ - Host: "https://accounts.cloud.databricks.com/", - Username: "abc", - Password: "bcd", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://accounts.cloud.databricks.com/", + Username: "abc", + Password: "bcd", + }, + }, }) assert.NoError(t, err) assert.True(t, dc.IsAws()) cc, err := dc.ClientForHost(context.Background(), "https://e2-workspace.cloud.databricks.com/") assert.NoError(t, err) - assert.Equal(t, dc.Username, cc.Username) - assert.Equal(t, dc.Password, cc.Password) - assert.NotEqual(t, dc.Host, cc.Host) + assert.Equal(t, dc.Config.Username, cc.Config.Username) + assert.Equal(t, dc.Config.Password, cc.Config.Password) + assert.NotEqual(t, dc.Config.Host, cc.Config.Host) } func TestClientForHostAuthError(t *testing.T) { c := &DatabricksClient{ - Token: "connfigured", - ConfigFile: "testdata/.databrickscfg", - Profile: "notoken", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Token: "connfigured", + ConfigFile: "testdata/.databrickscfg", + Profile: "notoken", + }, + }, } _, err := c.ClientForHost(context.Background(), "https://e2-workspace.cloud.databricks.com/") - if assert.NotNil(t, err) { - assert.True(t, strings.HasPrefix(err.Error(), - "cannot authenticate parent client: cannot configure databricks-cli auth"), err.Error()) - } -} - -func TestDatabricksCliCouldNotFindHomeDir(t *testing.T) { - _, err := (&DatabricksClient{ - ConfigFile: "~.databrickscfg", - }).configureWithDatabricksCfg(context.Background()) - assert.EqualError(t, err, "cannot find homedir: cannot expand user-specific home dir") -} - -func TestDatabricksCliCouldNotParseIni(t *testing.T) { - _, err := (&DatabricksClient{ - ConfigFile: "testdata/az", - }).configureWithDatabricksCfg(context.Background()) - if assert.NotNil(t, err) { - assert.True(t, strings.HasPrefix(err.Error(), - "cannot parse config file: key-value delimiter not found"), err.Error()) - } -} - -func TestDatabricksCliWrongProfile(t *testing.T) { - _, err := (&DatabricksClient{ - ConfigFile: "testdata/.databrickscfg", - Profile: "🤣", - }).configureWithDatabricksCfg(context.Background()) - assert.EqualError(t, err, "testdata/.databrickscfg has no 🤣 profile configured") -} - -func TestDatabricksNoHost(t *testing.T) { - _, err := (&DatabricksClient{ - ConfigFile: "testdata/corrupt/.databrickscfg", - Profile: "nohost", - }).configureWithDatabricksCfg(context.Background()) - assert.EqualError(t, err, "config file testdata/corrupt/.databrickscfg is corrupt: cannot find host in nohost profile") -} - -func TestDatabricksNoToken(t *testing.T) { - _, err := (&DatabricksClient{ - ConfigFile: "testdata/corrupt/.databrickscfg", - Profile: "notoken", - }).configureWithDatabricksCfg(context.Background()) - assert.EqualError(t, err, "config file testdata/corrupt/.databrickscfg is corrupt: cannot find token in notoken profile") -} - -func TestDatabricksBasicAuth(t *testing.T) { - c := &DatabricksClient{ - ConfigFile: "testdata/.databrickscfg", - Profile: "basic", - } - _, err := c.configureWithDatabricksCfg(context.Background()) assert.NoError(t, err) - assert.Equal(t, "abc", c.Username) - assert.Equal(t, "bcd", c.Password) } func TestDatabricksClientConfigure_NonsenseAuth(t *testing.T) { defer CleanupEnvironment()() failsToAuthenticateWith(t, &DatabricksClient{ - AuthType: "nonsense", - }, "cannot configure nonsense auth.") -} - -func TestConfigAttributeSetNonsense(t *testing.T) { - err := (&ConfigAttribute{ - Kind: reflect.Chan, - }).Set(&DatabricksClient{}, 1) - assert.EqualError(t, err, "cannot set of unknown type Chan") + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + AuthType: "nonsense", + }, + }, + }, "default auth: cannot configure default credentials") } -func TestDatabricksClientFixHost(t *testing.T) { - hostForInput := func(in string) (string, error) { - client := &DatabricksClient{ - Host: in, - } - if err := client.fixHost(); err != nil { - return "", err - } - return client.Host, nil - } - - { - // Strip trailing slash. - out, err := hostForInput("https://accounts.gcp.databricks.com/") - assert.Nil(t, err) - assert.Equal(t, out, "https://accounts.gcp.databricks.com") - } - - { - // Keep port. - out, err := hostForInput("https://accounts.gcp.databricks.com:443") - assert.Nil(t, err) - assert.Equal(t, out, "https://accounts.gcp.databricks.com:443") - } - - { - // Default scheme. - out, err := hostForInput("accounts.gcp.databricks.com") - assert.Nil(t, err) - assert.Equal(t, out, "https://accounts.gcp.databricks.com") +func TestGetJWTProperty_AzureCLI_SP(t *testing.T) { + defer CleanupEnvironment()() + p, _ := filepath.Abs("./testdata") + t.Setenv("PATH", p+":/bin") + + aa := DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + AzureClientID: "a", + AzureClientSecret: "b", + AzureTenantID: "c", + Host: "https://adb-1232.azuredatabricks.net", + }, + }, } + tid, err := aa.GetAzureJwtProperty("tid") + assert.NoError(t, err) + assert.Equal(t, "c", tid) +} - { - // Default scheme with port. - out, err := hostForInput("accounts.gcp.databricks.com:443") - assert.Nil(t, err) - assert.Equal(t, out, "https://accounts.gcp.databricks.com:443") +func TestGetJWTProperty_NonAzure(t *testing.T) { + defer CleanupEnvironment()() + p, _ := filepath.Abs("./testdata") + t.Setenv("PATH", p+":/bin") + + aa := DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://abc.cloud.databricks.com", + Token: "abc", + }, + }, } + _, err := aa.GetAzureJwtProperty("tid") + require.EqualError(t, err, "can't get Azure JWT token in non-Azure environment") +} - { - // Return error. - _, err := hostForInput("://@@@accounts.gcp.databricks.com/") - assert.NotNil(t, err) +func TestGetJWTProperty_Authenticate_Fail(t *testing.T) { + defer CleanupEnvironment()() + p, _ := filepath.Abs("./testdata") + t.Setenv("PATH", p+":/bin") + t.Setenv("FAIL", "yes") + + client := &DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://adb-1232.azuredatabricks.net", + }, + }, } + _, err := client.GetAzureJwtProperty("tid") + require.Error(t, err) + assert.True(t, strings.HasPrefix(err.Error(), + "default auth: azure-cli: cannot get access token: This is just a failing script")) } diff --git a/common/commands_test.go b/common/commands_test.go index 47b982cb3d..dee51d7ce3 100644 --- a/common/commands_test.go +++ b/common/commands_test.go @@ -4,17 +4,20 @@ import ( "context" "testing" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/stretchr/testify/assert" ) func TestCommandMock(t *testing.T) { c := DatabricksClient{ - Host: ".", - Token: ".", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: ".", + Token: ".", + }, + }, } - err := c.Configure() - assert.NoError(t, err) - called := false c.WithCommandMock(func(commandStr string) CommandResults { called = true diff --git a/common/context.go b/common/context.go index 2df2417348..9c00b9492e 100644 --- a/common/context.go +++ b/common/context.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "github.com/databricks/databricks-sdk-go/useragent" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -27,6 +28,12 @@ type op func(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostic // wrap operation invokations with additional context func (f op) addContext(k contextKey, v string) op { return func(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + switch k { + case ResourceName: + ctx = useragent.InContext(ctx, "resource", v) + case IsData: + ctx = useragent.InContext(ctx, "data", v) + } ctx = context.WithValue(ctx, k, v) return f(ctx, d, m) } diff --git a/common/env.go b/common/env.go index 65bcf82739..f6e3555f59 100644 --- a/common/env.go +++ b/common/env.go @@ -2,8 +2,6 @@ package common import ( "os" - "reflect" - "strconv" "strings" "sync" @@ -11,68 +9,9 @@ import ( ) var ( - envMutex sync.Mutex - onceClient sync.Once - commonClient *DatabricksClient + envMutex sync.Mutex ) -// NewClientFromEnvironment makes very good client for testing purposes -func NewClientFromEnvironment() *DatabricksClient { - client := DatabricksClient{} - for _, attr := range ClientAttributes() { - found := false - var value any - for _, envName := range attr.EnvVars { - v := os.Getenv(envName) - if v == "" { - continue - } - switch attr.Kind { - case reflect.String: - value = v - found = true - case reflect.Bool: - if vv, err := strconv.ParseBool(v); err == nil { - value = vv - found = true - } - case reflect.Int: - if vv, err := strconv.Atoi(v); err == nil { - value = vv - found = true - } - default: - continue - } - } - if found { - attr.Set(&client, value) - } - } - err := client.Configure() - if err != nil { - panic(err) - } - return &client -} - -// ResetCommonEnvironmentClient resets test dummy -func ResetCommonEnvironmentClient() { - commonClient = nil - onceClient = sync.Once{} -} - -// CommonEnvironmentClient configured once per run of application -func CommonEnvironmentClient() *DatabricksClient { - if commonClient != nil { - return commonClient - } - onceClient.Do(func() { - commonClient = NewClientFromEnvironment() - }) - return commonClient -} - // CleanupEnvironment backs up environment - use as `defer CleanupEnvironment()()` // clears it and restores it in the end. It's meant strictly for "unit" tests // as last resort, because it slows down parallel execution with mutex. diff --git a/common/env_test.go b/common/env_test.go deleted file mode 100644 index d69a52b47e..0000000000 --- a/common/env_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package common - -import ( - "context" - "log" - "os" - "sync" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCommonEnvironmentClient(t *testing.T) { - ResetCommonEnvironmentClient() - defer CleanupEnvironment()() - os.Setenv("DATABRICKS_TOKEN", ".") - os.Setenv("DATABRICKS_HOST", ".") - os.Setenv("DATABRICKS_DEBUG_HEADERS", "true") - os.Setenv("DATABRICKS_DEBUG_TRUNCATE_BYTES", "1024") - c := CommonEnvironmentClient() - c2 := CommonEnvironmentClient() - ctx := context.Background() - assert.Equal(t, c2.userAgent(ctx), c.userAgent(ctx)) - assert.Equal(t, "databricks-tf-provider/"+version+" (+unknown) terraform/unknown", c.userAgent(ctx)) - - ctx = context.WithValue(ctx, ResourceName, "cluster") - c.Provider = &schema.Provider{ - TerraformVersion: "0.12", - } - assert.Equal(t, "databricks-tf-provider/"+version+" (+cluster) terraform/0.12", c.userAgent(ctx)) - - defer func() { - paniced := recover() - log.Printf("[INFO] paniced with: %s", paniced) - require.NotNil(t, paniced, "Must have paniced!") - }() - commonClient = nil - onceClient = sync.Once{} - os.Setenv("ARM_ENVIRONMENT", "ANY") - // and this one will panic - CommonEnvironmentClient() -} diff --git a/common/gcp.go b/common/gcp.go deleted file mode 100644 index 9fc166c3fe..0000000000 --- a/common/gcp.go +++ /dev/null @@ -1,137 +0,0 @@ -package common - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "os" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - "google.golang.org/api/idtoken" - "google.golang.org/api/impersonate" - "google.golang.org/api/option" -) - -// Configures an authorizer that uses credentials sourced from JSON or file. -// This auth mode does NOT use impersonation and it does NOT use Application -// Default Credentials (ADC). -func (c *DatabricksClient) configureWithGoogleCrendentials( - ctx context.Context) (func(r *http.Request) error, error) { - if c.GoogleCredentials == "" || !c.IsGcp() || c.Host == "" { - return nil, nil - } - json, err := readCredentials(c.GoogleCredentials) - if err != nil { - err = fmt.Errorf("could not read GoogleCredentials. "+ - "Make sure the file exists, or the JSON content is valid: %w", err) - return nil, err - } - // Obtain token source for creating OIDC token. - audience := c.Host - oidcSource, err := idtoken.NewTokenSource(ctx, audience, option.WithCredentialsJSON([]byte(json))) - if err != nil { - return nil, fmt.Errorf("could not obtain OIDC token from JSON: %w", err) - } - // Obtain token source for creating Google Cloud Platform token. - creds, err := google.CredentialsFromJSON(ctx, []byte(json), - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/compute") - if err != nil { - return nil, fmt.Errorf("could not obtain OAuth2 token from JSON: %w", err) - } - return newOidcAuthorizerForAccountsAPI(oidcSource, creds.TokenSource), nil -} - -// Reads credentials as JSON. Credentials can be either a path to JSON file, -// or actual JSON string. -func readCredentials(credentials string) (string, error) { - // Try to read credentials as file path. - if _, err := os.Stat(credentials); err == nil { - jsonContents, err := ioutil.ReadFile(credentials) - if err != nil { - return string(jsonContents), err - } - return string(jsonContents), nil - } - // Assume that credential is actually JSON string. - return credentials, nil -} - -func (c *DatabricksClient) getGoogleOIDCSource(ctx context.Context) (oauth2.TokenSource, error) { - // source for generateIdToken - ts, err := impersonate.IDTokenSource(ctx, impersonate.IDTokenConfig{ - Audience: c.Host, - TargetPrincipal: c.GoogleServiceAccount, - IncludeEmail: true, - }, c.googleAuthOptions...) - if err != nil { - err = fmt.Errorf("could not obtain OIDC token. %w Running 'gcloud auth application-default login' may help", err) - return nil, err - } - // TODO: verify that refreshers work... - ts = oauth2.ReuseTokenSource(nil, ts) - return ts, nil -} - -func (c *DatabricksClient) configureWithGoogleForAccountsAPI(ctx context.Context) (func(*http.Request) error, error) { - if c.GoogleServiceAccount == "" || !c.IsGcp() || !c.isAccountsClient() { - return nil, nil - } - oidcSource, err := c.getGoogleOIDCSource(ctx) - if err != nil { - return nil, err - } - // source for generateAccessToken - platformSource, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ - TargetPrincipal: c.GoogleServiceAccount, - Scopes: []string{ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/compute", - }, - }, c.googleAuthOptions...) - if err != nil { - return nil, err - } - return newOidcAuthorizerForAccountsAPI(oidcSource, platformSource), nil -} - -func newOidcAuthorizerForAccountsAPI(oidcSource oauth2.TokenSource, - platformSource oauth2.TokenSource) func(r *http.Request) error { - return func(r *http.Request) error { - oidc, err := oidcSource.Token() - if err != nil { - return fmt.Errorf("failed to get oidc token: %w", err) - } - cloudAccess, err := platformSource.Token() - if err != nil { - return fmt.Errorf("failed to get access token: %w", err) - } - oidc.SetAuthHeader(r) - r.Header.Set("X-Databricks-GCP-SA-Access-Token", cloudAccess.AccessToken) - return nil - } -} - -func (c *DatabricksClient) configureWithGoogleForWorkspace(ctx context.Context) (func(r *http.Request) error, error) { - if c.GoogleServiceAccount == "" || !c.IsGcp() || c.isAccountsClient() { - return nil, nil - } - oidcSource, err := c.getGoogleOIDCSource(ctx) - if err != nil { - return nil, err - } - return newOidcAuthorizerWithJustBearer(oidcSource), nil -} - -func newOidcAuthorizerWithJustBearer(oidcSource oauth2.TokenSource) func(r *http.Request) error { - return func(r *http.Request) error { - oidc, err := oidcSource.Token() - if err != nil { - return err - } - oidc.SetAuthHeader(r) - return nil - } -} diff --git a/common/gcp_test.go b/common/gcp_test.go deleted file mode 100644 index d641779439..0000000000 --- a/common/gcp_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package common - -import ( - "context" - "io/ioutil" - "net/http/httptest" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/oauth2" - "google.golang.org/api/option" -) - -// Fake credentials for testing only. Private key was generated using: -// $ openssl genrsa -out name_of_private_key.pem 512 -const fakeCredentialsJson = `{ - "type": "service_account", - "project_id": "fake-project-id", - "private_key_id": "123456789", - "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAO3U0tUlSLIX06qGqalP+MUScgnmB9scOZ/fZNOykU+QLufdgqDe\nA59CfpytNg/zts8BsRgSTRiLs+6jLhjK6LkCAwEAAQJBALDuGibNROaQyTvcUI2P\n2/8oOMRaZ8++kLP56jV/a5DmwIYt5t5c35/LUWR2GA/7nvQvOJ1XZ6U+uyciOKGg\nJ7ECIQD5k+N8jIMJiobULRLAJgEWQat158sWQ3G23NakdJEWlQIhAPPzjhV+iuJ9\nu4SMOP0BLGgbjWQtna75/cOC916EmLSVAiBrBY7MTti2E7ADdhyPRvy6VYi386Cz\nuFIf7w0f0liRDQIgWD/XOndYjq6lU0HWq8/s3Ix7Da5iyJWu8zdBfXPCOjECIQC1\nusZas9Gcfu4oc3g29c0aQ5IozUTnJhAPjljxj3PmZg==\n-----END RSA PRIVATE KEY-----", - "client_email": "fake-sa2@example.com", - "client_id": "987654321", - "auth_uri": "https://accounts.example.com/o/oauth2/auth", - "token_uri": "http://127.0.0.1/token", - "auth_provider_x509_cert_url": "http://127.0.0.1/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/my-sa2%40example.com" - }` - -func TestGoogleOIDC(t *testing.T) { - defer CleanupEnvironment()() - client := &DatabricksClient{ - Host: "http://localhost", - GoogleServiceAccount: "a", - googleAuthOptions: []option.ClientOption{ - option.WithoutAuthentication(), - }, - } - client.configureHTTPCLient() - - _, err := client.getGoogleOIDCSource(context.Background()) - require.NoError(t, err) -} - -func TestConfigureWithGoogleCredentialsUsingInvalidJson(t *testing.T) { - defer CleanupEnvironment()() - client := &DatabricksClient{ - Host: "https://accounts.gcp.databricks.com/", - GoogleServiceAccount: "my-sa2@fake-project-id.iam.gserviceaccount.com", - GoogleCredentials: fakeCredentialsJson, - } - client.configureHTTPCLient() - - _, err := client.configureWithGoogleCrendentials(context.Background()) - assert.ErrorContains(t, err, - "could not obtain OIDC token from JSON: oauth2: cannot fetch token") - // TODO: To get better test coverage, start a local OAuth2 server to support - // exercising a full token fetching flow. -} - -func TestConfigureWithGoogleCredentialsUsingInvalidCredsFile(t *testing.T) { - defer CleanupEnvironment()() - - // Write fakeCredentialsJson to a temp file to pass to GoogleCredentials. - credsPath := filepath.Join(t.TempDir(), "creds.json") - err := ioutil.WriteFile(credsPath, []byte(fakeCredentialsJson), 0600) - assert.Nil(t, err, "Failed writing input JSON file.") - - client := &DatabricksClient{ - Host: "https://accounts.gcp.databricks.com/", - GoogleServiceAccount: "my-sa2@fake-project-id.iam.gserviceaccount.com", - GoogleCredentials: credsPath, - } - client.configureHTTPCLient() - - _, err = client.configureWithGoogleCrendentials(context.Background()) - assert.ErrorContains(t, err, - "could not obtain OIDC token from JSON: oauth2: cannot fetch token") -} - -func TestConfigureWithGoogleCredentialsNoCredsFile(t *testing.T) { - defer CleanupEnvironment()() - client := &DatabricksClient{ - Host: "https://accounts.gcp.databricks.com/", - GoogleServiceAccount: "my-sa2@fake-project-id.iam.gserviceaccount.com", - GoogleCredentials: "/tmp/this/path/does/not/exist/creds.json", - } - client.configureHTTPCLient() - - _, err := client.configureWithGoogleCrendentials(context.Background()) - assert.ErrorContains(t, err, - "could not obtain OIDC token from JSON: invalid character") -} - -func TestConfigureWithGoogleCredentialsEmptyHost(t *testing.T) { - defer CleanupEnvironment()() - client := &DatabricksClient{ - Host: "", - GoogleServiceAccount: "my-sa2@fake-project-id.iam.gserviceaccount.com", - GoogleCredentials: fakeCredentialsJson, - } - client.configureHTTPCLient() - - ret, err := client.configureWithGoogleCrendentials(context.Background()) - assert.Nil(t, ret) - assert.Nil(t, err) -} - -func TestConfigureWithGoogleForAccountsAPI(t *testing.T) { - defer CleanupEnvironment()() - client := &DatabricksClient{ - Host: "https://accounts.gcp.databricks.com/", - GoogleServiceAccount: "a", - } - client.configureHTTPCLient() - - _, err := client.configureWithGoogleForAccountsAPI(context.Background()) - assert.Error(t, err) - - client.googleAuthOptions = []option.ClientOption{option.WithoutAuthentication()} - a, err := client.configureWithGoogleForAccountsAPI(context.Background()) - require.NoError(t, err) - assert.NotNil(t, a) -} - -func TestConfigureWithGoogleForWorkspace(t *testing.T) { - defer CleanupEnvironment()() - client := &DatabricksClient{ - Host: "https://123.4.gcp.databricks.com/", - GoogleServiceAccount: "a", - } - client.configureHTTPCLient() - - _, err := client.configureWithGoogleForWorkspace(context.Background()) - assert.Error(t, err) - - client.googleAuthOptions = []option.ClientOption{option.WithoutAuthentication()} - a, err := client.configureWithGoogleForWorkspace(context.Background()) - require.NoError(t, err) - assert.NotNil(t, a) -} - -func TestNewOidcAuthorizerForAccountsAPI(t *testing.T) { - token := oauth2.Token{ - AccessToken: "abc", - TokenType: "Bearer", - } - auth := newOidcAuthorizerForAccountsAPI( - oauth2.StaticTokenSource(&token), - oauth2.StaticTokenSource(&token)) - request := httptest.NewRequest("GET", "http://localhost", nil) - err := auth(request) - require.NoError(t, err) - - assert.Equal(t, "Bearer abc", request.Header.Get("Authorization")) - assert.Equal(t, "abc", request.Header.Get("X-Databricks-GCP-SA-Access-Token")) -} - -func TestNewOidcAuthorizerForWorkspace(t *testing.T) { - token := oauth2.Token{ - AccessToken: "abc", - TokenType: "Bearer", - } - auth := newOidcAuthorizerWithJustBearer( - oauth2.StaticTokenSource(&token)) - request := httptest.NewRequest("GET", "http://localhost", nil) - err := auth(request) - require.NoError(t, err) - - assert.Equal(t, "Bearer abc", request.Header.Get("Authorization")) - assert.Equal(t, "", request.Header.Get("X-Databricks-GCP-SA-Access-Token")) -} diff --git a/common/http.go b/common/http.go deleted file mode 100644 index 02dc0e0fbd..0000000000 --- a/common/http.go +++ /dev/null @@ -1,596 +0,0 @@ -package common - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net/http" - "net/url" - "reflect" - "regexp" - "sort" - "strings" - - "github.com/google/go-querystring/query" - "github.com/hashicorp/go-retryablehttp" -) - -var ( - e2example = "https://registry.terraform.io/providers/databricks/databricks/latest/docs/guides/aws-workspace" - accountsHost = "accounts.cloud.databricks.com" - transientErrorStringMatches = []string{ - "com.databricks.backend.manager.util.UnknownWorkerEnvironmentException", - "does not have any associated worker environments", - "There is no worker environment with id", - "Unknown worker environment", - "ClusterNotReadyException", - "connection reset by peer", - "TLS handshake timeout", - "connection refused", - "Unexpected error", - "i/o timeout", - } -) - -// APIErrorBody maps "proper" databricks rest api errors to a 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"` - ScimType string `json:"scimType,omitempty"` - API12Error string `json:"error,omitempty"` -} - -// APIError is a generic struct for an api error on databricks -type APIError struct { - ErrorCode string - Message string - Resource string - StatusCode int -} - -// Error returns error message string instead of -func (apiError APIError) Error() string { - if apiError.StatusCode != 404 { - docs := apiError.DocumentationURL() - log.Printf("[WARN] %s:%d - %s %s", apiError.Resource, apiError.StatusCode, apiError.Message, docs) - } - return apiError.Message -} - -// IsMissing tells if error is about missing resource -func IsMissing(err error) bool { - if err == nil { - return false - } - e, ok := err.(APIError) - return ok && e.IsMissing() -} - -// IsMissing tells if it is missing resource -func (apiError APIError) IsMissing() bool { - return apiError.StatusCode == http.StatusNotFound -} - -// IsTooManyRequests shows rate exceeded limits -func (apiError APIError) IsTooManyRequests() bool { - return apiError.StatusCode == http.StatusTooManyRequests -} - -// 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]) -} - -// IsRetriable returns true if error is retriable -func (apiError APIError) IsRetriable() bool { - // Handle transient errors for retries - for _, substring := range transientErrorStringMatches { - if strings.Contains(apiError.Message, substring) { - log.Printf("[INFO] Attempting retry because of %#v", substring) - return true - } - } - // some API's recommend retries on HTTP 500, but we'll add that later - return false -} - -// NotFound returns properly formatted Not Found error -func NotFound(message string) APIError { - return APIError{ - ErrorCode: "NOT_FOUND", - StatusCode: 404, - Message: message, - } -} - -func (c *DatabricksClient) parseUnknownError( - status string, body []byte, err error) (errorBody APIErrorBody) { - // this is most likely HTML... since un-marshalling JSON failed - // Status parts first in case html message is not as expected - statusParts := strings.SplitN(status, " ", 2) - if len(statusParts) < 2 { - errorBody.ErrorCode = "UNKNOWN" - } else { - errorBody.ErrorCode = strings.ReplaceAll( - strings.ToUpper(strings.Trim(statusParts[1], " .")), - " ", "_") - } - stringBody := string(body) - messageRE := regexp.MustCompile(`
(.*)`) - messageMatches := messageRE.FindStringSubmatch(stringBody) - // No messages with
format found so return a APIError - if len(messageMatches) < 2 { - errorBody.Message = fmt.Sprintf("Response from server (%s) %s: %v", - status, stringBody, err) - return - } - errorBody.Message = strings.Trim(messageMatches[1], " .") - return -} - -func (c *DatabricksClient) isAccountsClient() bool { - return strings.HasPrefix(c.Host, "https://accounts.") -} - -func (c *DatabricksClient) commonErrorClarity(resp *http.Response) *APIError { - isAccountsAPI := strings.HasPrefix(resp.Request.URL.Path, "/api/2.0/accounts") || strings.HasPrefix(resp.Request.URL.Path, "/api/2.0/preview/accounts") - isAccountsClient := c.isAccountsClient() - isTesting := strings.HasPrefix(resp.Request.URL.Host, "127.0.0.1") - if !isTesting && isAccountsClient && !isAccountsAPI { - return &APIError{ - ErrorCode: "INCORRECT_CONFIGURATION", - Message: fmt.Sprintf("Databricks API (%s) requires you to set `host` property "+ - "(or DATABRICKS_HOST env variable) to result of `databricks_mws_workspaces.this.workspace_url`. "+ - "This error may happen if you're using provider in both normal and multiworkspace mode. Please "+ - "refactor your code into different modules. Runnable example that we use for integration testing "+ - "can be found in this repository at %s", resp.Request.URL.Path, e2example), - StatusCode: resp.StatusCode, - Resource: resp.Request.URL.Path, - } - } - // common confusion with this provider: calling workspace apis on accounts host - if !isTesting && isAccountsAPI && !isAccountsClient { - return &APIError{ - ErrorCode: "INCORRECT_CONFIGURATION", - Message: fmt.Sprintf("Accounts API (%s) requires you to set %s as DATABRICKS_HOST, but you have "+ - "specified %s instead. This error may happen if you're using provider in both "+ - "normal and multiworkspace mode. Please refactor your code into different modules. "+ - "Runnable example that we use for integration testing can be found in this "+ - "repository at %s", resp.Request.URL.Path, accountsHost, c.Host, e2example), - StatusCode: resp.StatusCode, - Resource: resp.Request.URL.Path, - } - } - return nil -} - -func (c *DatabricksClient) parseError(resp *http.Response) APIError { - body, err := io.ReadAll(resp.Body) - if err != nil { - return APIError{ - Message: err.Error(), - ErrorCode: "IO_READ", - StatusCode: resp.StatusCode, - Resource: resp.Request.URL.Path, - } - } - log.Printf("[DEBUG] %s %v", resp.Status, c.redactedDump(body)) - mwsError := c.commonErrorClarity(resp) - if mwsError != nil { - return *mwsError - } - // try to read in nicely formatted API error response - var errorBody APIErrorBody - err = json.Unmarshal(body, &errorBody) - if err != nil { - errorBody = c.parseUnknownError(resp.Status, body, err) - } - if errorBody.API12Error != "" { - // API 1.2 has different response format, let's adapt - errorBody.Message = errorBody.API12Error - } - // Handle SCIM error message details - if errorBody.Message == "" && errorBody.ScimDetail != "" { - if errorBody.ScimDetail == "null" { - errorBody.Message = "SCIM API Internal Error" - } else { - errorBody.Message = errorBody.ScimDetail - } - // add more context from SCIM responses - errorBody.Message = fmt.Sprintf("%s %s", errorBody.ScimType, errorBody.Message) - errorBody.Message = strings.Trim(errorBody.Message, " ") - errorBody.ErrorCode = fmt.Sprintf("SCIM_%s", errorBody.ScimStatus) - } - if resp.StatusCode == 403 { - errorBody.Message = fmt.Sprintf("%s. Using %s auth: %s", - strings.Trim(errorBody.Message, "."), c.AuthType, - c.configDebugString()) - } - return APIError{ - Message: errorBody.Message, - ErrorCode: errorBody.ErrorCode, - StatusCode: resp.StatusCode, - Resource: resp.Request.URL.Path, - } -} - -// checkHTTPRetry inspects HTTP errors from the Databricks API for known transient errors on Workspace creation -func (c *DatabricksClient) checkHTTPRetry(ctx context.Context, resp *http.Response, err error) (bool, error) { - if ue, ok := err.(*url.Error); ok { - apiError := APIError{ - ErrorCode: "IO_ERROR", - StatusCode: 523, - Message: ue.Error(), - } - return apiError.IsRetriable(), apiError - } - if resp == nil { - // If response is nil we can't make retry choices. - // In this case don't retry and return the original error from httpclient - return false, err - } - if resp.StatusCode == 429 { - return true, APIError{ - ErrorCode: "TOO_MANY_REQUESTS", - Message: "Current request has to be retried", - StatusCode: 429, - } - } - if resp.StatusCode >= 400 { - apiError := c.parseError(resp) - return apiError.IsRetriable(), apiError - } - return false, nil -} - -// Get on path -func (c *DatabricksClient) Get(ctx context.Context, path string, request any, response any) error { - body, err := c.authenticatedQuery(ctx, http.MethodGet, path, request, c.completeUrl) - if err != nil { - return err - } - return c.unmarshall(path, body, &response) -} - -// Post on path -func (c *DatabricksClient) Post(ctx context.Context, path string, request any, response any) error { - body, err := c.authenticatedQuery(ctx, http.MethodPost, path, request, c.completeUrl) - if err != nil { - return err - } - return c.unmarshall(path, body, &response) -} - -// Delete on path -func (c *DatabricksClient) Delete(ctx context.Context, path string, request any) error { - _, err := c.authenticatedQuery(ctx, http.MethodDelete, path, request, c.completeUrl) - return err -} - -// Patch on path -func (c *DatabricksClient) Patch(ctx context.Context, path string, request any) error { - _, err := c.authenticatedQuery(ctx, http.MethodPatch, path, request, c.completeUrl) - return err -} - -// Put on path -func (c *DatabricksClient) Put(ctx context.Context, path string, request any) error { - _, err := c.authenticatedQuery(ctx, http.MethodPut, path, request, c.completeUrl) - return err -} - -func (c *DatabricksClient) unmarshall(path string, body []byte, response any) error { - if response == nil { - return nil - } - if len(body) == 0 { - return nil - } - err := json.Unmarshal(body, &response) - if err == nil { - return nil - } - return APIError{ - ErrorCode: "UNKNOWN", - StatusCode: 200, - Resource: "..." + path, - Message: fmt.Sprintf("%s\n%s", err.Error(), onlyNBytes(string(body), 32)), - } -} - -type ApiVersion string - -const ( - API_1_2 ApiVersion = "1.2" - API_2_0 ApiVersion = "2.0" - API_2_1 ApiVersion = "2.1" -) - -func (c *DatabricksClient) completeUrl(r *http.Request) error { - if r.URL == nil { - return fmt.Errorf("no URL found in request") - } - - ctx := r.Context() - av, ok := ctx.Value(Api).(ApiVersion) - if !ok { - av = API_2_0 - } - - r.URL.Path = fmt.Sprintf("/api/%s%s", av, r.URL.Path) - r.Header.Set("Content-Type", "application/json") - - url, err := url.Parse(c.Host) - if err != nil { - return err - } - r.URL.Host = url.Host - r.URL.Scheme = url.Scheme - - return nil -} - -// scimPathVisitorFactory is a separate method for the sake of unit tests -func (c *DatabricksClient) scimVisitor(r *http.Request) error { - r.Header.Set("Content-Type", "application/scim+json; charset=utf-8") - if c.isAccountsClient() && c.AccountID != "" { - // until `/preview` is there for workspace scim, - // `/api/2.0` is added by completeUrl visitor - r.URL.Path = strings.ReplaceAll(r.URL.Path, "/api/2.0/preview", - fmt.Sprintf("/api/2.0/accounts/%s", c.AccountID)) - } - return nil -} - -// Scim sets SCIM headers -func (c *DatabricksClient) Scim(ctx context.Context, method, path string, request any, response any) error { - body, err := c.authenticatedQuery(ctx, method, path, request, c.completeUrl, c.scimVisitor) - if err != nil { - return err - } - return c.unmarshall(path, body, &response) -} - -func (c *DatabricksClient) authenticatedQuery(ctx context.Context, method, requestURL string, - data any, visitors ...func(*http.Request) error) (body []byte, err error) { - err = c.Authenticate(ctx) - if err != nil { - return - } - visitors = append([]func(*http.Request) error{c.authVisitor}, visitors...) - return c.genericQuery(ctx, method, requestURL, data, visitors...) -} - -func (c *DatabricksClient) recursiveMask(requestMap map[string]any) any { - for k, v := range requestMap { - if k == "string_value" { - requestMap[k] = "**REDACTED**" - continue - } - if k == "token_value" { - requestMap[k] = "**REDACTED**" - continue - } - if k == "secret" { - requestMap[k] = "**REDACTED**" - continue - } - if k == "content" { - requestMap[k] = "**REDACTED**" - continue - } - if m, ok := v.(map[string]any); ok { - requestMap[k] = c.recursiveMask(m) - continue - } - // todo: dapi... - // TODO: just redact any dapiXXX & "secret": "...."... - if s, ok := v.(string); ok { - requestMap[k] = onlyNBytes(s, c.DebugTruncateBytes) - } - } - return requestMap -} - -func (c *DatabricksClient) redactedDump(body []byte) (res string) { - if len(body) == 0 { - return - } - if body[0] != '{' { - return fmt.Sprintf("[non-JSON document of %d bytes]", len(body)) - } - var requestMap map[string]any - err := json.Unmarshal(body, &requestMap) - if err != nil { - // error in this case is not much relevant - return - } - rePacked, err := json.MarshalIndent(c.recursiveMask(requestMap), "", " ") - if err != nil { - // error in this case is not much relevant - return - } - maxBytes := 1024 - if c.DebugTruncateBytes > maxBytes { - maxBytes = c.DebugTruncateBytes - } - return onlyNBytes(string(rePacked), maxBytes) -} - -func (c *DatabricksClient) userAgent(ctx context.Context) string { - resource := "unknown" - terraformVersion := "unknown" - if rn, ok := ctx.Value(ResourceName).(string); ok { - resource = rn - } - if c.Provider != nil { - terraformVersion = c.Provider.TerraformVersion - } - return fmt.Sprintf("databricks-tf-provider/%s (+%s) terraform/%s", - Version(), resource, terraformVersion) -} - -// CWE-117 prevention -func escapeNewLines(in string) string { - in = strings.Replace(in, "\n", "", -1) - in = strings.Replace(in, "\r", "", -1) - return in -} - -func (c *DatabricksClient) createDebugHeaders(header http.Header, host string) string { - headers := "" - if c.DebugHeaders { - if host != "" { - headers += fmt.Sprintf("\n * Host: %s", escapeNewLines(host)) - } - for k, v := range header { - trunc := onlyNBytes(strings.Join(v, ""), c.DebugTruncateBytes) - headers += fmt.Sprintf("\n * %s: %s", k, escapeNewLines(trunc)) - } - if len(headers) > 0 { - headers += "\n" - } - } - return headers -} - -// todo: do is better name -func (c *DatabricksClient) genericQuery(ctx context.Context, method, requestURL string, data any, - visitors ...func(*http.Request) error) (body []byte, err error) { - if c.httpClient == nil { - return nil, fmt.Errorf("DatabricksClient is not configured") - } - if err = c.rateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("rate limited: %w", err) - } - requestBody, err := makeRequestBody(method, &requestURL, data) - if err != nil { - return nil, fmt.Errorf("request marshal: %w", err) - } - request, err := http.NewRequestWithContext(ctx, method, requestURL, bytes.NewBuffer(requestBody)) - if err != nil { - return nil, fmt.Errorf("new request: %w", err) - } - request.Header.Set("User-Agent", c.userAgent(ctx)) - for _, requestVisitor := range visitors { - err = requestVisitor(request) - if err != nil { - return nil, fmt.Errorf("failed visitor: %w", err) - } - } - headers := c.createDebugHeaders(request.Header, c.Host) - log.Printf("[DEBUG] %s %s %s%v", method, escapeNewLines(request.URL.Path), - headers, c.redactedDump(requestBody)) // lgtm [go/log-injection] lgtm [go/clear-text-logging] - - r, err := retryablehttp.FromRequest(request) - if err != nil { - return nil, err // no error invariants possible because of `makeRequestBody` - } - resp, err := c.httpClient.Do(r) - // retryablehttp library now returns only wrapped errors - var ae APIError - if errors.As(err, &ae) { - // don't re-wrap, as upper layers may depend on handling common.APIError - return nil, ae - } - if err != nil { - // i don't even know which errors in the real world would end up here. - // `retryablehttp` package nicely wraps _everything_ to `url.Error`. - return nil, fmt.Errorf("failed request: %w", err) - } - defer func() { - if ferr := resp.Body.Close(); ferr != nil { - err = fmt.Errorf("failed to close: %w", ferr) - } - }() - body, err = io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("response body: %w", err) - } - headers = c.createDebugHeaders(resp.Header, "") - log.Printf("[DEBUG] %s %s %s <- %s %s", resp.Status, headers, c.redactedDump(body), method, strings.ReplaceAll(request.URL.Path, "\n", "")) - return body, nil -} - -func makeQueryString(data any) (string, error) { - inputVal := reflect.ValueOf(data) - inputType := reflect.TypeOf(data) - if inputType.Kind() == reflect.Map { - s := []string{} - keys := inputVal.MapKeys() - // sort map keys by their string repr, so that tests can be deterministic - sort.Slice(keys, func(i, j int) bool { - return keys[i].String() < keys[j].String() - }) - for _, k := range keys { - v := inputVal.MapIndex(k) - if v.IsZero() { - continue - } - s = append(s, fmt.Sprintf("%s=%s", - strings.Replace(url.QueryEscape(fmt.Sprintf("%v", k.Interface())), "+", "%20", -1), - strings.Replace(url.QueryEscape(fmt.Sprintf("%v", v.Interface())), "+", "%20", -1))) - } - return "?" + strings.Join(s, "&"), nil - } - if inputType.Kind() == reflect.Struct { - params, err := query.Values(data) - if err != nil { - return "", fmt.Errorf("cannot create query string: %w", err) - } - return "?" + params.Encode(), nil - } - return "", fmt.Errorf("unsupported query string data: %#v", data) -} - -func makeRequestBody(method string, requestURL *string, data any) ([]byte, error) { - var requestBody []byte - if data == nil && (method == "DELETE" || method == "GET") { - return requestBody, nil - } - if method == "GET" { - qs, err := makeQueryString(data) - if err != nil { - return nil, err - } - *requestURL += qs - return requestBody, nil - } - if reader, ok := data.(io.Reader); ok { - raw, err := io.ReadAll(reader) - if err != nil { - return nil, fmt.Errorf("failed to read from reader: %w", err) - } - return raw, nil - } - if str, ok := data.(string); ok { - return []byte(str), nil - } - bodyBytes, err := json.MarshalIndent(data, "", " ") - if err != nil { - return nil, fmt.Errorf("request marshal failure: %w", err) - } - return bodyBytes, nil -} - -func onlyNBytes(j string, numBytes int) string { - diff := len([]byte(j)) - numBytes - if diff > 0 { - return fmt.Sprintf("%s... (%d more bytes)", j[:numBytes], diff) - } - return j -} diff --git a/common/http_test.go b/common/http_test.go deleted file mode 100644 index ccb7e8df92..0000000000 --- a/common/http_test.go +++ /dev/null @@ -1,678 +0,0 @@ -package common - -import ( - "bytes" - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/hashicorp/go-retryablehttp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAPIError(t *testing.T) { - ae := NotFound("ClusterNotReadyException: test") - ae.Resource = "c" - assert.Equal(t, "ClusterNotReadyException: test", ae.Error()) - assert.True(t, ae.IsMissing()) - assert.True(t, ae.IsRetriable()) - - ae.StatusCode = http.StatusTooManyRequests - assert.True(t, ae.IsTooManyRequests()) -} - -func TestCommonErrorFromWorkspaceClientToE2(t *testing.T) { - ws := DatabricksClient{ - Host: "https://qwerty.cloud.databricks.com/", - } - accountsAPIForWorkspaceClient := ws.commonErrorClarity(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://accounts.cloud.databricks.com/api/2.0/accounts/a/log-delivery", - nil), - }) - require.Error(t, accountsAPIForWorkspaceClient) - assert.True(t, strings.HasPrefix(accountsAPIForWorkspaceClient.Error(), - "Accounts API (/api/2.0/accounts/a/log-delivery) requires you to set accounts.cloud.databricks.com"), - "Actual message: %s", accountsAPIForWorkspaceClient.Error()) - - workspaceAPIFromWorkspaceClient := ws.commonErrorClarity(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://qwerty.cloud.databricks.com/api/2.0/clusters/list", - nil), - }) - assert.Nil(t, workspaceAPIFromWorkspaceClient) -} - -func TestCommonErrorFromWorkspaceClientToAccountSCIM(t *testing.T) { - ws := DatabricksClient{ - Host: "https://qwerty.cloud.databricks.com/", - } - accountsAPIForWorkspaceClient := ws.commonErrorClarity(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://accounts.cloud.databricks.com/api/2.0/preview/accounts/a/scim/v2/Groups", - nil), - }) - require.Error(t, accountsAPIForWorkspaceClient) - assert.True(t, strings.HasPrefix(accountsAPIForWorkspaceClient.Error(), - "Accounts API (/api/2.0/preview/accounts/a/scim/v2/Groups) requires you to set accounts.cloud.databricks.com"), - "Actual message: %s", accountsAPIForWorkspaceClient.Error()) - - workspaceAPIFromWorkspaceClient := ws.commonErrorClarity(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://qwerty.cloud.databricks.com/api/2.0/clusters/list", - nil), - }) - assert.Nil(t, workspaceAPIFromWorkspaceClient) -} - -func TestCommonErrorFromE2ClientToWorkspace(t *testing.T) { - ws := DatabricksClient{ - Host: "accounts.cloud.databricks.com", - } - ws.fixHost() - accountsAPIForWorkspaceClient := ws.commonErrorClarity(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://querty.cloud.databricks.com/api/2.0/clusters/list", - nil), - }) - require.Error(t, accountsAPIForWorkspaceClient) - assert.True(t, strings.HasPrefix(accountsAPIForWorkspaceClient.Error(), - "Databricks API (/api/2.0/clusters/list) requires you to set `host` property (or DATABRICKS_HOST env variable)"), - "Actual message: %s", accountsAPIForWorkspaceClient.Error()) - - e2APIFromE2Client := ws.commonErrorClarity(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://accounts.cloud.databricks.com/api/2.0/accounts/a/log-delivery", - nil), - }) - assert.Nil(t, e2APIFromE2Client) -} - -type errReader int - -func (errReader) Read(p []byte) (n int, err error) { - return 0, fmt.Errorf("test error") -} - -func (i errReader) Close() error { - if int(i) > 100 { - return fmt.Errorf("test error") - } - return nil -} - -func TestParseError_IO(t *testing.T) { - ws := DatabricksClient{ - Host: "qwerty.cloud.databricks.com", - } - var body errReader - err := ws.parseError(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://querty.cloud.databricks.com/api/2.0/clusters/list", - nil), - Body: body, - }) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "test error"), - "Actual message: %s", err.Error()) -} - -func TestParseError_MWS(t *testing.T) { - ws := DatabricksClient{ - Host: "qwerty.cloud.databricks.com", - } - err := ws.parseError(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://accounts.cloud.databricks.com/api/2.0/accounts/a/log-delivery", - nil), - Body: http.NoBody, - StatusCode: 400, - }) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), - "Accounts API (/api/2.0/accounts/a/log-delivery) requires you to set accounts.cloud.databricks.com"), - "Actual message: %s", err.Error()) -} - -func TestParseError_API12(t *testing.T) { - ws := DatabricksClient{ - Host: "qwerty.cloud.databricks.com", - } - err := ws.parseError(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://querty.cloud.databricks.com/api/2.0/clusters/list", - nil), - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{ - "error": "Error from API 1.2" - }`))), - }) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "Error from API 1.2"), - "Actual message: %s", err.Error()) -} - -func TestParseError_Enhance403(t *testing.T) { - ws := DatabricksClient{ - Host: "qwerty.cloud.databricks.com", - Token: "x", - } - assert.NoError(t, ws.Authenticate(context.Background())) - err := ws.parseError(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://querty.cloud.databricks.com/api/2.0/clusters/list", - nil), - StatusCode: 403, - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{ - "error_code": "PERMISSION_DENIED", - "message": "You are not authorized." - }`))), - }) - assert.EqualError(t, err, "You are not authorized. Using pat auth: "+ - "host=https://qwerty.cloud.databricks.com, token=***REDACTED***") -} - -func TestParseError_SCIM(t *testing.T) { - ws := DatabricksClient{ - Host: "qwerty.cloud.databricks.com", - } - err := ws.parseError(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://querty.cloud.databricks.com/api/2.0/clusters/list", - nil), - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{ - "detail": "Detailed SCIM message", - "status": "MALFUNCTION", - "string_value": "sensitive", - "token_value": "sensitive", - "content": "sensitive" - }`))), - }) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "Detailed SCIM message"), - "Actual message: %s", err.Error()) -} - -func TestParseError_SCIMNull(t *testing.T) { - ws := DatabricksClient{ - Host: "qwerty.cloud.databricks.com", - } - err := ws.parseError(&http.Response{ - Request: httptest.NewRequest( - "GET", "https://querty.cloud.databricks.com/api/2.0/clusters/list", - nil), - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{ - "detail": "null" - }`))), - }) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "SCIM API Internal Error"), - "Actual message: %s", err.Error()) -} - -func TestCheckHTTPRetry_Connection(t *testing.T) { - ws := DatabricksClient{ - Host: "qwerty.cloud.databricks.com", - } - retry, err := ws.checkHTTPRetry(context.Background(), nil, &url.Error{ - Err: fmt.Errorf("connection refused"), - URL: "xyz", - }) - assert.True(t, retry) - require.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "connection refused"), - "Actual message: %s", err.Error()) -} - -func TestCheckHTTPRetry_NilResp(t *testing.T) { - ws := DatabricksClient{ - Host: "qwerty.cloud.databricks.com", - } - retry, _ := ws.checkHTTPRetry(context.Background(), nil, fmt.Errorf("test error")) - assert.False(t, retry) -} - -func TestCheckHTTPRetry_429(t *testing.T) { - ws := DatabricksClient{ - Host: "qwerty.cloud.databricks.com", - } - retry, err := ws.checkHTTPRetry(context.Background(), &http.Response{ - StatusCode: 429, - }, fmt.Errorf("test error")) - assert.True(t, retry) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "Current request has to be retried"), - "Actual message: %s", err.Error()) -} - -func singleRequestServer(t *testing.T, method, url, response string) (*DatabricksClient, *httptest.Server) { - server := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - if req.Method == method && req.RequestURI == url { - _, err := rw.Write([]byte(response)) - assert.NoError(t, err) - return - } - assert.Fail(t, fmt.Sprintf("Received unexpected call: %s %s", - req.Method, req.RequestURI)) - })) - client := &DatabricksClient{ - Host: server.URL + "/", - Token: "..", - InsecureSkipVerify: true, - DebugHeaders: true, - } - err := client.Configure() - assert.NoError(t, err) - return client, server -} - -func TestGet_Error(t *testing.T) { - defer CleanupEnvironment()() - ws := DatabricksClient{} - ws.configureHTTPCLient() - err := ws.Get(context.Background(), "/imaginary/endpoint", nil, nil) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "authentication is not configured"), - "Actual message: %s", err.Error()) -} - -func TestPost_Error(t *testing.T) { - ws, server := singleRequestServer(t, "POST", "/api/2.0/imaginary/endpoint", `{corrupt: "json"`) - defer server.Close() - - var resp map[string]string - err := ws.Post(context.Background(), "/imaginary/endpoint", APIErrorBody{ - ScimDetail: "some", - }, &resp) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "invalid character 'c'"), - "Actual message: %s", err.Error()) -} - -func TestDelete(t *testing.T) { - ws, server := singleRequestServer(t, "DELETE", "/api/2.0/imaginary/endpoint", ``) - defer server.Close() - - err := ws.Delete(context.Background(), "/imaginary/endpoint", APIErrorBody{ - ScimDetail: "some", - }) - require.NoError(t, err) -} - -func TestPatch(t *testing.T) { - ws, server := singleRequestServer(t, "PATCH", "/api/2.0/imaginary/endpoint", ``) - defer server.Close() - - err := ws.Patch(context.Background(), "/imaginary/endpoint", APIErrorBody{ - ScimDetail: "some", - }) - require.NoError(t, err) -} - -func TestPut(t *testing.T) { - ws, server := singleRequestServer(t, "PUT", "/api/2.0/imaginary/endpoint", ``) - defer server.Close() - - err := ws.Put(context.Background(), "/imaginary/endpoint", APIErrorBody{ - ScimDetail: "some", - }) - require.NoError(t, err) -} - -func TestUnmarshall(t *testing.T) { - ws := DatabricksClient{} - err := ws.unmarshall("/a/b/c", nil, nil) - require.NoError(t, err) - err = ws.unmarshall("/a/b/c", nil, "abc") - require.NoError(t, err) -} - -func TestUnmarshallError(t *testing.T) { - v := struct { - Correct string `json:"c"` - Incorrect int `json:"i"` - }{} - ws := DatabricksClient{} - err := ws.unmarshall("/a/b/c", []byte(`{"c":"val", "i":"123"}`), &v) - require.Error(t, err) - require.EqualError(t, err, - "json: cannot unmarshal string into Go struct field .i of type int\n{\"c\":\"val\", \"i\":\"123\"}") -} - -func TestAPI2(t *testing.T) { - ws := DatabricksClient{Host: "ht_tp://example.com/"} - err := ws.completeUrl(&http.Request{}) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "no URL found in request"), - "Actual message: %s", err.Error()) - - err = ws.completeUrl(&http.Request{ - Header: http.Header{}, - URL: &url.URL{ - Path: "/x/y/x", - }, - }) - require.Error(t, err) - assert.True(t, strings.HasPrefix(err.Error(), - `parse "ht_tp://example.com/": first path segment in URL cannot contain colon`), - "Actual message: %s", err.Error()) -} - -func TestScim(t *testing.T) { - ws, server := singleRequestServer(t, "GET", "/api/2.0/imaginary/endpoint", `{"a": "b"}`) - defer server.Close() - - var resp map[string]string - err := ws.Scim(context.Background(), "GET", "/imaginary/endpoint", nil, &resp) - require.NoError(t, err) -} - -func TestScimFailingQuery(t *testing.T) { - err := (&DatabricksClient{ - Host: "https://localhost", - Token: "..", - }).Scim(context.Background(), "GET", "/foo", nil, nil) - assert.EqualError(t, err, "DatabricksClient is not configured") -} - -func TestScimVisitorForAccounts(t *testing.T) { - request := &http.Request{ - Header: http.Header{}, - URL: &url.URL{ - Path: "/api/2.0/preview/scim/v2/Users/abc", - }, - } - err := (&DatabricksClient{ - Host: "https://accounts.everywhere", - AccountID: "uuid", - }).scimVisitor(request) - assert.NoError(t, err) - assert.Equal(t, "application/scim+json; charset=utf-8", request.Header.Get("Content-Type")) - assert.Equal(t, "/api/2.0/accounts/uuid/scim/v2/Users/abc", request.URL.Path) -} - -func TestMakeRequestBody(t *testing.T) { - type x struct { - Scope string `json:"scope" url:"scope"` - } - requestURL := "/a/b/c" - _, err := makeRequestBody("GET", &requestURL, x{"test"}) - require.NoError(t, err) - assert.Equal(t, "/a/b/c?scope=test", requestURL) - - body, _ := makeRequestBody("POST", &requestURL, "abc") - assert.Equal(t, []byte("abc"), body) -} - -func TestMakeRequestBodyFromReader(t *testing.T) { - requestURL := "/a/b/c" - body, err := makeRequestBody("PUT", &requestURL, strings.NewReader("abc")) - require.NoError(t, err) - assert.Equal(t, []byte("abc"), body) -} - -func TestMakeRequestBodyReaderError(t *testing.T) { - requestURL := "/a/b/c" - _, err := makeRequestBody("POST", &requestURL, errReader(1)) - assert.EqualError(t, err, "failed to read from reader: test error") -} - -func TestMakeRequestBodyJsonError(t *testing.T) { - requestURL := "/a/b/c" - type x struct { - Foo chan string `json:"foo"` - } - _, err := makeRequestBody("POST", &requestURL, x{make(chan string)}) - assert.EqualError(t, err, "request marshal failure: json: unsupported type: chan string") -} - -type failingUrlEncode string - -func (fue failingUrlEncode) EncodeValues(key string, v *url.Values) error { - return fmt.Errorf(string(fue)) -} - -func TestMakeRequestBodyQueryFailingEncode(t *testing.T) { - requestURL := "/a/b/c" - type x struct { - Foo failingUrlEncode `url:"foo"` - } - _, err := makeRequestBody("GET", &requestURL, x{failingUrlEncode("always failing")}) - assert.EqualError(t, err, "cannot create query string: always failing") -} - -func TestMakeRequestBodyQueryUnsupported(t *testing.T) { - requestURL := "/a/b/c" - _, err := makeRequestBody("GET", &requestURL, true) - assert.EqualError(t, err, "unsupported query string data: true") -} - -func TestReaderBodyIsNotDumped(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - raw, err := ioutil.ReadAll(req.Body) - assert.NoError(t, err) - assert.Equal(t, "abc", string(raw)) - rw.WriteHeader(200) - })) - defer server.Close() - client := &DatabricksClient{ - Host: server.URL + "/", - Token: "..", - InsecureSkipVerify: true, - DebugHeaders: true, - } - err := client.Configure() - assert.NoError(t, err) - ctx := context.Background() - err = client.Post(ctx, "/workspace/import-file", strings.NewReader("abc"), nil) - assert.NoError(t, err) -} - -func TestRedactedDumpMalformedJsonReturnsEmptyString(t *testing.T) { - client := &DatabricksClient{} - res := client.redactedDump([]byte("{..}")) - assert.Equal(t, "", res) -} - -func TestRedactedDumpOverridesMaxBytes(t *testing.T) { - client := &DatabricksClient{ - DebugTruncateBytes: 1300, - } - res := client.redactedDump([]byte(`{"foo":"` + strings.Repeat("x", 1500) + `"}`)) - assert.Len(t, res, 1319) - assert.True(t, strings.HasSuffix(res, "... (35 more bytes)")) -} - -func TestMakeRequestBodyForMap(t *testing.T) { - requestURL := "/a" - _, err := makeRequestBody("GET", &requestURL, map[string]int{ - // i hope this will not trigger false positives too often - "e": 1, - "a": 2, - "f": 3, - "g": 4, - "c": 5, - "b": 6, - "d": 7, - }) - require.NoError(t, err) - assert.Equal(t, "/a?a=2&b=6&c=5&d=7&e=1&f=3&g=4", requestURL) -} - -func TestClient_HandleErrors(t *testing.T) { - tests := []struct { - name string - response string - responseStatus int - expectedErrorCode string - expectedMessage string - expectedResource string - expectedStatusCode int - apiCall func(client *DatabricksClient) error - }{ - { - name: "Status 404", - response: `{ - "error_code": "RESOURCE_DOES_NOT_EXIST", - "message": "Token ... does not exist!" - }`, - responseStatus: http.StatusNotFound, - expectedErrorCode: "RESOURCE_DOES_NOT_EXIST", - expectedMessage: "Token ... does not exist!", - expectedResource: "/api/2.0/token/create", - expectedStatusCode: 404, - apiCall: func(client *DatabricksClient) error { - return client.Post(context.Background(), "/token/create", map[string]string{ - "foo": "bar", - }, nil) - }, - }, - { - name: "HTML Status 404", - response: `
Hello world`, - responseStatus: http.StatusNotFound, - expectedErrorCode: "NOT_FOUND", - expectedMessage: "Hello world", - expectedResource: "/api/2.0/token/create", - expectedStatusCode: 404, - apiCall: func(client *DatabricksClient) error { - return client.Post(context.Background(), "/token/create", map[string]string{ - "foo": "bar", - }, nil) - }, - }, - { - name: "Invalid HTML Status 404", - response: ` Hello world `, - responseStatus: http.StatusNotFound, - expectedErrorCode: "NOT_FOUND", - expectedMessage: "Response from server (404 Not Found) Hello world : invalid character '<' looking for beginning of value", - expectedResource: "/api/2.0/token/create", - expectedStatusCode: 404, - apiCall: func(client *DatabricksClient) error { - return client.Post(context.Background(), "/token/create", map[string]string{ - "foo": "bar", - }, nil) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(tt.responseStatus) - _, err := rw.Write([]byte(tt.response)) - assert.NoError(t, err) - })) - // Close the server when test finishes - defer server.Close() - client := DatabricksClient{ - Host: server.URL, - Token: "...", - } - err := client.Configure() - assert.NoError(t, err) - - err = tt.apiCall(&client) - t.Log(err) - assert.IsType(t, APIError{}, err) - assert.Equal(t, tt.expectedErrorCode, err.(APIError).ErrorCode, "error code is not the same") - assert.Equal(t, tt.expectedMessage, err.(APIError).Message, "message is not the same") - assert.Equal(t, tt.expectedResource, err.(APIError).Resource, "resource is not the same") - assert.Equal(t, tt.expectedStatusCode, err.(APIError).StatusCode, "status code is not the same") - }) - } -} - -func TestGenericQueryNotConfigured(t *testing.T) { - _, err := (&DatabricksClient{}).genericQuery(context.Background(), "GET", "/foo", true) - assert.EqualError(t, err, "DatabricksClient is not configured") -} - -func TestGenericQueryStoppedContext(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - client := &DatabricksClient{Host: "https://localhost", Token: ".."} - err := client.Configure() - assert.NoError(t, err) - _, err = client.genericQuery(ctx, "GET", "/foo", true) - assert.EqualError(t, err, "rate limited: context canceled") -} - -func TestGenericQueryMarshalError(t *testing.T) { - ctx := context.Background() - client := &DatabricksClient{Host: "https://localhost", Token: ".."} - err := client.Configure() - assert.NoError(t, err) - _, err = client.genericQuery(ctx, "POST", "/foo", errReader(1)) - assert.EqualError(t, err, "request marshal: failed to read from reader: test error") -} - -func TestGenericQueryInvalidMethod(t *testing.T) { - ctx := context.Background() - client := &DatabricksClient{Host: "https://localhost", Token: ".."} - err := client.Configure() - assert.NoError(t, err) - _, err = client.genericQuery(ctx, "😃", "/foo", strings.NewReader("abc")) - assert.EqualError(t, err, `new request: net/http: invalid method "😃"`) -} - -func TestGenericQueryFailingVisitor(t *testing.T) { - ctx := context.Background() - client := &DatabricksClient{Host: "https://localhost", Token: ".."} - err := client.Configure() - assert.NoError(t, err) - _, err = client.genericQuery(ctx, "POST", "/foo", strings.NewReader("abc"), - func(r *http.Request) error { - return fmt.Errorf("😃") - }) - assert.EqualError(t, err, `failed visitor: 😃`) -} - -func TestGenericQueryFailingRequest(t *testing.T) { - ctx := context.Background() - client := &DatabricksClient{Host: "https://localhost", Token: ".."} - err := client.Configure() - assert.NoError(t, err) - client.httpClient.RetryMax = 0 - client.httpClient.ErrorHandler = func(_ *http.Response, _ error, _ int) (*http.Response, error) { - return nil, fmt.Errorf("😃") - } - _, err = client.genericQuery(ctx, "PUT", "https://127.0.0.1/foo", strings.NewReader("abc")) - assert.EqualError(t, err, `failed request: 😃`) -} - -func TestGenericQueryFailingResponseBodyRead(t *testing.T) { - client, server := singleRequestServer(t, "GET", "/api/2.0/imaginary/endpoint", `{"a": "b"}`) - defer server.Close() - client.httpClient.RetryMax = 0 - client.httpClient.ResponseLogHook = func(_ retryablehttp.Logger, r *http.Response) { - r.Body = errReader(1) - } - ctx := context.Background() - _, err := client.genericQuery(ctx, "GET", fmt.Sprintf("%s/api/2.0/imaginary/endpoint", server.URL), nil) - assert.EqualError(t, err, "response body: test error") -} - -func TestGenericQueryFailingResponseBodyClose(t *testing.T) { - client, server := singleRequestServer(t, "GET", "/api/2.0/imaginary/endpoint", `{"a": "b"}`) - defer server.Close() - client.httpClient.RetryMax = 0 - client.httpClient.ResponseLogHook = func(_ retryablehttp.Logger, r *http.Response) { - r.Body = errReader(1000) - } - ctx := context.Background() - _, err := client.genericQuery(ctx, "GET", fmt.Sprintf("%s/api/2.0/imaginary/endpoint", server.URL), nil) - assert.EqualError(t, err, "failed to close: test error") -} - -func TestParseUnknownErrorStatusMalformed(t *testing.T) { - eb := (&DatabricksClient{}).parseUnknownError("malformed", nil, fmt.Errorf("test")) - assert.Equal(t, "UNKNOWN", eb.ErrorCode) -} diff --git a/common/m2m.go b/common/m2m.go deleted file mode 100644 index 7665413784..0000000000 --- a/common/m2m.go +++ /dev/null @@ -1,74 +0,0 @@ -package common - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net/http" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" -) - -type oauthAuthorizationServer struct { - AuthorizationEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` -} - -var errNotAvailable = errors.New("not available") - -func (c *DatabricksClient) getOAuthEndpoints() (*oauthAuthorizationServer, error) { - err := c.fixHost() - if err != nil { - return nil, fmt.Errorf("host: %w", err) - } - oidc := fmt.Sprintf("%s/oidc/.well-known/oauth-authorization-server", c.Host) - oidcResponse, err := http.Get(oidc) - if err != nil { - return nil, errNotAvailable - } - if oidcResponse.Body == nil { - return nil, fmt.Errorf("fetch .well-known: empty body") - } - defer oidcResponse.Body.Close() - raw, err := io.ReadAll(oidcResponse.Body) - if err != nil { - return nil, fmt.Errorf("read .well-known: %w", err) - } - var oauthEndpoints oauthAuthorizationServer - err = json.Unmarshal(raw, &oauthEndpoints) - if err != nil { - return nil, fmt.Errorf("parse .well-known: %w", err) - } - return &oauthEndpoints, nil -} - -func (c *DatabricksClient) configureWithOAuthM2M( - ctx context.Context) (func(r *http.Request) error, error) { - if !c.IsAws() || c.ClientID == "" || c.ClientSecret == "" || c.Host == "" { - return nil, nil - } - // workaround for accounts endpoint not having yet a well-known OIDC alias - if c.TokenEndpoint == "" { - endpoints, err := c.getOAuthEndpoints() - if err == errNotAvailable { - return nil, nil - } - if err != nil { - return nil, fmt.Errorf("databricks oauth: %w", err) - } - c.TokenEndpoint = endpoints.TokenEndpoint - } - log.Printf("[INFO] Generating Databricks OAuth token for Service Principal (%s)", c.ClientID) - ts := (&clientcredentials.Config{ - ClientID: c.ClientID, - ClientSecret: c.ClientSecret, - AuthStyle: oauth2.AuthStyleInHeader, - TokenURL: c.TokenEndpoint, - Scopes: []string{"all-apis"}, - }).TokenSource(ctx) - return newOidcAuthorizerWithJustBearer(ts), nil -} diff --git a/common/m2m_test.go b/common/m2m_test.go deleted file mode 100644 index efdc79c089..0000000000 --- a/common/m2m_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package common - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestConfigureWithOAuthM2M(t *testing.T) { - defer CleanupEnvironment()() - cnt := []int{0} - server := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - if req.RequestURI == - "/oidc/.well-known/oauth-authorization-server" { - _, err := rw.Write([]byte( - `{"token_endpoint": "http://localhost/oauth/token"}`)) - assert.NoError(t, err) - cnt[0]++ - return - } - assert.Fail(t, fmt.Sprintf("Received unexpected call: %s %s", - req.Method, req.RequestURI)) - })) - defer server.Close() - - c := &DatabricksClient{ - Host: server.URL, - ClientID: "abc", - ClientSecret: "bcd", - } - _, err := c.configureWithOAuthM2M(context.Background()) - assert.NoError(t, err) - assert.Equal(t, 1, cnt[0]) -} - -func TestConfigureWithOAuthOIDCUnavailableSkips(t *testing.T) { - defer CleanupEnvironment()() - c := &DatabricksClient{ - Host: "http://localhost:22", - ClientID: "abc", - ClientSecret: "bcd", - } - _, err := c.configureWithOAuthM2M(context.Background()) - assert.NoError(t, err) -} diff --git a/common/pair_test.go b/common/pair_test.go index 209c047c4d..4efa0bcf9e 100644 --- a/common/pair_test.go +++ b/common/pair_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -80,7 +81,7 @@ func TestPairIDResource(t *testing.T) { { read: true, id: "a|b", - err: NotFound("Nope"), + err: apierr.NotFound("Nope"), left: "a", right: "b", assertID: "", diff --git a/common/resource.go b/common/resource.go index 92e70f12b1..45ea3cd961 100644 --- a/common/resource.go +++ b/common/resource.go @@ -8,6 +8,7 @@ import ( "regexp" "strings" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -97,7 +98,8 @@ func (r Resource) ToResource() *schema.Resource { return func(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { err := recoverable(r.Read)(ctx, d, m.(*DatabricksClient)) - if ignoreMissing && IsMissing(err) { + // TODO: https://github.com/databricks/terraform-provider-databricks/issues/2021 + if ignoreMissing && apierr.IsMissing(err) { log.Printf("[INFO] %s[id=%s] is removed on backend", ResourceName.GetOrUnknown(ctx), d.Id()) d.SetId("") @@ -134,7 +136,8 @@ func (r Resource) ToResource() *schema.Resource { DeleteContext: func(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { err := recoverable(r.Delete)(ctx, d, m.(*DatabricksClient)) - if IsMissing(err) { + if apierr.IsMissing(err) { + // TODO: https://github.com/databricks/terraform-provider-databricks/issues/2021 log.Printf("[INFO] %s[id=%s] is removed on backend", ResourceName.GetOrUnknown(ctx), d.Id()) d.SetId("") diff --git a/common/resource_test.go b/common/resource_test.go index 86f3e2cef3..7d70a8d7cf 100644 --- a/common/resource_test.go +++ b/common/resource_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -41,7 +42,7 @@ func TestHTTP404TriggersResourceRemovalForReadAndDelete(t *testing.T) { nope := func(ctx context.Context, d *schema.ResourceData, c *DatabricksClient) error { - return NotFound("nope") + return apierr.NotFound("nope") } r := Resource{ Create: nope, @@ -96,12 +97,12 @@ func TestUpdate(t *testing.T) { Read: func(ctx context.Context, d *schema.ResourceData, c *DatabricksClient) error { - return NotFound("nope") + return apierr.NotFound("nope") }, Delete: func(ctx context.Context, d *schema.ResourceData, c *DatabricksClient) error { - return NotFound("nope") + return apierr.NotFound("nope") }, Schema: map[string]*schema.Schema{ "foo": { diff --git a/common/testdata/az b/common/testdata/az index 7282d6ceb9..68d7f8c78f 100755 --- a/common/testdata/az +++ b/common/testdata/az @@ -5,6 +5,11 @@ if [ "yes" == "$FAIL" ]; then exit 1 fi +if [ "logout" == "$FAIL" ]; then + >&2 /bin/echo "No subscription found. Run 'az account set' to select a subscription." + exit 1 +fi + if [ "corrupt" == "$FAIL" ]; then /bin/echo "{accessToken: ..corrupt" exit @@ -16,7 +21,7 @@ if [ -z "${EXP}" ]; then # Linux EXPIRE=$(/bin/echo $EXPIRE | /bin/sed 's/S/seconds/') EXPIRE=$(/bin/echo $EXPIRE | /bin/sed 's/M/minutes/') - EXP=$(/bin/date --date=+${EXPIRE:=10seconds} +'%FT%TZ') + EXP=$(/bin/date --date=+${EXPIRE:=10seconds} +'%F %T') fi if [ -z "${TF_AAD_TOKEN}" ]; then diff --git a/docs/data-sources/catalogs.md b/docs/data-sources/catalogs.md index ca7cbb0580..1221a2565c 100644 --- a/docs/data-sources/catalogs.md +++ b/docs/data-sources/catalogs.md @@ -3,7 +3,7 @@ subcategory: "Unity Catalog" --- # databricks_catalogs Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves a list of [databricks_catalog](../resources/catalog.md) ids, that were created by Terraform or manually, so that special handling could be applied. diff --git a/docs/data-sources/cluster.md b/docs/data-sources/cluster.md index 30d500b8f3..aa5fd4d8e0 100644 --- a/docs/data-sources/cluster.md +++ b/docs/data-sources/cluster.md @@ -3,7 +3,7 @@ subcategory: "Compute" --- # databricks_cluster Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves information about a [databricks_cluster](../resources/cluster.md) using its id. This could be retrieved programmatically using [databricks_clusters](../data-sources/clusters.md) data source. diff --git a/docs/data-sources/cluster_policy.md b/docs/data-sources/cluster_policy.md index bdba93b13c..e7561b19e2 100644 --- a/docs/data-sources/cluster_policy.md +++ b/docs/data-sources/cluster_policy.md @@ -4,7 +4,7 @@ subcategory: "Compute" # databricks_cluster_policy Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves information about [databricks_cluster_policy](../resources/cluster_policy.md). diff --git a/docs/data-sources/clusters.md b/docs/data-sources/clusters.md index 373144e842..81ab775ecb 100644 --- a/docs/data-sources/clusters.md +++ b/docs/data-sources/clusters.md @@ -3,7 +3,7 @@ subcategory: "Compute" --- # databricks_clusters Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves a list of [databricks_cluster](../resources/cluster.md#cluster_id) ids, that were created by Terraform or manually, with or without [databricks_cluster_policy](../resources/cluster_policy.md). diff --git a/docs/data-sources/current_user.md b/docs/data-sources/current_user.md index 3acbaceb37..24a2f50e3b 100644 --- a/docs/data-sources/current_user.md +++ b/docs/data-sources/current_user.md @@ -3,7 +3,7 @@ subcategory: "Security" --- # databricks_current_user Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves information about [databricks_user](../resources/user.md) or [databricks_service_principal](../resources/service_principal.md), that is calling Databricks REST API. Might be useful in applying the same Terraform by different users in the shared workspace for testing purposes. diff --git a/docs/data-sources/dbfs_file.md b/docs/data-sources/dbfs_file.md index 2546a5ab05..6f950f955a 100644 --- a/docs/data-sources/dbfs_file.md +++ b/docs/data-sources/dbfs_file.md @@ -3,7 +3,7 @@ subcategory: "Storage" --- # databricks_dbfs_file Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. This data source allows to get file content from [Databricks File System (DBFS)](https://docs.databricks.com/data/databricks-file-system.html). diff --git a/docs/data-sources/dbfs_file_paths.md b/docs/data-sources/dbfs_file_paths.md index 24cb4f51d7..c43340d9c8 100644 --- a/docs/data-sources/dbfs_file_paths.md +++ b/docs/data-sources/dbfs_file_paths.md @@ -3,7 +3,7 @@ subcategory: "Storage" --- # databricks_dbfs_file_paths Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. This data source allows to get list of file names from get file content from [Databricks File System (DBFS)](https://docs.databricks.com/data/databricks-file-system.html). diff --git a/docs/data-sources/directory.md b/docs/data-sources/directory.md index 718481e64d..21340ce774 100644 --- a/docs/data-sources/directory.md +++ b/docs/data-sources/directory.md @@ -3,7 +3,7 @@ subcategory: "Workspace" --- # databricks_directory Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. This data source allows to get information about a directory in a Databricks Workspace. diff --git a/docs/data-sources/group.md b/docs/data-sources/group.md index 0fb55ffe34..dfa1c7efb6 100644 --- a/docs/data-sources/group.md +++ b/docs/data-sources/group.md @@ -3,7 +3,7 @@ subcategory: "Security" --- # databricks_group Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves information about [databricks_group](../resources/group.md) members, entitlements and instance profiles. diff --git a/docs/data-sources/instance_pool.md b/docs/data-sources/instance_pool.md index 1a1cd677a3..7cb92a183a 100644 --- a/docs/data-sources/instance_pool.md +++ b/docs/data-sources/instance_pool.md @@ -4,7 +4,7 @@ subcategory: "Compute" # databricks_instance_pool Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves information about [databricks_instance_pool](../resources/instance_pool.md). diff --git a/docs/data-sources/job.md b/docs/data-sources/job.md index 58282a5fb4..3565a77667 100755 --- a/docs/data-sources/job.md +++ b/docs/data-sources/job.md @@ -3,7 +3,7 @@ subcategory: "Compute" --- # databricks_job Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves the settings of [databricks_job](../resources/job.md) by name or by id. Complements the feature of the [databricks_jobs](jobs.md) data source. diff --git a/docs/data-sources/jobs.md b/docs/data-sources/jobs.md index ed2f4709c0..e82556088b 100644 --- a/docs/data-sources/jobs.md +++ b/docs/data-sources/jobs.md @@ -3,7 +3,7 @@ subcategory: "Compute" --- # databricks_jobs Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves a list of [databricks_job](../resources/job.md) ids, that were created by Terraform or manually, so that special handling could be applied. diff --git a/docs/data-sources/mws_credentials.md b/docs/data-sources/mws_credentials.md index 30659957ee..1e80fc0014 100755 --- a/docs/data-sources/mws_credentials.md +++ b/docs/data-sources/mws_credentials.md @@ -3,7 +3,7 @@ subcategory: "AWS" --- # databricks_mws_credentials Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Lists all [databricks_mws_credentials](../resources/mws_credentials.md) in Databricks Account. diff --git a/docs/data-sources/mws_workspaces.md b/docs/data-sources/mws_workspaces.md index 5358eea7a2..de237dae63 100755 --- a/docs/data-sources/mws_workspaces.md +++ b/docs/data-sources/mws_workspaces.md @@ -3,7 +3,7 @@ subcategory: "Deployment" --- # databricks_mws_workspaces Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Lists all [databricks_mws_workspaces](../resources/mws_workspaces.md) in Databricks Account. diff --git a/docs/data-sources/node_type.md b/docs/data-sources/node_type.md index 91f045dd85..bcb1a36f4b 100644 --- a/docs/data-sources/node_type.md +++ b/docs/data-sources/node_type.md @@ -3,7 +3,7 @@ subcategory: "Compute" --- # databricks_node_type Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Gets the smallest node type for [databricks_cluster](../resources/cluster.md) that fits search criteria, like amount of RAM or number of cores. [AWS](https://databricks.com/product/aws-pricing/instance-types) or [Azure](https://azure.microsoft.com/en-us/pricing/details/databricks/). Internally data source fetches [node types](https://docs.databricks.com/dev-tools/api/latest/clusters.html#list-node-types) available per cloud, similar to executing `databricks clusters list-node-types`, and filters it to return the smallest possible node with criteria. diff --git a/docs/data-sources/notebook.md b/docs/data-sources/notebook.md index 045ef11a72..7af6ce4a25 100644 --- a/docs/data-sources/notebook.md +++ b/docs/data-sources/notebook.md @@ -3,7 +3,7 @@ subcategory: "Workspace" --- # databricks_notebook Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. This data source allows to export a notebook from Databricks Workspace. diff --git a/docs/data-sources/notebook_paths.md b/docs/data-sources/notebook_paths.md index c0d6b55156..382434770d 100644 --- a/docs/data-sources/notebook_paths.md +++ b/docs/data-sources/notebook_paths.md @@ -3,7 +3,7 @@ subcategory: "Workspace" --- # databricks_notebook_paths Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. This data source allows to list notebooks in the Databricks Workspace. diff --git a/docs/data-sources/schemas.md b/docs/data-sources/schemas.md index 07d9ab30a5..7e62a0c944 100644 --- a/docs/data-sources/schemas.md +++ b/docs/data-sources/schemas.md @@ -3,7 +3,7 @@ subcategory: "Unity Catalog" --- # databricks_schemas Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves a list of [databricks_schema](../resources/schema.md) ids, that were created by Terraform or manually, so that special handling could be applied. diff --git a/docs/data-sources/service_principal.md b/docs/data-sources/service_principal.md index 3b080f1e1c..b7b0e1fec4 100644 --- a/docs/data-sources/service_principal.md +++ b/docs/data-sources/service_principal.md @@ -4,7 +4,7 @@ subcategory: "Security" # databricks_service_principal Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves information about [databricks_service_principal](../resources/service_principal.md). diff --git a/docs/data-sources/service_principals.md b/docs/data-sources/service_principals.md index 4921be9690..a5fbe0e40c 100644 --- a/docs/data-sources/service_principals.md +++ b/docs/data-sources/service_principals.md @@ -4,7 +4,7 @@ subcategory: "Security" # databricks_service_principals Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves `application_ids` of all [databricks_service_principal](../resources/service_principal.md) based on their `display_name` diff --git a/docs/data-sources/spark_version.md b/docs/data-sources/spark_version.md index c5dfd84bbd..70fcef78f6 100644 --- a/docs/data-sources/spark_version.md +++ b/docs/data-sources/spark_version.md @@ -3,7 +3,7 @@ subcategory: "Compute" --- # databricks_spark_version Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Gets [Databricks Runtime (DBR)](https://docs.databricks.com/runtime/dbr.html) version that could be used for `spark_version` parameter in [databricks_cluster](../resources/cluster.md) and other resources that fits search criteria, like specific Spark or Scala version, ML or Genomics runtime, etc., similar to executing `databricks clusters spark-versions`, and filters it to return the latest version that matches criteria. Often used along [databricks_node_type](node_type.md) data source. diff --git a/docs/data-sources/sql_warehouse.md b/docs/data-sources/sql_warehouse.md index ac1d69e727..c65c5a7934 100644 --- a/docs/data-sources/sql_warehouse.md +++ b/docs/data-sources/sql_warehouse.md @@ -3,7 +3,7 @@ subcategory: "Databricks SQL" --- # databricks_sql_warehouse Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves information about a [databricks_sql_warehouse](../resources/sql_warehouse.md) using its id. This could be retrieved programmatically using [databricks_sql_warehouses](../data-sources/sql_warehouses.md) data source. diff --git a/docs/data-sources/sql_warehouses.md b/docs/data-sources/sql_warehouses.md index e8175c0019..cb6e578624 100644 --- a/docs/data-sources/sql_warehouses.md +++ b/docs/data-sources/sql_warehouses.md @@ -3,7 +3,7 @@ subcategory: "Databricks SQL" --- # databricks_sql_warehouses Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves a list of [databricks_sql_endpoint](../resources/sql_endpoint.md#id) ids, that were created by Terraform or manually. diff --git a/docs/data-sources/tables.md b/docs/data-sources/tables.md index 2117d8df6d..60b70f2249 100644 --- a/docs/data-sources/tables.md +++ b/docs/data-sources/tables.md @@ -3,7 +3,7 @@ subcategory: "Unity Catalog" --- # databricks_tables Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves a list of managed or external table full names in Unity Catalog, that were created by Terraform or manually. Use [databricks_views](views.md) for retrieving a list of views. diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index 92b4a58fe5..764c7b55bc 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -4,7 +4,7 @@ subcategory: "Security" # databricks_user Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves information about [databricks_user](../resources/user.md). diff --git a/docs/data-sources/views.md b/docs/data-sources/views.md index 42d1088d53..bc1cb696b1 100644 --- a/docs/data-sources/views.md +++ b/docs/data-sources/views.md @@ -3,7 +3,7 @@ subcategory: "Unity Catalog" --- # databricks_views Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. Retrieves a list of view full names in Unity Catalog, that were created by Terraform or manually. Use [databricks_tables](tables.md) for retrieving a list of tables. diff --git a/docs/data-sources/zones.md b/docs/data-sources/zones.md index 2013a06336..a4eebb29e9 100644 --- a/docs/data-sources/zones.md +++ b/docs/data-sources/zones.md @@ -3,7 +3,7 @@ subcategory: "Deployment" --- # databricks_zones Data Source --> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _default auth: cannot configure default credentials_ errors. This data source allows you to fetch all available AWS availability zones on your workspace on AWS. diff --git a/docs/guides/aws-workspace.md b/docs/guides/aws-workspace.md index 1fe75e153c..a1ae58bfe4 100644 --- a/docs/guides/aws-workspace.md +++ b/docs/guides/aws-workspace.md @@ -285,7 +285,7 @@ output "databricks_token" { ### Data resources and Authentication is not configured errors -*In Terraform 0.13 and later*, data resources have the same dependency resolution behavior [as defined for managed resources](https://www.terraform.io/docs/language/resources/behavior.html#resource-dependencies). Most data resources make an API call to a workspace. If a workspace doesn't exist yet, `authentication is not configured for provider` error is raised. To work around this issue and guarantee a proper lazy authentication with data resources, you should add `depends_on = [databricks_mws_workspaces.this]` to the body. This issue doesn't occur if workspace is created *in one module* and resources [within the workspace](workspace-management.md) are created *in another*. We do not recommend using Terraform 0.12 and earlier, if your usage involves data resources. +*In Terraform 0.13 and later*, data resources have the same dependency resolution behavior [as defined for managed resources](https://www.terraform.io/docs/language/resources/behavior.html#resource-dependencies). Most data resources make an API call to a workspace. If a workspace doesn't exist yet, `default auth: cannot configure default credentials` error is raised. To work around this issue and guarantee a proper lazy authentication with data resources, you should add `depends_on = [databricks_mws_workspaces.this]` to the body. This issue doesn't occur if workspace is created *in one module* and resources [within the workspace](workspace-management.md) are created *in another*. We do not recommend using Terraform 0.12 and earlier, if your usage involves data resources. ```hcl data "databricks_current_user" "me" { diff --git a/docs/guides/azure-workspace.md b/docs/guides/azure-workspace.md index 8eef8e95a3..6660675b6a 100644 --- a/docs/guides/azure-workspace.md +++ b/docs/guides/azure-workspace.md @@ -68,7 +68,7 @@ output "databricks_host" { ### Data resources and Authentication is not configured errors -*In Terraform 0.13 and later*, data resources have the same dependency resolution behavior [as defined for managed resources](https://www.terraform.io/docs/language/resources/behavior.html#resource-dependencies). Most data resources make an API call to a workspace. If a workspace doesn't exist yet, `authentication is not configured for provider` error is raised. To work around this issue and guarantee a proper lazy authentication with data resources, you should add `depends_on = [azurerm_databricks_workspace.this]` to the body. This issue doesn't occur if workspace is created *in one module* and resources [within the workspace](workspace-management.md) are created *in another*. We do not recommend using Terraform 0.12 and earlier, if your usage involves data resources. +*In Terraform 0.13 and later*, data resources have the same dependency resolution behavior [as defined for managed resources](https://www.terraform.io/docs/language/resources/behavior.html#resource-dependencies). Most data resources make an API call to a workspace. If a workspace doesn't exist yet, `default auth: cannot configure default credentials` error is raised. To work around this issue and guarantee a proper lazy authentication with data resources, you should add `depends_on = [azurerm_databricks_workspace.this]` to the body. This issue doesn't occur if workspace is created *in one module* and resources [within the workspace](workspace-management.md) are created *in another*. We do not recommend using Terraform 0.12 and earlier, if your usage involves data resources. ```hcl data "databricks_current_user" "me" { diff --git a/docs/guides/gcp-workspace.md b/docs/guides/gcp-workspace.md index d3901a4603..4abba4c29f 100644 --- a/docs/guides/gcp-workspace.md +++ b/docs/guides/gcp-workspace.md @@ -248,7 +248,7 @@ output "databricks_token" { ### Data resources and Authentication is not configured errors -*In Terraform 0.13 and later*, data resources have the same dependency resolution behavior [as defined for managed resources](https://www.terraform.io/docs/language/resources/behavior.html#resource-dependencies). Most data resources make an API call to a workspace. If a workspace doesn't exist yet, `authentication is not configured for provider` error is raised. To work around this issue and guarantee a proper lazy authentication with data resources, you should add `depends_on = [databricks_mws_workspaces.this]` to the body. This issue doesn't occur if workspace is created *in one module* and resources [within the workspace](workspace-management.md) are created *in another*. We do not recommend using Terraform 0.12 and earlier, if your usage involves data resources. +*In Terraform 0.13 and later*, data resources have the same dependency resolution behavior [as defined for managed resources](https://www.terraform.io/docs/language/resources/behavior.html#resource-dependencies). Most data resources make an API call to a workspace. If a workspace doesn't exist yet, `default auth: cannot configure default credentials` error is raised. To work around this issue and guarantee a proper lazy authentication with data resources, you should add `depends_on = [databricks_mws_workspaces.this]` to the body. This issue doesn't occur if workspace is created *in one module* and resources [within the workspace](workspace-management.md) are created *in another*. We do not recommend using Terraform 0.12 and earlier, if your usage involves data resources. ```hcl data "databricks_current_user" "me" { diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md index d3760573c5..70900161f7 100644 --- a/docs/guides/troubleshooting.md +++ b/docs/guides/troubleshooting.md @@ -22,7 +22,7 @@ TF_LOG=DEBUG DATABRICKS_DEBUG_TRUNCATE_BYTES=250000 terraform apply -no-color 2> ## Data resources and Authentication is not configured errors -*In Terraform 0.13 and later*, data resources have the same dependency resolution behavior [as defined for managed resources](https://www.terraform.io/docs/language/resources/behavior.html#resource-dependencies). Most data resources make an API call to a workspace. If a workspace doesn't exist yet, `authentication is not configured for provider` error is raised. To work around this issue and guarantee a proper lazy authentication with data resources, you should add `depends_on = [azurerm_databricks_workspace.this]` or `depends_on = [databricks_mws_workspaces.this]` to the body. This issue doesn't occur if workspace is created *in one module* and resources [within the workspace](guides/workspace-management.md) are created *in another*. We do not recommend using Terraform 0.12 and earlier, if your usage involves data resources. +*In Terraform 0.13 and later*, data resources have the same dependency resolution behavior [as defined for managed resources](https://www.terraform.io/docs/language/resources/behavior.html#resource-dependencies). Most data resources make an API call to a workspace. If a workspace doesn't exist yet, `default auth: cannot configure default credentials` error is raised. To work around this issue and guarantee a proper lazy authentication with data resources, you should add `depends_on = [azurerm_databricks_workspace.this]` or `depends_on = [databricks_mws_workspaces.this]` to the body. This issue doesn't occur if workspace is created *in one module* and resources [within the workspace](guides/workspace-management.md) are created *in another*. We do not recommend using Terraform 0.12 and earlier, if your usage involves data resources. ## Multiple Provider Configurations diff --git a/exporter/command.go b/exporter/command.go index bc55697bf8..6a1572d19d 100644 --- a/exporter/command.go +++ b/exporter/command.go @@ -4,9 +4,12 @@ import ( "flag" "fmt" "log" + "net/http" "os" "strings" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/common" ) @@ -45,9 +48,10 @@ func (ic *importContext) allServicesAndListing() (string, string) { } func (ic *importContext) interactivePrompts() { - for ic.Client.Authenticate(ic.Context) != nil { - ic.Client.Host = askFor("🔑 Databricks Workspace URL:") - ic.Client.Token = askFor("🔑 Databricks Workspace PAT:") + req, _ := http.NewRequest("GET", "/", nil) + for ic.Client.DatabricksClient.Config.Authenticate(req) != nil { + ic.Client.DatabricksClient.Config.Host = askFor("🔑 Databricks Workspace URL:") + ic.Client.DatabricksClient.Config.Token = askFor("🔑 Databricks Workspace PAT:") } ic.match = askFor("🔍 Match entity names (optional):") listing := "" @@ -72,8 +76,13 @@ func (ic *importContext) interactivePrompts() { // Run import according to flags func Run(args ...string) error { log.SetOutput(&logLevel) - c := common.NewClientFromEnvironment() - ic := newImportContext(c) + client, err := client.New(&config.Config{}) + if err != nil { + return err + } + ic := newImportContext(&common.DatabricksClient{ + DatabricksClient: client, + }) flags := flag.NewFlagSet("exporter", flag.ExitOnError) flags.StringVar(&ic.Module, "module", "", diff --git a/exporter/command_test.go b/exporter/command_test.go index 7783cf0e87..1ea1683a7b 100644 --- a/exporter/command_test.go +++ b/exporter/command_test.go @@ -5,6 +5,8 @@ import ( "context" "testing" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/common" "github.com/stretchr/testify/assert" ) @@ -20,7 +22,11 @@ func TestInteractivePrompts(t *testing.T) { cliInput = dummyReader("y\n") cliOutput = &bytes.Buffer{} ic := &importContext{ - Client: &common.DatabricksClient{}, + Client: &common.DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{}, + }, + }, Context: context.Background(), Importables: map[string]importable{ "x": { diff --git a/exporter/context.go b/exporter/context.go index 68d621b4c4..b52c73a465 100644 --- a/exporter/context.go +++ b/exporter/context.go @@ -113,7 +113,6 @@ func newImportContext(c *common.DatabricksClient) *importContext { p := provider.DatabricksProvider() p.TerraformVersion = "exporter" p.SetMeta(c) - c.Provider = p ctx := context.WithValue(context.Background(), common.Provider, p) ctx = context.WithValue(ctx, common.ResourceName, "exporter") c.WithCommandExecutor(func( diff --git a/exporter/importables_test.go b/exporter/importables_test.go index cdbce28cbf..636d063435 100644 --- a/exporter/importables_test.go +++ b/exporter/importables_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/commands" "github.com/databricks/terraform-provider-databricks/common" @@ -259,7 +260,7 @@ func TestImportClusterLibrariesFails(t *testing.T) { Method: "GET", Status: 404, Resource: "/api/2.0/libraries/cluster-status?cluster_id=abc", - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { ic := importContextForTest() @@ -281,7 +282,7 @@ func TestClusterListFails(t *testing.T) { Method: "GET", Resource: "/api/2.0/clusters/list", Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { ic := importContextForTest() @@ -335,7 +336,7 @@ func TestJobList_FailGetRuns(t *testing.T) { Method: "GET", Resource: "/api/2.0/jobs/runs/list?completed_only=true&job_id=1&limit=1", Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { ic := importContextForTest() @@ -386,7 +387,7 @@ func TestGroupCacheError(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups?", Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { ic := importContextForTest() @@ -445,7 +446,7 @@ func TestUserSearchFails(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Users?filter=userName%20eq%20%27dbc%27", Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { ic := importContextForTest() @@ -474,7 +475,7 @@ func TestSpnSearchFails(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=applicationId%20eq%20%27dbc%27", Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { ic := importContextForTest() @@ -533,7 +534,7 @@ func TestSpnSearchSuccess(t *testing.T) { assert.True(t, resourcesMap["databricks_service_principal"].ShouldOmitField(ic, "application_id", scim.ResourceServicePrincipal().Schema["application_id"], d)) - ic.Client.Host = "https://abc.azuredatabricks.net" + ic.Client.Config.Host = "https://abc.azuredatabricks.net" assert.True(t, resourcesMap["databricks_service_principal"].ShouldOmitField(ic, "display_name", scim.ResourceServicePrincipal().Schema["display_name"], d)) @@ -645,7 +646,7 @@ func TestGlobalInitScriptsErrors(t *testing.T) { ReuseRequest: true, MatchAny: true, Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { ic := importContextForTest() @@ -708,7 +709,7 @@ func TestRepoListFails(t *testing.T) { ReuseRequest: true, MatchAny: true, Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { ic := importContextForTest() diff --git a/go.mod b/go.mod index 72a31134a4..30584617ae 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,10 @@ go 1.18 require ( github.com/Azure/go-autorest/autorest v0.11.28 - github.com/Azure/go-autorest/autorest/adal v0.9.22 github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 - github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 + github.com/databricks/databricks-sdk-go v0.3.3-0.20230217184724-a15de889505b github.com/golang-jwt/jwt/v4 v4.4.3 - github.com/google/go-querystring v1.1.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 - github.com/hashicorp/go-retryablehttp v0.7.2 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.16.1 github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 @@ -19,16 +16,14 @@ require ( github.com/zclconf/go-cty v1.12.1 golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb golang.org/x/mod v0.8.0 - golang.org/x/oauth2 v0.5.0 - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac - google.golang.org/api v0.110.0 - gopkg.in/ini.v1 v1.67.0 ) require ( cloud.google.com/go/compute v1.18.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect @@ -41,6 +36,7 @@ require ( github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect @@ -74,11 +70,15 @@ require ( go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.1.0 // indirect golang.org/x/net v0.6.0 // indirect + golang.org/x/oauth2 v0.5.0 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + google.golang.org/api v0.110.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect google.golang.org/grpc v1.53.0 // indirect google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f9f2edec6b..02fbaf714b 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/databricks/databricks-sdk-go v0.3.3-0.20230217184724-a15de889505b h1:bPSCWahS7X/CNNWU4Xyz0l8bwo3bCt+fyPxvqKycaRo= +github.com/databricks/databricks-sdk-go v0.3.3-0.20230217184724-a15de889505b/go.mod h1:ZieiQuTk7Sq0UzQGyU6Z0uoth2BxccuqhoxYGdhqKfU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -124,15 +126,12 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.6 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA= github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= -github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= -github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= diff --git a/internal/acceptance/acceptance.go b/internal/acceptance/acceptance.go index 8785f3a36d..8678685e81 100644 --- a/internal/acceptance/acceptance.go +++ b/internal/acceptance/acceptance.go @@ -4,10 +4,13 @@ import ( "context" "fmt" "os" + "path" "regexp" "strings" "testing" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/commands" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/provider" @@ -16,6 +19,30 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) +// GetEnvOrSkipTest proceeds with test only with that env variable +func GetEnvOrSkipTest(t *testing.T, name string) string { + value := os.Getenv(name) + if value == "" { + skipf(t)("Environment variable %s is missing", name) + } + return value +} + +func skipf(t *testing.T) func(format string, args ...any) { + if isInDebug() { + // VSCode "debug test" feature doesn't show dlv logs, + // so that we fail here for maintainer productivity. + return t.Fatalf + } + return t.Skipf +} + +// detects if test is run from "debug test" feature in VSCode +func isInDebug() bool { + ex, _ := os.Executable() + return path.Base(ex) == "__debug_bin" +} + // Step ... type Step struct { Template string @@ -53,8 +80,13 @@ func Test(t *testing.T, steps []Step, otherVars ...map[string]string) { } ts := []resource.TestStep{} ctx := context.Background() - client := common.CommonEnvironmentClient() - + c, err := client.New(&config.Config{}) + if err != nil { + panic(err) + } + client := &common.DatabricksClient{ + DatabricksClient: c, + } type testResource struct { ID string Name string @@ -159,7 +191,12 @@ func ResourceCheck(name string, if !ok { return fmt.Errorf("not found: %s", name) } - client := common.CommonEnvironmentClient() - return cb(context.Background(), client, rs.Primary.ID) + client, err := client.New(&config.Config{}) + if err != nil { + panic(err) + } + return cb(context.Background(), &common.DatabricksClient{ + DatabricksClient: client, + }, rs.Primary.ID) } } diff --git a/internal/acceptance/acceptance_test.go b/internal/acceptance/acceptance_test.go new file mode 100644 index 0000000000..1292f49cc5 --- /dev/null +++ b/internal/acceptance/acceptance_test.go @@ -0,0 +1,65 @@ +package acceptance + +import ( + "context" + "testing" + + "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/terraform-provider-databricks/qa" + "github.com/databricks/terraform-provider-databricks/tokens" +) + +func TestRunningRealTerraformWithFixtureBackend(t *testing.T) { + t.Skip("fails on GitHub Actions") + defer common.CleanupEnvironment() + t.Setenv("CLOUD_ENV", "AWS") + + qa.HTTPFixturesApply(t, []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/token/create", + ExpectedRequest: tokens.TokenRequest{ + LifetimeSeconds: 6000, + Comment: "Testing token", + }, + Response: tokens.TokenResponse{ + TokenValue: "xyz", + TokenInfo: &tokens.TokenInfo{ + TokenID: "abc", + Comment: "Testing token", + }, + }, + }, + { + Method: "GET", + Resource: "/api/2.0/token/list", + ReuseRequest: true, + Response: tokens.TokenList{ + TokenInfos: []tokens.TokenInfo{ + { + TokenID: "abc", + Comment: "Testing token", + }, + }, + }, + }, + { + Method: "POST", + Resource: "/api/2.0/token/delete", + ExpectedRequest: map[string]interface{}{"token_id": "abc"}, + }, + }, + func(ctx context.Context, client *common.DatabricksClient) { + t.Setenv("DATABRICKS_HOST", client.Config.Host) + t.Setenv("DATABRICKS_TOKEN", client.Config.Token) + + Test(t, []Step{ + { + Template: `resource "databricks_token" "this" { + lifetime_seconds = 6000 + comment = "Testing token" + }`, + }, + }) + }) +} diff --git a/internal/acceptance/environment_template_test.go b/internal/acceptance/environment_template_test.go index cd22727e84..7c8acfcc68 100644 --- a/internal/acceptance/environment_template_test.go +++ b/internal/acceptance/environment_template_test.go @@ -12,8 +12,7 @@ import ( func TestEnvironmentTemplate(t *testing.T) { defer common.CleanupEnvironment() - err := os.Setenv("USER", qa.RandomName()) - assert.NoError(t, err) + t.Setenv("USER", qa.RandomName()) res := EnvironmentTemplate(t, ` resource "user" "me" { @@ -35,8 +34,8 @@ func TestEnvironmentTemplate_other_vars(t *testing.T) { func TestEnvironmentTemplate_unset_env(t *testing.T) { res, err := environmentTemplate(t, ` resource "user" "me" { - name = "{env.USER}" - email = "{env.USER}+{var.RANDOM}@example.com" + name = "{env.A}" + email = "{env.A}+{var.RANDOM}@example.com" }`) assert.Equal(t, "", res) assert.Errorf(t, err, fmt.Sprintf("please set %d variables and restart.", 2)) diff --git a/internal/azureit/azureit_test.go b/internal/azureit/azureit_test.go index d03014f7c8..5fa9d0956b 100644 --- a/internal/azureit/azureit_test.go +++ b/internal/azureit/azureit_test.go @@ -3,7 +3,6 @@ package main import ( "context" "net/http/httptest" - "os" "testing" "github.com/Azure/go-autorest/autorest/azure" @@ -23,20 +22,20 @@ func TestStart(t *testing.T) { }, }, func(ctx context.Context, client *common.DatabricksClient) { responseWriter := httptest.NewRecorder() - azure.PublicCloud.ResourceManagerEndpoint = client.Host - os.Setenv("MSI_ENDPOINT", client.Host) - os.Setenv("MSI_SECRET", "secret") - os.Setenv("ACI_CONTAINER_GROUP", "") + azure.PublicCloud.ResourceManagerEndpoint = client.Config.Host + t.Setenv("MSI_ENDPOINT", client.Config.Host) + t.Setenv("MSI_SECRET", "secret") + t.Setenv("ACI_CONTAINER_GROUP", "") triggerStart(responseWriter, nil) assert.Equal(t, "400 Bad Request", responseWriter.Result().Status) responseWriter = httptest.NewRecorder() - os.Setenv("ACI_CONTAINER_GROUP", "/abc") + t.Setenv("ACI_CONTAINER_GROUP", "/abc") triggerStart(responseWriter, nil) assert.Equal(t, "200 OK", responseWriter.Result().Status) // test that app properly fails - os.Setenv("FUNCTIONS_CUSTOMHANDLER_PORT", "abc") + t.Setenv("FUNCTIONS_CUSTOMHANDLER_PORT", "abc") defer func() { err := recover() require.NotNil(t, err) diff --git a/internal/compute/common_instances.go b/internal/compute/common_instances.go deleted file mode 100644 index fd894de12d..0000000000 --- a/internal/compute/common_instances.go +++ /dev/null @@ -1,127 +0,0 @@ -package compute - -import ( - "context" - "fmt" - "log" - "os" - "sync" - - "github.com/databricks/terraform-provider-databricks/clusters" - "github.com/databricks/terraform-provider-databricks/commands" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/pools" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" -) - -var ( - oncePool sync.Once - commonInstancePool *pools.InstancePoolAndStats -) - -// CommonInstancePoolID returns common instance pool that is supposed to be used for internal testing purposes -func CommonInstancePoolID() string { - if commonInstancePool != nil { - return commonInstancePool.InstancePoolID - } - configured := os.Getenv("TEST_INSTANCE_POOL_ID") - if configured != "" { - return configured - } - client := common.CommonEnvironmentClient() - oncePool.Do(func() { // atomic - log.Printf("[INFO] Initializing common instance pool") - ctx := context.Background() - instancePoolsAPI := pools.NewInstancePoolsAPI(ctx, client) - clustersAPI := clusters.NewClustersAPI(ctx, client) - currentUserPool := fmt.Sprintf("Terraform Integration Test by %s", os.Getenv("USER")) - poolList, err := instancePoolsAPI.List() - if err != nil { - log.Printf("[ERROR] Cannot list instance pools: %v", err) - panic(err) - } - for _, existingPool := range poolList.InstancePools { - if existingPool.InstancePoolName == currentUserPool { - log.Printf( - "[INFO] Using existing instance pool: %s/#setting/clusters/instance-pools/view/%s", - client.Host, existingPool.InstancePoolID) - commonInstancePool = &existingPool - return - } - } - instancePool := pools.InstancePool{ - PreloadedSparkVersions: []string{ - clustersAPI.LatestSparkVersionOrDefault(clusters.SparkVersionRequest{Latest: true, LongTermSupport: true})}, - NodeTypeID: clustersAPI.GetSmallestNodeType(clusters.NodeTypeRequest{ - LocalDisk: true, - }), - InstancePoolName: currentUserPool, - MaxCapacity: 10, - - IdleInstanceAutoTerminationMinutes: 15, - } - if client.IsAws() { - instancePool.AwsAttributes = &pools.InstancePoolAwsAttributes{ - Availability: clusters.AwsAvailabilitySpot, - } - } - newPool, err := instancePoolsAPI.Create(instancePool) - if err != nil { - log.Printf("[ERROR] Cannot create instance pool: %v", err) - panic(err) - } - log.Printf("[INFO] Created common instance pool: %s/#setting/clusters/instance-pools/view/%s", - client.Host, newPool.InstancePoolID) - commonInstancePool = &newPool - }) - return commonInstancePool.InstancePoolID -} - -// CommonEnvironmentClientWithRealCommandExecutor is good for internal tests -func CommonEnvironmentClientWithRealCommandExecutor() *common.DatabricksClient { - client := common.CommonEnvironmentClient() - client.WithCommandExecutor(func(ctx context.Context, _ *common.DatabricksClient) common.CommandExecutor { - return commands.NewCommandsAPI(ctx, client) - }) - return client -} - -// NewTinyClusterInCommonPool creates new cluster for short-lived purposes -func NewTinyClusterInCommonPool() (c clusters.ClusterInfo, err error) { - randomName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - ctx := context.Background() - clustersAPI := clusters.NewClustersAPI(ctx, CommonEnvironmentClientWithRealCommandExecutor()) - c, err = clustersAPI.Create(clusters.Cluster{ - NumWorkers: 1, - ClusterName: "Terraform " + randomName, - SparkVersion: clustersAPI.LatestSparkVersionOrDefault(clusters.SparkVersionRequest{ - Latest: true, - }), - InstancePoolID: CommonInstancePoolID(), - IdempotencyToken: "tf-" + randomName, - AutoterminationMinutes: 20, - }) - return -} - -// NewTinyClusterInCommonPoolPossiblyReused is recommended to be used for testing only -func NewTinyClusterInCommonPoolPossiblyReused() (c clusters.ClusterInfo) { - randomName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - currentCluster := "TerraformIntegrationTest" - ctx := context.Background() - clustersAPI := clusters.NewClustersAPI(ctx, CommonEnvironmentClientWithRealCommandExecutor()) - c, err := clustersAPI.GetOrCreateRunningCluster(currentCluster, clusters.Cluster{ - NumWorkers: 1, - ClusterName: currentCluster, - SparkVersion: clustersAPI.LatestSparkVersionOrDefault(clusters.SparkVersionRequest{ - Latest: true, - }), - InstancePoolID: CommonInstancePoolID(), - IdempotencyToken: "tf-" + randomName, - AutoterminationMinutes: 20, - }) - if err != nil { - panic(err) - } - return -} diff --git a/internal/compute/common_instances_test.go b/internal/compute/common_instances_test.go deleted file mode 100644 index ebee21e1d0..0000000000 --- a/internal/compute/common_instances_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package compute - -import ( - "context" - "os" - "sync" - "testing" - - "github.com/databricks/terraform-provider-databricks/clusters" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/pools" - - "github.com/databricks/terraform-provider-databricks/qa" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// func TestCommonInstancePoolID_Existing(t *testing.T) { -// defer common.CleanupEnvironment()() -// common.ResetCommonEnvironmentClient() -// oncePool = sync.Once{} -// qa.HTTPFixturesApply(t, []qa.HTTPFixture{ -// { -// Method: "GET", -// Resource: "/api/2.0/instance-pools/list", -// Response: pools.InstancePoolList{ -// InstancePools: []pools.InstancePoolAndStats{ -// { -// InstancePoolID: "abc", -// InstancePoolName: "Terraform Integration Test by test", -// }, -// }, -// }, -// }, -// }, func(ctx context.Context, client *common.DatabricksClient) { -// os.Setenv("DATABRICKS_HOST", client.Host) -// os.Setenv("DATABRICKS_TOKEN", client.Token) -// os.Setenv("USER", "test") - -// id := CommonInstancePoolID() -// assert.Equal(t, "abc", id) -// }) -// } - -// func TestCommonInstancePoolID_Panic(t *testing.T) { -// defer common.CleanupEnvironment()() -// defer func() { recover() }() -// common.ResetCommonEnvironmentClient() -// oncePool = sync.Once{} -// qa.HTTPFixturesApply(t, []qa.HTTPFixture{ -// { -// Method: "GET", -// Resource: "/api/2.0/instance-pools/list", -// Status: 404, -// }, -// }, func(ctx context.Context, client *common.DatabricksClient) { -// os.Setenv("DATABRICKS_HOST", client.Host) -// os.Setenv("DATABRICKS_TOKEN", client.Token) - -// CommonInstancePoolID() -// }) -// } - -var sparkVersions = clusters.SparkVersionsList{ - SparkVersions: []clusters.SparkVersion{ - { - Version: "7.1.x-cpu-ml-scala2.12", - Description: "7.1 ML (includes Apache Spark 3.0.0, Scala 2.12)", - }, - { - Version: "apache-spark-2.4.x-scala2.11", - Description: "Light 2.4 (includes Apache Spark 2.4, Scala 2.11)", - }, - { - Version: "7.3.x-scala2.12", - Description: "7.3 LTS (includes Apache Spark 3.0.1, Scala 2.12)", - }, - { - Version: "6.4.x-scala2.11", - Description: "6.4 (includes Apache Spark 2.4.5, Scala 2.11)", - }, - }, -} - -var nodeTypes = clusters.NodeTypeList{ - NodeTypes: []clusters.NodeType{ - { - NodeTypeID: "m4.large", - InstanceTypeID: "m4.large", - NodeInstanceType: &clusters.NodeInstanceType{ - LocalDisks: 1, - InstanceTypeID: "m4.large", - }, - }, - }, -} - -func TestNewTinyClusterInCommonPoolPossiblyReused(t *testing.T) { - defer common.CleanupEnvironment()() - common.ResetCommonEnvironmentClient() - oncePool = sync.Once{} - client, server, err := qa.HttpFixtureClient(t, []qa.HTTPFixture{ - { - Method: "GET", - ReuseRequest: true, - Resource: "/api/2.0/clusters/spark-versions", - Response: sparkVersions, - }, - { - Method: "GET", - Resource: "/api/2.0/instance-pools/list", - Response: pools.InstancePoolList{ - InstancePools: []pools.InstancePoolAndStats{}, - }, - }, - { - Method: "POST", - Resource: "/api/2.0/instance-pools/create", - ExpectedRequest: pools.InstancePool{ - AwsAttributes: &pools.InstancePoolAwsAttributes{ - Availability: "SPOT", - }, - NodeTypeID: "m4.large", - IdleInstanceAutoTerminationMinutes: 15, - PreloadedSparkVersions: []string{"7.3.x-scala2.12"}, - InstancePoolName: "Terraform Integration Test by test", - MaxCapacity: 10, - }, - Response: pools.InstancePoolAndStats{ - InstancePoolID: "abc", - }, - }, - { - Method: "GET", - Resource: "/api/2.0/clusters/list", - Response: map[string]any{}, - }, - { - Method: "GET", - ReuseRequest: true, - Resource: "/api/2.0/clusters/list-node-types", - Response: nodeTypes, - }, - { - Method: "POST", - Resource: "/api/2.0/clusters/create", - Response: clusters.ClusterID{ - ClusterID: "bcd", - }, - }, - { - Method: "GET", - Resource: "/api/2.0/clusters/get?cluster_id=bcd", - Response: clusters.ClusterInfo{ - State: "RUNNING", - }, - }, - }) - defer server.Close() - require.NoError(t, err) - // trick common env client - err = os.Setenv("DATABRICKS_HOST", client.Host) - assert.NoError(t, err) - err = os.Setenv("DATABRICKS_TOKEN", client.Token) - assert.NoError(t, err) - err = os.Setenv("USER", "test") - assert.NoError(t, err) - - c := NewTinyClusterInCommonPoolPossiblyReused() - assert.NotNil(t, c) -} - -func TestNewTinyClusterInCommonPool(t *testing.T) { - defer common.CleanupEnvironment()() - common.ResetCommonEnvironmentClient() - oncePool = sync.Once{} - client, server, err := qa.HttpFixtureClient(t, []qa.HTTPFixture{ - { - Method: "GET", - ReuseRequest: true, - Resource: "/api/2.0/clusters/spark-versions", - Response: sparkVersions, - }, - { - Method: "GET", - Resource: "/api/2.0/instance-pools/list", - Response: pools.InstancePoolList{ - InstancePools: []pools.InstancePoolAndStats{ - { - InstancePoolName: "Terraform Integration Test by test", - InstancePoolID: "abc", - }, - }, - }, - }, - { - Method: "POST", - Resource: "/api/2.0/clusters/create", - Response: clusters.ClusterID{ - ClusterID: "bcd", - }, - }, - { - Method: "GET", - Resource: "/api/2.0/clusters/get?cluster_id=bcd", - Response: clusters.ClusterInfo{ - State: "RUNNING", - }, - }, - }) - defer server.Close() - require.NoError(t, err) - // trick common env client - os.Setenv("DATABRICKS_HOST", client.Host) - os.Setenv("DATABRICKS_TOKEN", client.Token) - os.Setenv("USER", "test") - - c, err := NewTinyClusterInCommonPool() - require.NoError(t, err) - assert.NotNil(t, c) - - client = CommonEnvironmentClientWithRealCommandExecutor() - client.CommandExecutor(context.Background()) -} diff --git a/jobs/acceptance/job_test.go b/jobs/acceptance/job_test.go index 12a20d520b..2911819041 100644 --- a/jobs/acceptance/job_test.go +++ b/jobs/acceptance/job_test.go @@ -1,92 +1,11 @@ package acceptance import ( - "context" - "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - - "github.com/databricks/terraform-provider-databricks/clusters" - "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/internal/acceptance" - "github.com/databricks/terraform-provider-databricks/internal/compute" - "github.com/databricks/terraform-provider-databricks/jobs" - "github.com/databricks/terraform-provider-databricks/libraries" - "github.com/databricks/terraform-provider-databricks/qa" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestAccAwsJobsCreate(t *testing.T) { - qa.RequireCloudEnv(t, "aws") - client := common.NewClientFromEnvironment() - jobsAPI := jobs.NewJobsAPI(context.Background(), client) - clustersAPI := clusters.NewClustersAPI(context.Background(), client) - sparkVersion := clustersAPI.LatestSparkVersionOrDefault(clusters.SparkVersionRequest{Latest: true, LongTermSupport: true}) - - jobSettings := jobs.JobSettings{ - NewCluster: &clusters.Cluster{ - NumWorkers: 2, - SparkVersion: sparkVersion, - SparkConf: nil, - AwsAttributes: &clusters.AwsAttributes{ - Availability: "ON_DEMAND", - }, - NodeTypeID: clustersAPI.GetSmallestNodeType(clusters.NodeTypeRequest{ - LocalDisk: true, - }), - }, - NotebookTask: &jobs.NotebookTask{ - NotebookPath: "/tf-test/demo-terraform/demo-notebook", - }, - Name: "1-test-job", - Libraries: []libraries.Library{ - { - Maven: &libraries.Maven{ - Coordinates: "org.jsoup:jsoup:1.7.2", - }, - }, - }, - EmailNotifications: &jobs.EmailNotifications{ - OnStart: []string{}, - OnSuccess: []string{}, - OnFailure: []string{}, - }, - TimeoutSeconds: 3600, - MaxRetries: 1, - Schedule: &jobs.CronSchedule{ - QuartzCronExpression: "0 15 22 ? * *", - TimezoneID: "America/Los_Angeles", - }, - MaxConcurrentRuns: 1, - } - - job, err := jobsAPI.Create(jobSettings) - require.NoError(t, err) - id := job.ID() - defer func() { - err := jobsAPI.Delete(id) - assert.NoError(t, err) - }() - t.Log(id) - job, err = jobsAPI.Read(id) - assert.NoError(t, err) - assert.True(t, job.Settings.NewCluster.SparkVersion == sparkVersion, "Something is wrong with spark version") - - newSparkVersion := clustersAPI.LatestSparkVersionOrDefault(clusters.SparkVersionRequest{Latest: true}) - jobSettings.NewCluster.SparkVersion = newSparkVersion - - err = jobsAPI.Update(id, jobSettings) - assert.NoError(t, err) - - job, err = jobsAPI.Read(id) - assert.NoError(t, err) - assert.True(t, job.Settings.NewCluster.SparkVersion == newSparkVersion, "Something is wrong with spark version") -} - func TestAccJobTasks(t *testing.T) { t.Parallel() acceptance.Test(t, []acceptance.Step{ @@ -169,116 +88,3 @@ func TestAccJobTasks(t *testing.T) { }, }) } - -func TestAccJobResource(t *testing.T) { - qa.RequireAnyCloudEnv(t) - t.Parallel() - clustersAPI := clusters.NewClustersAPI(context.Background(), common.CommonEnvironmentClient()) - sparkVersion := clustersAPI.LatestSparkVersionOrDefault(clusters.SparkVersionRequest{Latest: true, LongTermSupport: true}) - acceptance.AccTest(t, resource.TestCase{ - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(`resource "databricks_job" "this" { - new_cluster { - autoscale { - min_workers = 2 - max_workers = 3 - } - instance_pool_id = "%s" - spark_version = "%s" - } - notebook_task { - notebook_path = "/Production/MakeFeatures" - } - email_notifications { - no_alert_for_skipped_runs = true - } - name = "%s" - timeout_seconds = 3600 - max_retries = 1 - max_concurrent_runs = 1 - }`, compute.CommonInstancePoolID(), sparkVersion, qa.RandomLongName()), - // compose a basic test, checking both remote and local values - Check: resource.ComposeTestCheckFunc( - // query the API to retrieve the tokenInfo object - acceptance.ResourceCheck("databricks_job.this", - func(ctx context.Context, client *common.DatabricksClient, id string) error { - job, err := jobs.NewJobsAPI(ctx, client).Read(id) - assert.NoError(t, err) - assert.NotNil(t, job.Settings) - assert.NotNil(t, job.Settings.NewCluster) - assert.NotNil(t, job.Settings.NewCluster.Autoscale) - assert.NotNil(t, job.Settings.NotebookTask) - assert.Equal(t, 2, int(job.Settings.NewCluster.Autoscale.MinWorkers)) - assert.Equal(t, 3, int(job.Settings.NewCluster.Autoscale.MaxWorkers)) - assert.Equal(t, sparkVersion, job.Settings.NewCluster.SparkVersion) - assert.Equal(t, "/Production/MakeFeatures", job.Settings.NotebookTask.NotebookPath) - assert.Equal(t, 3600, int(job.Settings.TimeoutSeconds)) - assert.Equal(t, 1, int(job.Settings.MaxRetries)) - assert.Equal(t, 1, int(job.Settings.MaxConcurrentRuns)) - return nil - }), - ), - }, - }, - }) -} - -func TestAccAwsJobResource_NoInstancePool(t *testing.T) { - qa.RequireCloudEnv(t, "aws") - clustersAPI := clusters.NewClustersAPI(context.Background(), common.CommonEnvironmentClient()) - sparkVersion := clustersAPI.LatestSparkVersionOrDefault(clusters.SparkVersionRequest{Latest: true, LongTermSupport: true}) - randomStr := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - instanceProfileARN := fmt.Sprintf("arn:aws:iam::999999999999:instance-profile/tf-test-%s", randomStr) - acceptance.AccTest(t, resource.TestCase{ - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(`resource "databricks_job" "this" { - new_cluster { - num_workers = 1 - aws_attributes { - zone_id = "eu-central-1" - spot_bid_price_percent = "100" - instance_profile_arn = "%s" - first_on_demand = 1 - ebs_volume_type = "GENERAL_PURPOSE_SSD" - ebs_volume_count = 1 - ebs_volume_size = 32 - } - node_type_id = "m4.large" - spark_version = "%s" - } - notebook_task { - notebook_path = "/Production/MakeFeatures" - } - library { - pypi { - package = "networkx" - } - } - email_notifications { - no_alert_for_skipped_runs = true - } - name = "%s" - timeout_seconds = 3600 - max_retries = 1 - max_concurrent_runs = 1 - }`, instanceProfileARN, sparkVersion, - qa.RandomLongName()), - // compose a basic test, checking both remote and local values - Check: resource.ComposeTestCheckFunc( - // query the API to retrieve the tokenInfo object - acceptance.ResourceCheck("databricks_job.this", - func(ctx context.Context, client *common.DatabricksClient, id string) error { - job, err := jobs.NewJobsAPI(ctx, client).Read(id) - assert.NoError(t, err) - assert.NotNil(t, job.Settings) - assert.NotNil(t, job.Settings.NewCluster) - assert.NotNil(t, job.Settings.NewCluster.AwsAttributes) - return nil - }), - ), - }, - }, - }) -} diff --git a/jobs/data_job_test.go b/jobs/data_job_test.go index 2e13142974..32323070be 100755 --- a/jobs/data_job_test.go +++ b/jobs/data_job_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" ) @@ -124,7 +124,7 @@ func TestDataSourceQueryableJobNoMatchId(t *testing.T) { { Method: "GET", Resource: "/api/2.0/jobs/get?job_id=567", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "Job 567 does not exist.", }, diff --git a/jobs/resource_job.go b/jobs/resource_job.go index 26ee185254..dc923e1958 100644 --- a/jobs/resource_job.go +++ b/jobs/resource_job.go @@ -2,6 +2,7 @@ package jobs import ( "context" + "errors" "fmt" "log" "sort" @@ -13,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/libraries" @@ -506,8 +508,8 @@ func wrapMissingJobError(err error, id string) error { if err == nil { return nil } - apiErr, ok := err.(common.APIError) - if !ok { + var apiErr *apierr.APIError + if !errors.As(err, &apiErr) { return err } if apiErr.IsMissing() { diff --git a/jobs/resource_job_test.go b/jobs/resource_job_test.go index 35285bbd8a..85740cabc1 100644 --- a/jobs/resource_job_test.go +++ b/jobs/resource_job_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/libraries" @@ -869,7 +870,7 @@ func TestResourceJobRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/jobs/get?job_id=789", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -890,7 +891,7 @@ func TestResourceJobRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/jobs/get?job_id=789", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -1135,7 +1136,7 @@ func TestJobRestarts(t *testing.T) { Method: "GET", Resource: "/api/2.0/jobs/runs/get?run_id=345", Status: 400, - Response: common.APIError{ + Response: apierr.APIError{ Message: "nope", }, }, @@ -1200,7 +1201,7 @@ func TestJobRestarts(t *testing.T) { Method: "POST", Resource: "/api/2.0/jobs/runs/cancel", Status: 400, - Response: common.APIError{ + Response: apierr.APIError{ Message: "nope", }, }, @@ -1208,7 +1209,7 @@ func TestJobRestarts(t *testing.T) { Method: "GET", Resource: "/api/2.0/jobs/runs/list?active_only=true&job_id=222", Status: 400, - Response: common.APIError{ + Response: apierr.APIError{ Message: "nope", }, }, diff --git a/libraries/acceptance/libraries_api_test.go b/libraries/acceptance/libraries_api_test.go deleted file mode 100644 index e8b64f3032..0000000000 --- a/libraries/acceptance/libraries_api_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package acceptance - -import ( - "context" - "os" - "testing" - - "github.com/databricks/terraform-provider-databricks/clusters" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/internal/compute" - "github.com/databricks/terraform-provider-databricks/libraries" - "github.com/stretchr/testify/assert" -) - -func TestAccLibraryCreate(t *testing.T) { - cloud := os.Getenv("CLOUD_ENV") - if cloud == "" { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - client := common.CommonEnvironmentClient() - clusterInfo, err := compute.NewTinyClusterInCommonPool() - assert.NoError(t, err) - defer func() { - ctx := context.Background() - err := clusters.NewClustersAPI(ctx, client).PermanentDelete(clusterInfo.ClusterID) - assert.NoError(t, err) - }() - - clusterID := clusterInfo.ClusterID - libs := []libraries.Library{ - { - Pypi: &libraries.PyPi{ - Package: "networkx", - }, - }, - { - Maven: &libraries.Maven{ - Coordinates: "com.crealytics:spark-excel_2.12:0.13.1", - }, - }, - } - - ctx := context.Background() - libsAPI := libraries.NewLibrariesAPI(ctx, client) - err = libsAPI.Install(libraries.ClusterLibraryList{ - ClusterID: clusterID, - Libraries: libs, - }) - assert.NoError(t, err) - - defer func() { - err = libsAPI.Uninstall(libraries.ClusterLibraryList{ - ClusterID: clusterID, - Libraries: libs, - }) - assert.NoError(t, err) - }() - - libraryStatusList, err := libsAPI.ClusterStatus(clusterID) - assert.NoError(t, err) - assert.Equal(t, len(libraryStatusList.LibraryStatuses), len(libs)) -} diff --git a/libraries/libraries_api.go b/libraries/libraries_api.go index 8c67d23ceb..d7a77f543d 100644 --- a/libraries/libraries_api.go +++ b/libraries/libraries_api.go @@ -2,12 +2,14 @@ package libraries import ( "context" + "errors" "fmt" "log" "sort" "strings" "time" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -79,8 +81,8 @@ func (a LibrariesAPI) WaitForLibrariesInstalled(wait Wait) (result *ClusterLibra err = resource.RetryContext(a.context, wait.Timeout, func() *resource.RetryError { libsClusterStatus, err := a.ClusterStatus(wait.ClusterID) if err != nil { - apiErr, ok := err.(common.APIError) - if !ok { + var apiErr *apierr.APIError + if !errors.As(err, &apiErr) { return resource.NonRetryableError(err) } if apiErr.StatusCode != 404 && strings.Contains(apiErr.Message, diff --git a/libraries/libraries_api_test.go b/libraries/libraries_api_test.go index 25ad0413f9..764da6afdf 100644 --- a/libraries/libraries_api_test.go +++ b/libraries/libraries_api_test.go @@ -2,9 +2,11 @@ package libraries import ( "context" + "errors" "testing" "time" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -18,14 +20,14 @@ func TestWaitForLibrariesInstalled(t *testing.T) { Resource: "/api/2.0/libraries/cluster-status?cluster_id=missing", ReuseRequest: true, Status: 404, - Response: common.NotFound("missing"), + Response: apierr.NotFound("missing"), }, { Method: "GET", Resource: "/api/2.0/libraries/cluster-status?cluster_id=error", ReuseRequest: true, Status: 500, - Response: common.APIError{ + Response: apierr.APIError{ Message: "internal error", }, }, @@ -34,7 +36,7 @@ func TestWaitForLibrariesInstalled(t *testing.T) { Resource: "/api/2.0/libraries/cluster-status?cluster_id=1005-abcd", ReuseRequest: true, Status: 400, - Response: common.APIError{ + Response: apierr.APIError{ Message: "Cluster 1005-abcd does not exist", }, }, @@ -129,7 +131,8 @@ func TestWaitForLibrariesInstalled(t *testing.T) { "1005-abcd", 50 * time.Millisecond, false, false, }) - ae, _ := err.(common.APIError) + var ae *apierr.APIError + assert.True(t, errors.As(err, &ae)) assert.Equal(t, 404, ae.StatusCode) assert.Equal(t, "Cluster 1005-abcd does not exist", ae.Message) }) diff --git a/mws/data_mws_credentials.go b/mws/data_mws_credentials.go index d7bc7a8a79..701407aa63 100755 --- a/mws/data_mws_credentials.go +++ b/mws/data_mws_credentials.go @@ -14,10 +14,10 @@ func DataSourceMwsCredentials() *schema.Resource { } return common.DataResource(mwsCredentialsData{}, func(ctx context.Context, e any, c *common.DatabricksClient) error { data := e.(*mwsCredentialsData) - if c.AccountID == "" { + if c.Config.AccountID == "" { return fmt.Errorf("provider block is missing `account_id` property") } - credentials, err := NewCredentialsAPI(ctx, c).List(c.AccountID) + credentials, err := NewCredentialsAPI(ctx, c).List(c.Config.AccountID) if err != nil { return err } diff --git a/mws/data_mws_workspaces.go b/mws/data_mws_workspaces.go index b16a5a0c6d..063629ab78 100755 --- a/mws/data_mws_workspaces.go +++ b/mws/data_mws_workspaces.go @@ -14,10 +14,10 @@ func DataSourceMwsWorkspaces() *schema.Resource { } return common.DataResource(mwsWorkspacesData{}, func(ctx context.Context, e any, c *common.DatabricksClient) error { data := e.(*mwsWorkspacesData) - if c.AccountID == "" { + if c.Config.AccountID == "" { return fmt.Errorf("provider block is missing `account_id` property") } - workspaces, err := NewWorkspacesAPI(ctx, c).List(c.AccountID) + workspaces, err := NewWorkspacesAPI(ctx, c).List(c.Config.AccountID) if err != nil { return err } diff --git a/mws/resource_mws_credentials_test.go b/mws/resource_mws_credentials_test.go index 947f23feb3..de9c9eae09 100644 --- a/mws/resource_mws_credentials_test.go +++ b/mws/resource_mws_credentials_test.go @@ -1,38 +1,15 @@ package mws import ( - "context" "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) -func TestMwsAccCreds(t *testing.T) { - arn := qa.GetEnvOrSkipTest(t, "TEST_CROSSACCOUNT_ARN") - acctID := qa.GetEnvOrSkipTest(t, "DATABRICKS_ACCOUNT_ID") - client := common.CommonEnvironmentClient() - credsAPI := NewCredentialsAPI(context.Background(), client) - credsList, err := credsAPI.List(acctID) - assert.NoError(t, err) - t.Log(credsList) - - myCreds, err := credsAPI.Create(acctID, qa.RandomName("tf-test"), arn) - assert.NoError(t, err) - - myCredsFull, err := credsAPI.Read(acctID, myCreds.CredentialsID) - assert.NoError(t, err) - t.Log(myCredsFull.AwsCredentials.StsRole.ExternalID) - - defer func() { - err = credsAPI.Delete(acctID, myCreds.CredentialsID) - assert.NoError(t, err) - }() -} - func TestResourceCredentialsCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -83,7 +60,7 @@ func TestResourceCredentialsCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/accounts/abc/credentials", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -138,7 +115,7 @@ func TestResourceCredentialsRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/credentials/cid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -158,7 +135,7 @@ func TestResourceCredentialsRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/credentials/cid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -195,7 +172,7 @@ func TestResourceCredentialsDelete_Error(t *testing.T) { { Method: "DELETE", Resource: "/api/2.0/accounts/abc/credentials/cid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/mws/resource_mws_customer_managed_keys_test.go b/mws/resource_mws_customer_managed_keys_test.go index 6014c64d01..a828b65225 100644 --- a/mws/resource_mws_customer_managed_keys_test.go +++ b/mws/resource_mws_customer_managed_keys_test.go @@ -4,45 +4,11 @@ import ( "context" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) -// TODO: move to `acceptance` -func TestMwsAccCustomerManagedKeys(t *testing.T) { - acctID := qa.GetEnvOrSkipTest(t, "DATABRICKS_ACCOUNT_ID") - kmsKeyArn := qa.GetEnvOrSkipTest(t, "TEST_MANAGED_KMS_KEY_ARN") - kmsKeyAlias := qa.GetEnvOrSkipTest(t, "TEST_MANAGED_KMS_KEY_ALIAS") - client := common.CommonEnvironmentClient() - cmkAPI := NewCustomerManagedKeysAPI(context.Background(), client) - cmkList, err := cmkAPI.List(acctID) - assert.NoError(t, err) - t.Log(cmkList) - - keyInfo, err := cmkAPI.Create(CustomerManagedKey{ - AwsKeyInfo: &AwsKeyInfo{ - KeyArn: kmsKeyArn, - KeyAlias: kmsKeyAlias, - }, - AccountID: acctID, - UseCases: []string{"MANAGED_SERVICES"}, - }) - assert.NoError(t, err) - - keyID := keyInfo.CustomerManagedKeyID - - defer func() { - err := cmkAPI.Delete(acctID, keyID) - assert.NoError(t, err) - }() - - getKeyInfo, err := cmkAPI.Read(acctID, keyID) - assert.NoError(t, err) - assert.NotNil(t, getKeyInfo, "key info should not be nil") -} - func TestResourceCustomerManagedKeyCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -109,7 +75,7 @@ func TestResourceCustomerManagedKeyCreate_Error(t *testing.T) { }, UseCases: []string{"MANAGED_SERVICE"}, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -194,7 +160,7 @@ func TestResourceCustomerManagedKeyRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/customer-managed-keys/cmkid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ Message: "Invalid endpoint", }, Status: 404, diff --git a/mws/resource_mws_log_delivery_test.go b/mws/resource_mws_log_delivery_test.go index d23aa176a0..5bb05af13f 100644 --- a/mws/resource_mws_log_delivery_test.go +++ b/mws/resource_mws_log_delivery_test.go @@ -1,60 +1,14 @@ package mws import ( - "context" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestMwsAccLogDelivery(t *testing.T) { - acctID := qa.GetEnvOrSkipTest(t, "DATABRICKS_ACCOUNT_ID") - roleARN := qa.GetEnvOrSkipTest(t, "TEST_LOGDELIVERY_ARN") - bucket := qa.GetEnvOrSkipTest(t, "TEST_LOGDELIVERY_BUCKET") - client := common.CommonEnvironmentClient() - randomName := qa.RandomName("tf-logdelivery-") - - ctx := context.Background() - logDeliveryAPI := NewLogDeliveryAPI(ctx, client) - credentialsAPI := NewCredentialsAPI(ctx, client) - storageConfigurationsAPI := NewStorageConfigurationsAPI(ctx, client) - - creds, err := credentialsAPI.Create(acctID, randomName, roleARN) - require.NoError(t, err) - defer func() { - assert.NoError(t, credentialsAPI.Delete(acctID, creds.CredentialsID)) - }() - - storageConfig, err := storageConfigurationsAPI.Create(acctID, randomName, bucket) - require.NoError(t, err) - defer func() { - assert.NoError(t, storageConfigurationsAPI.Delete(acctID, storageConfig.StorageConfigurationID)) - }() - - configID, err := logDeliveryAPI.Create(LogDeliveryConfiguration{ - AccountID: acctID, - CredentialsID: creds.CredentialsID, - StorageConfigurationID: storageConfig.StorageConfigurationID, - ConfigName: randomName, - DeliveryPathPrefix: randomName, - LogType: "AUDIT_LOGS", - OutputFormat: "JSON", - }) - require.NoError(t, err) - defer func() { - assert.NoError(t, logDeliveryAPI.Patch(acctID, configID, "DISABLED")) - }() - - ldc, err := logDeliveryAPI.Read(acctID, configID) - require.NoError(t, err) - assert.Equal(t, "ENABLED", ldc.Status) -} - func TestResourceLogDeliveryCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -188,7 +142,7 @@ func TestResourceLogDeliveryCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/accounts/abc/log-delivery", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -272,7 +226,7 @@ func TestResourceLogDeliveryRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/log-delivery/nid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -353,7 +307,7 @@ func TestUpdateLogDeliveryError(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/accounts/abc/log-delivery/nid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -415,7 +369,7 @@ func TestResourceLogDeliveryDelete_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/accounts/abc/log-delivery/nid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/mws/resource_mws_networks.go b/mws/resource_mws_networks.go index ca5325abc4..b4ff69ad4f 100644 --- a/mws/resource_mws_networks.go +++ b/mws/resource_mws_networks.go @@ -6,6 +6,7 @@ import ( "log" "time" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -46,7 +47,7 @@ func (a NetworksAPI) Delete(mwsAcctID, networksID string) error { } return resource.RetryContext(a.context, 60*time.Second, func() *resource.RetryError { network, err := a.Read(mwsAcctID, networksID) - if common.IsMissing(err) { + if apierr.IsMissing(err) { log.Printf("[INFO] Network %s/%s is removed.", mwsAcctID, networksID) return nil } diff --git a/mws/resource_mws_networks_test.go b/mws/resource_mws_networks_test.go index b5bb9e14a5..dcb4de2c39 100644 --- a/mws/resource_mws_networks_test.go +++ b/mws/resource_mws_networks_test.go @@ -1,47 +1,14 @@ package mws import ( - "context" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) -func TestMwsAccNetworks(t *testing.T) { - acctID := qa.GetEnvOrSkipTest(t, "DATABRICKS_ACCOUNT_ID") - client := common.CommonEnvironmentClient() - if !client.IsAws() { - t.Skip("only AWS") - } - ctx := context.Background() - networksAPI := NewNetworksAPI(ctx, client) - networksList, err := networksAPI.List(acctID) - assert.NoError(t, err) - t.Log(networksList) - - network := Network{ - AccountID: acctID, - NetworkName: qa.RandomName(), - VPCID: "vpc-0abcdef1234567890", - SubnetIds: []string{"subnet-0123456789abcdef0", "subnet-0fedcba9876543210"}, - SecurityGroupIds: []string{"sg-0a1b2c3d4e5f6a7b8"}, - } - err = networksAPI.Create(&network) - assert.NoError(t, err) - defer func() { - err = networksAPI.Delete(acctID, network.NetworkID) - assert.NoError(t, err) - }() - - myNetworkFull, err := networksAPI.Read(acctID, network.NetworkID) - assert.NoError(t, err) - t.Log(myNetworkFull) -} - func TestResourceNetworkCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -178,7 +145,7 @@ func TestResourceNetworkCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/accounts/abc/networks", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -235,7 +202,7 @@ func TestResourceNetworkRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/networks/nid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -255,7 +222,7 @@ func TestResourceNetworkRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/networks/nid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -291,7 +258,7 @@ func TestResourceNetworkDelete(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/networks/nid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Yes, it's not found", }, @@ -312,7 +279,7 @@ func TestResourceNetworkDelete_Error(t *testing.T) { { Method: "DELETE", Resource: "/api/2.0/accounts/abc/networks/nid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/mws/resource_mws_permission_assignment.go b/mws/resource_mws_permission_assignment.go index e243edf79c..8481e6a245 100644 --- a/mws/resource_mws_permission_assignment.go +++ b/mws/resource_mws_permission_assignment.go @@ -6,6 +6,7 @@ import ( "fmt" "strconv" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -24,22 +25,22 @@ type Permissions struct { } func (a PermissionAssignmentAPI) CreateOrUpdate(workspaceId, principalId int64, r Permissions) error { - if a.client.AccountID == "" { + if a.client.Config.AccountID == "" { return errors.New("must have `account_id` on provider") } path := fmt.Sprintf( "/preview/accounts/%s/workspaces/%d/permissionassignments/principals/%d", - a.client.AccountID, workspaceId, principalId) + a.client.Config.AccountID, workspaceId, principalId) return a.client.Put(a.context, path, r) } func (a PermissionAssignmentAPI) Remove(workspaceId, principalId string) error { - if a.client.AccountID == "" { + if a.client.Config.AccountID == "" { return errors.New("must have `account_id` on provider") } path := fmt.Sprintf( "/preview/accounts/%s/workspaces/%s/permissionassignments/principals/%s", - a.client.AccountID, workspaceId, principalId) + a.client.Config.AccountID, workspaceId, principalId) return a.client.Delete(a.context, path, nil) } @@ -67,15 +68,15 @@ func (l PermissionAssignmentList) ForPrincipal(principalId int64) (res Permissio } return Permissions{v.Permissions}, nil } - return res, common.NotFound(fmt.Sprintf("%d not found", principalId)) + return res, apierr.NotFound(fmt.Sprintf("%d not found", principalId)) } func (a PermissionAssignmentAPI) List(workspaceId int64) (list PermissionAssignmentList, err error) { - if a.client.AccountID == "" { + if a.client.Config.AccountID == "" { return list, errors.New("must have `account_id` on provider") } path := fmt.Sprintf("/preview/accounts/%s/workspaces/%d/permissionassignments", - a.client.AccountID, workspaceId) + a.client.Config.AccountID, workspaceId) err = a.client.Get(a.context, path, nil, &list) return } diff --git a/mws/resource_mws_private_access_settings_test.go b/mws/resource_mws_private_access_settings_test.go index ab0d2c7181..af1c268099 100644 --- a/mws/resource_mws_private_access_settings_test.go +++ b/mws/resource_mws_private_access_settings_test.go @@ -4,42 +4,13 @@ import ( "context" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestMwsAccPAS(t *testing.T) { - t.SkipNow() - acctID := qa.GetEnvOrSkipTest(t, "DATABRICKS_ACCOUNT_ID") - awsRegion := qa.GetEnvOrSkipTest(t, "AWS_REGION") - client := common.CommonEnvironmentClient() - ctx := context.Background() - pasAPI := NewPrivateAccessSettingsAPI(ctx, client) - pasList, err := pasAPI.List(acctID) - assert.NoError(t, err) - t.Log(pasList) - - pas := PrivateAccessSettings{ - AccountID: acctID, - PasName: qa.RandomName("tf-"), - Region: awsRegion, - } - err = pasAPI.Create(&pas) - assert.NoError(t, err) - defer func() { - err = pasAPI.Delete(acctID, pas.PasID) - assert.NoError(t, err) - }() - - myPAS, err := pasAPI.Read(acctID, pas.PasID) - assert.NoError(t, err) - t.Log(myPAS) -} - func TestResourcePASCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -86,7 +57,7 @@ func TestResourcePASCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/accounts/abc/private-access-settings", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -137,7 +108,7 @@ func TestResourcePAStRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/private-access-settings/pas_id", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -157,7 +128,7 @@ func TestResourcePAS_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/private-access-settings/pas_id", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -234,7 +205,7 @@ func TestResourcePASDelete(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/private-access-settings/pas_id", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Yes, it's not found", }, @@ -255,7 +226,7 @@ func TestResourcePASDelete_Error(t *testing.T) { { Method: "DELETE", Resource: "/api/2.0/accounts/abc/private-access-settings/pas_id", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/mws/resource_mws_storage_configurations_test.go b/mws/resource_mws_storage_configurations_test.go index 1a9035329b..c21b46c156 100644 --- a/mws/resource_mws_storage_configurations_test.go +++ b/mws/resource_mws_storage_configurations_test.go @@ -1,43 +1,13 @@ package mws import ( - "context" - "strings" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) -func TestMwsAccStorageConfigurations(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode.") - } - cloudEnv := qa.GetEnvOrSkipTest(t, "CLOUD_ENV") - if strings.Contains(cloudEnv, "azure") { - t.Skip("cannot run Account Workspace tests in azure") - } - acctID := qa.GetEnvOrSkipTest(t, "DATABRICKS_ACCOUNT_ID") - storageAPI := NewStorageConfigurationsAPI(context.Background(), common.CommonEnvironmentClient()) - storageConfigsList, err := storageAPI.List(acctID) - assert.NoError(t, err) - t.Log(storageConfigsList) - - storageConfig, err := storageAPI.Create(acctID, "sri-mws-terraform-storage-root-bucket", "sri-root-s3-bucket") - assert.NoError(t, err) - - myStorageConfig, err := storageAPI.Read(acctID, storageConfig.StorageConfigurationID) - assert.NoError(t, err) - t.Log(myStorageConfig.RootBucketInfo.BucketName) - - defer func() { - err = storageAPI.Delete(acctID, storageConfig.StorageConfigurationID) - assert.NoError(t, err) - }() -} - func TestResourceStorageConfigurationCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -84,7 +54,7 @@ func TestResourceStorageConfigurationCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/accounts/abc/storage-configurations", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -136,7 +106,7 @@ func TestResourceStorageConfigurationRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/storage-configurations/scid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -156,7 +126,7 @@ func TestResourceStorageConfigurationRead_Error(t *testing.T) { { // read log output for correct url... Method: "GET", Resource: "/api/2.0/accounts/abc/storage-configurations/scid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -193,7 +163,7 @@ func TestResourceStorageConfigurationDelete_Error(t *testing.T) { { Method: "DELETE", Resource: "/api/2.0/accounts/abc/storage-configurations/scid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/mws/resource_mws_vpc_endpoint_test.go b/mws/resource_mws_vpc_endpoint_test.go index e0c2055947..849981ae5e 100644 --- a/mws/resource_mws_vpc_endpoint_test.go +++ b/mws/resource_mws_vpc_endpoint_test.go @@ -2,50 +2,15 @@ package mws import ( "context" - "os" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestMwsAccVPCEndpointIntegration(t *testing.T) { - t.SkipNow() - cloudEnv := os.Getenv("CLOUD_ENV") - if cloudEnv != "MWS" { - t.Skip("Cannot run test on non-MWS environment") - } - acctID := qa.GetEnvOrSkipTest(t, "DATABRICKS_ACCOUNT_ID") - awsvreID := qa.GetEnvOrSkipTest(t, "TEST_REST_API_VPC_ENDPOINT") - awsRegion := qa.GetEnvOrSkipTest(t, "AWS_REGION") - client := common.CommonEnvironmentClient() - ctx := context.Background() - vpcEndpointAPI := NewVPCEndpointAPI(ctx, client) - endpointList, err := vpcEndpointAPI.List(acctID) - require.NoError(t, err) - t.Logf("VPC Endpoints: %v", endpointList) - - vpcEndpoint := VPCEndpoint{ - AccountID: acctID, - VPCEndpointName: qa.RandomName("tf-"), - AwsVPCEndpointID: awsvreID, - Region: awsRegion, - } - err = vpcEndpointAPI.Create(&vpcEndpoint) - require.NoError(t, err) - defer func() { - err = vpcEndpointAPI.Delete(acctID, vpcEndpoint.VPCEndpointID) - assert.NoError(t, err) - }() - thisEndpoint, err := vpcEndpointAPI.Read(acctID, vpcEndpoint.VPCEndpointID) - assert.NoError(t, err) - assert.Equal(t, "available", thisEndpoint.State) -} - func TestResourceVPCEndpointCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -95,7 +60,7 @@ func TestResourceVPCEndpointCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/accounts/abc/vpc-endpoints", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -148,7 +113,7 @@ func TestResourceVPCEndpointRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/vpc-endpoints/veid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -168,7 +133,7 @@ func TestResourceVPCEndpoint_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/vpc-endpoints/veid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -203,7 +168,7 @@ func TestResourceVPCEndpointDelete(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/vpc-endpoints/veid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Yes, it's not found", }, @@ -224,7 +189,7 @@ func TestResourceVPCEndpointDelete_Error(t *testing.T) { { Method: "DELETE", Resource: "/api/2.0/accounts/abc/vpc-endpoints/veid", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/mws/resource_mws_workspaces.go b/mws/resource_mws_workspaces.go index aa6208c612..b743029a20 100644 --- a/mws/resource_mws_workspaces.go +++ b/mws/resource_mws_workspaces.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "log" "net" @@ -11,6 +12,7 @@ import ( "strings" "time" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/tokens" @@ -158,7 +160,7 @@ func (a WorkspacesAPI) Create(ws *Workspace, timeout time.Duration) error { // generateWorkspaceHostname computes the hostname for the specified workspace, // given the account console hostname. func generateWorkspaceHostname(client *common.DatabricksClient, ws Workspace) string { - u, err := url.Parse(client.Host) + u, err := url.Parse(client.Config.Host) if err != nil { // Fallback. log.Printf("[WARN] Unable to parse URL from client host: %v", err) @@ -190,7 +192,8 @@ func (a WorkspacesAPI) verifyWorkspaceReachable(ws Workspace) *resource.RetryErr // make a request to Tokens API, just to verify there are no errors var response map[string]any err = wsClient.Get(ctx, "/token/list", nil, &response) - if apiError, ok := err.(common.APIError); ok { + var apiError *apierr.APIError + if errors.As(err, &apiError) { err = fmt.Errorf("workspace %s is not yet reachable: %s", ws.WorkspaceURL, apiError) log.Printf("[INFO] %s", err) @@ -305,7 +308,7 @@ func (a WorkspacesAPI) Delete(mwsAcctID, workspaceID string) error { } return resource.RetryContext(a.context, 15*time.Minute, func() *resource.RetryError { workspace, err := a.Read(mwsAcctID, workspaceID) - if common.IsMissing(err) { + if apierr.IsMissing(err) { log.Printf("[INFO] Workspace %s/%s is removed.", mwsAcctID, workspaceID) return nil } @@ -375,7 +378,7 @@ func EnsureTokenExistsIfNeeded(a WorkspacesAPI, } tokensAPI := tokens.NewTokensAPI(a.context, client) _, err = tokensAPI.Read(wsToken.Token.TokenID) - if common.IsMissing(err) { + if apierr.IsMissing(err) { return CreateTokenIfNeeded(a, workspaceSchema, d) } if err != nil { diff --git a/mws/resource_mws_workspaces_test.go b/mws/resource_mws_workspaces_test.go index bf86b1cc38..7c23a174de 100644 --- a/mws/resource_mws_workspaces_test.go +++ b/mws/resource_mws_workspaces_test.go @@ -2,11 +2,12 @@ package mws import ( "context" - "fmt" - "strings" "testing" "time" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/tokens" @@ -15,47 +16,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestMwsAccWorkspace(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode.") - } - cloudEnv := qa.GetEnvOrSkipTest(t, "CLOUD_ENV") - if strings.Contains(cloudEnv, "azure") { - t.Skip("cannot run Account Workspace tests in azure") - } - acctID := qa.GetEnvOrSkipTest(t, "DATABRICKS_ACCOUNT_ID") - client := common.CommonEnvironmentClient() - workspaceList, err := NewWorkspacesAPI(context.Background(), client).List(acctID) - assert.NoError(t, err) - t.Log(workspaceList) -} - -func TestGcpaAccWorkspace(t *testing.T) { - acctID := qa.GetEnvOrSkipTest(t, "DATABRICKS_ACCOUNT_ID") - client := common.CommonEnvironmentClient() - workspacesAPI := NewWorkspacesAPI(context.Background(), client) - - workspaceList, err := workspacesAPI.List(acctID) - require.NoError(t, err) - t.Log(workspaceList) - - ws := Workspace{ - AccountID: acctID, - WorkspaceName: qa.RandomName(qa.GetEnvOrSkipTest(t, "TEST_PREFIX") + "-"), - Location: qa.GetEnvOrSkipTest(t, "GOOGLE_REGION"), - CloudResourceBucket: &CloudResourceContainer{ - GCP: &GCP{ - ProjectID: qa.GetEnvOrSkipTest(t, "GOOGLE_PROJECT"), - }, - }, - } - err = workspacesAPI.Create(&ws, 5*time.Minute) - require.NoError(t, err) - - err = workspacesAPI.Delete(acctID, fmt.Sprintf("%d", ws.WorkspaceID)) - require.NoError(t, err) -} - func TestResourceWorkspaceCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -319,7 +279,7 @@ func TestResourceWorkspaceCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/accounts/abc/workspaces", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -328,7 +288,7 @@ func TestResourceWorkspaceCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/accounts/abc/workspaces", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -458,7 +418,7 @@ func TestResourceWorkspaceRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/workspaces/1234", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -478,7 +438,7 @@ func TestResourceWorkspaceRead_Error(t *testing.T) { { // read log output for correct url... Method: "GET", Resource: "/api/2.0/accounts/abc/workspaces/1234", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -660,7 +620,7 @@ func TestResourceWorkspaceUpdate_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/accounts/abc/workspaces/1234", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -705,7 +665,7 @@ func TestResourceWorkspaceDelete(t *testing.T) { { Method: "GET", Resource: "/api/2.0/accounts/abc/workspaces/1234", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Cannot find anything", }, @@ -726,7 +686,7 @@ func TestResourceWorkspaceDelete_Error(t *testing.T) { { Method: "DELETE", Resource: "/api/2.0/accounts/abc/workspaces/1234", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -938,7 +898,7 @@ func TestWorkspace_WaitForResolve(t *testing.T) { AccountID: "abc", WorkspaceID: 1234, WorkspaceStatus: "RUNNING", - WorkspaceURL: wsClient.Host, + WorkspaceURL: wsClient.Config.Host, }, }, }, func(ctx context.Context, client *common.DatabricksClient) { @@ -977,9 +937,9 @@ func updateWorkspaceTokenFixture(t *testing.T, fixtures []qa.HTTPFixture, state // a bit hacky, but the whole thing is more readable accountsAPI[1].Response = Workspace{ WorkspaceStatus: "RUNNING", - WorkspaceURL: wsClient.Host, + WorkspaceURL: wsClient.Config.Host, } - state["workspace_url"] = wsClient.Host + state["workspace_url"] = wsClient.Config.Host state["workspace_name"] = "b" state["account_id"] = "c" state["is_no_public_ip_enabled"] = "false" @@ -1093,7 +1053,7 @@ func TestEnsureTokenExists(t *testing.T) { }, func(ctx context.Context, client *common.DatabricksClient) { r := ResourceMwsWorkspaces() d := r.TestResourceData() - d.Set("workspace_url", client.Host) + d.Set("workspace_url", client.Config.Host) d.Set("token", []any{ map[string]any{ "lifetime_seconds": 3600, @@ -1123,7 +1083,7 @@ func TestEnsureTokenExists_NoRecreate(t *testing.T) { }, func(ctx context.Context, client *common.DatabricksClient) { r := ResourceMwsWorkspaces() d := r.TestResourceData() - d.Set("workspace_url", client.Host) + d.Set("workspace_url", client.Config.Host) d.Set("token", []any{ map[string]any{ "lifetime_seconds": 3600, @@ -1139,10 +1099,13 @@ func TestEnsureTokenExists_NoRecreate(t *testing.T) { func TestWorkspaceTokenWrongAuthCornerCase(t *testing.T) { defer common.CleanupEnvironment()() - client := &common.DatabricksClient{} + client, err := client.New(&config.Config{}) + if err != nil { + t.Fatal(err) + } r := ResourceMwsWorkspaces() d := r.TestResourceData() - d.Set("workspace_url", client.Host) + d.Set("workspace_url", client.Config.Host) d.Set("token", []any{ map[string]any{ "lifetime_seconds": 3600, @@ -1151,15 +1114,14 @@ func TestWorkspaceTokenWrongAuthCornerCase(t *testing.T) { }, }) - wsApi := NewWorkspacesAPI(context.Background(), client) + wsApi := NewWorkspacesAPI(context.Background(), &common.DatabricksClient{ + DatabricksClient: client, + }) - noAuth := "cannot authenticate parent client: authentication is not configured " + - "for provider. Please check https://registry.terraform.io/providers/" + - "databricks/databricks/latest/docs#authentication for details" + noAuth := "cannot authenticate parent client: default auth: cannot configure default credentials" assert.EqualError(t, CreateTokenIfNeeded(wsApi, r.Schema, d), noAuth, "create") assert.EqualError(t, EnsureTokenExistsIfNeeded(wsApi, r.Schema, d), noAuth, "ensure") assert.EqualError(t, removeTokenIfNeeded(wsApi, r.Schema, "x", d), noAuth, "remove") - } func TestWorkspaceTokenHttpCornerCases(t *testing.T) { @@ -1168,7 +1130,7 @@ func TestWorkspaceTokenHttpCornerCases(t *testing.T) { MatchAny: true, ReuseRequest: true, Status: 418, - Response: common.APIError{ + Response: apierr.APIError{ ErrorCode: "NONSENSE", StatusCode: 418, Message: "I'm a teapot", @@ -1178,7 +1140,7 @@ func TestWorkspaceTokenHttpCornerCases(t *testing.T) { wsApi := NewWorkspacesAPI(context.Background(), client) r := ResourceMwsWorkspaces() d := r.TestResourceData() - d.Set("workspace_url", client.Host) + d.Set("workspace_url", client.Config.Host) d.Set("token", []any{ map[string]any{ "lifetime_seconds": 3600, @@ -1199,13 +1161,21 @@ func TestWorkspaceTokenHttpCornerCases(t *testing.T) { func TestGenerateWorkspaceHostname_CornerCases(t *testing.T) { assert.Equal(t, "fallback.cloud.databricks.com", generateWorkspaceHostname(&common.DatabricksClient{ - Host: "$%^&*", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "$%^&*", + }, + }, }, Workspace{ DeploymentName: "fallback", })) assert.Equal(t, "stuff.is.exaple.com", generateWorkspaceHostname(&common.DatabricksClient{ - Host: "https://this.is.exaple.com", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://this.is.exaple.com", + }, + }, }, Workspace{ DeploymentName: "stuff", })) @@ -1217,7 +1187,7 @@ func TestExplainWorkspaceFailureCornerCase(t *testing.T) { MatchAny: true, ReuseRequest: true, Status: 418, - Response: common.APIError{ + Response: apierr.APIError{ ErrorCode: "NONSENSE", StatusCode: 418, Message: "🐜", diff --git a/permissions/acceptance/api_test.go b/permissions/acceptance/api_test.go deleted file mode 100644 index 233dbb276f..0000000000 --- a/permissions/acceptance/api_test.go +++ /dev/null @@ -1,323 +0,0 @@ -package acceptance - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/databricks/terraform-provider-databricks/clusters" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/internal/compute" - "github.com/databricks/terraform-provider-databricks/jobs" - "github.com/databricks/terraform-provider-databricks/permissions" - "github.com/databricks/terraform-provider-databricks/policies" - "github.com/databricks/terraform-provider-databricks/pools" - "github.com/databricks/terraform-provider-databricks/scim" - "github.com/databricks/terraform-provider-databricks/workspace" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func permissionsTestHelper(t *testing.T, - cb func(permissionsAPI permissions.PermissionsAPI, user, group string, - ef func(string) permissions.PermissionsEntity)) { - if os.Getenv("CLOUD_ENV") == "" { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - randomName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - client := common.NewClientFromEnvironment() - - ctx := context.Background() - usersAPI := scim.NewUsersAPI(ctx, client) - me, err := usersAPI.Me() - require.NoError(t, err) - - user, err := usersAPI.Create(scim.User{ - UserName: fmt.Sprintf("tf-%s@example.com", randomName), - }) - require.NoError(t, err) - defer func() { - assert.NoError(t, usersAPI.Delete(user.ID)) - }() - - groupsAPI := scim.NewGroupsAPI(ctx, client) - group, err := groupsAPI.Create(scim.Group{ - DisplayName: fmt.Sprintf("tf-%s", randomName), - Members: []scim.ComplexValue{ - { - Value: user.ID, - }, - }, - }) - require.NoError(t, err) - defer func() { - assert.NoError(t, groupsAPI.Delete(group.ID)) - }() - - permissionsAPI := permissions.NewPermissionsAPI(ctx, client) - cb(permissionsAPI, user.UserName, group.DisplayName, func(id string) permissions.PermissionsEntity { - d := permissions.ResourcePermissions().TestResourceData() - objectACL, err := permissionsAPI.Read(id) - require.NoError(t, err) - entity, err := objectACL.ToPermissionsEntity(d, me.UserName) - require.NoError(t, err) - return entity - }) -} - -func TestAccPermissionsClusterPolicy(t *testing.T) { - permissionsTestHelper(t, func(permissionsAPI permissions.PermissionsAPI, user, group string, - ef func(string) permissions.PermissionsEntity) { - policy := policies.ClusterPolicy{ - Name: group, - Definition: "{}", - } - ctx := context.Background() - client := common.NewClientFromEnvironment() - policiesAPI := policies.NewClusterPoliciesAPI(ctx, client) - require.NoError(t, policiesAPI.Create(&policy)) - defer func() { - assert.NoError(t, policiesAPI.Delete(policy.PolicyID)) - }() - - objectID := fmt.Sprintf("/cluster-policies/%s", policy.PolicyID) - require.NoError(t, permissionsAPI.Update(objectID, permissions.AccessControlChangeList{ - AccessControlList: []permissions.AccessControlChange{ - { - UserName: user, - PermissionLevel: "CAN_USE", - }, - { - GroupName: group, - PermissionLevel: "CAN_USE", - }, - }, - })) - entity := ef(objectID) - assert.Equal(t, "cluster-policy", entity.ObjectType) - assert.Len(t, entity.AccessControlList, 2) - - require.NoError(t, permissionsAPI.Delete(objectID)) - entity = ef(objectID) - assert.Len(t, entity.AccessControlList, 0) - }) -} - -func TestAccPermissionsInstancePool(t *testing.T) { - permissionsTestHelper(t, func(permissionsAPI permissions.PermissionsAPI, user, group string, - ef func(string) permissions.PermissionsEntity) { - client := common.NewClientFromEnvironment() - poolsAPI := pools.NewInstancePoolsAPI(context.Background(), client) - ctx := context.Background() - ips, err := poolsAPI.Create(pools.InstancePool{ - InstancePoolName: group, - NodeTypeID: clusters.NewClustersAPI(ctx, client).GetSmallestNodeType( - clusters.NodeTypeRequest{ - LocalDisk: true, - }), - }) - require.NoError(t, err) - defer func() { - assert.NoError(t, poolsAPI.Delete(ips.InstancePoolID)) - }() - - objectID := fmt.Sprintf("/instance-pools/%s", ips.InstancePoolID) - require.NoError(t, permissionsAPI.Update(objectID, permissions.AccessControlChangeList{ - AccessControlList: []permissions.AccessControlChange{ - { - UserName: user, - PermissionLevel: "CAN_MANAGE", - }, - { - GroupName: group, - PermissionLevel: "CAN_ATTACH_TO", - }, - }, - })) - entity := ef(objectID) - assert.Equal(t, "instance-pool", entity.ObjectType) - assert.Len(t, entity.AccessControlList, 2) - - require.NoError(t, permissionsAPI.Delete(objectID)) - entity = ef(objectID) - assert.Len(t, entity.AccessControlList, 0) - }) -} - -func TestAccPermissionsClusters(t *testing.T) { - permissionsTestHelper(t, func(permissionsAPI permissions.PermissionsAPI, user, group string, - ef func(string) permissions.PermissionsEntity) { - ctx := context.Background() - client := common.NewClientFromEnvironment() - clustersAPI := clusters.NewClustersAPI(ctx, client) - clusterInfo, err := compute.NewTinyClusterInCommonPool() - require.NoError(t, err) - defer func() { - assert.NoError(t, clustersAPI.PermanentDelete(clusterInfo.ClusterID)) - }() - - objectID := fmt.Sprintf("/clusters/%s", clusterInfo.ClusterID) - require.NoError(t, permissionsAPI.Update(objectID, permissions.AccessControlChangeList{ - AccessControlList: []permissions.AccessControlChange{ - { - UserName: user, - PermissionLevel: "CAN_RESTART", - }, - { - GroupName: group, - PermissionLevel: "CAN_ATTACH_TO", - }, - }, - })) - entity := ef(objectID) - assert.Equal(t, "cluster", entity.ObjectType) - assert.Len(t, entity.AccessControlList, 2) - - require.NoError(t, permissionsAPI.Delete(objectID)) - entity = ef(objectID) - assert.Len(t, entity.AccessControlList, 0) - }) -} - -func TestAccPermissionsTokens(t *testing.T) { - permissionsTestHelper(t, func(permissionsAPI permissions.PermissionsAPI, user, group string, - ef func(string) permissions.PermissionsEntity) { - objectID := "/authorization/tokens" - require.NoError(t, permissionsAPI.Update(objectID, permissions.AccessControlChangeList{ - AccessControlList: []permissions.AccessControlChange{ - { - UserName: user, - PermissionLevel: "CAN_USE", - }, - { - GroupName: group, - PermissionLevel: "CAN_USE", - }, - }, - })) - entity := ef(objectID) - assert.Equal(t, "tokens", entity.ObjectType) - assert.Len(t, entity.AccessControlList, 2) - - require.NoError(t, permissionsAPI.Delete(objectID)) - entity = ef(objectID) - assert.Len(t, entity.AccessControlList, 0) - }) -} - -func TestAccPermissionsJobs(t *testing.T) { - permissionsTestHelper(t, func(permissionsAPI permissions.PermissionsAPI, user, group string, - ef func(string) permissions.PermissionsEntity) { - ctx := context.Background() - client := common.NewClientFromEnvironment() - jobsAPI := jobs.NewJobsAPI(ctx, client) - job, err := jobsAPI.Create(jobs.JobSettings{ - NewCluster: &clusters.Cluster{ - NumWorkers: 2, - SparkVersion: "6.4.x-scala2.11", - NodeTypeID: clusters.NewClustersAPI(ctx, client).GetSmallestNodeType( - clusters.NodeTypeRequest{ - LocalDisk: true, - }), - }, - NotebookTask: &jobs.NotebookTask{ - NotebookPath: "/Production/Featurize", - }, - Name: group, - }) - require.NoError(t, err) - defer func() { - assert.NoError(t, jobsAPI.Delete(job.ID())) - }() - - objectID := fmt.Sprintf("/jobs/%s", job.ID()) - require.NoError(t, permissionsAPI.Update(objectID, permissions.AccessControlChangeList{ - AccessControlList: []permissions.AccessControlChange{ - { - UserName: user, - PermissionLevel: "IS_OWNER", - }, - { - GroupName: group, - PermissionLevel: "CAN_MANAGE_RUN", - }, - }, - })) - entity := ef(objectID) - assert.Equal(t, "job", entity.ObjectType) - assert.Len(t, entity.AccessControlList, 2) - - require.NoError(t, permissionsAPI.Delete(objectID)) - entity = ef(objectID) - assert.Len(t, entity.AccessControlList, 0) - }) -} - -func TestAccPermissionsNotebooks(t *testing.T) { - permissionsTestHelper(t, func(permissionsAPI permissions.PermissionsAPI, user, group string, - ef func(string) permissions.PermissionsEntity) { - client := common.NewClientFromEnvironment() - workspaceAPI := workspace.NewNotebooksAPI(context.Background(), client) - - notebookDir := fmt.Sprintf("/Testing/%s/something", group) - err := workspaceAPI.Mkdirs(notebookDir) - require.NoError(t, err) - - notebookPath := fmt.Sprintf("%s/Dummy", notebookDir) - - err = workspaceAPI.Create(workspace.ImportPath{ - Path: notebookPath, - Content: "MSsx", - Format: "SOURCE", - Language: "PYTHON", - Overwrite: true, - }) - require.NoError(t, err) - defer func() { - assert.NoError(t, workspaceAPI.Delete(notebookDir, true)) - }() - - folder, err := workspaceAPI.Read(fmt.Sprintf("/Testing/%s", group)) - require.NoError(t, err) - - directoryID := fmt.Sprintf("/directories/%d", folder.ObjectID) - require.NoError(t, permissionsAPI.Update(directoryID, permissions.AccessControlChangeList{ - AccessControlList: []permissions.AccessControlChange{ - { - GroupName: "users", - PermissionLevel: "CAN_READ", - }, - }, - })) - entity := ef(directoryID) - assert.Equal(t, "directory", entity.ObjectType) - assert.Len(t, entity.AccessControlList, 1) - - notebook, err := workspaceAPI.Read(notebookPath) - require.NoError(t, err) - notebookID := fmt.Sprintf("/notebooks/%d", notebook.ObjectID) - require.NoError(t, permissionsAPI.Update(notebookID, permissions.AccessControlChangeList{ - AccessControlList: []permissions.AccessControlChange{ - { - UserName: user, - PermissionLevel: "CAN_MANAGE", - }, - { - GroupName: group, - PermissionLevel: "CAN_EDIT", - }, - }, - })) - - entity = ef(notebookID) - assert.Equal(t, "notebook", entity.ObjectType) - assert.Len(t, entity.AccessControlList, 2) - - require.NoError(t, permissionsAPI.Delete(directoryID)) - entity = ef(directoryID) - assert.Len(t, entity.AccessControlList, 0) - }) -} diff --git a/permissions/acceptance/permissions_test.go b/permissions/acceptance/permissions_test.go index d7d710526e..8560e2d3b9 100644 --- a/permissions/acceptance/permissions_test.go +++ b/permissions/acceptance/permissions_test.go @@ -132,4 +132,4 @@ func TestAccDatabricksReposPermissionsResourceFullLifecycle(t *testing.T) { }, }, }) -} +} \ No newline at end of file diff --git a/permissions/resource_permissions.go b/permissions/resource_permissions.go index fb4be74680..f71bceb57e 100644 --- a/permissions/resource_permissions.go +++ b/permissions/resource_permissions.go @@ -9,12 +9,13 @@ import ( "strconv" "strings" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/jobs" "github.com/databricks/terraform-provider-databricks/pipelines" "github.com/databricks/terraform-provider-databricks/scim" - "github.com/databricks/terraform-provider-databricks/workspace" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -238,12 +239,12 @@ func (a PermissionsAPI) Delete(objectID string) error { // Read gets all relevant permissions for the object, including inherited ones func (a PermissionsAPI) Read(objectID string) (objectACL ObjectACL, err error) { err = a.client.Get(a.context, urlPathForObjectID(objectID), nil, &objectACL) - apiErr, ok := err.(common.APIError) + var apiErr *apierr.APIError // https://github.com/databricks/terraform-provider-databricks/issues/1227 // platform propagates INVALID_STATE error for auto-purged clusters in // the permissions api. this adds "a logical fix" also here, not to introduce // cross-package dependency on "clusters". - if ok && strings.Contains(apiErr.Message, "Cannot access cluster") && apiErr.StatusCode == 400 { + if errors.As(err, &apiErr) && strings.Contains(apiErr.Message, "Cannot access cluster") && apiErr.StatusCode == 400 { apiErr.StatusCode = 404 err = apiErr return @@ -257,20 +258,20 @@ type permissionsIDFieldMapping struct { allowedPermissionLevels []string - idRetriever func(ctx context.Context, client *common.DatabricksClient, id string) (string, error) + idRetriever func(ctx context.Context, w *databricks.WorkspaceClient, id string) (string, error) } // PermissionsResourceIDFields shows mapping of id columns to resource types func permissionsResourceIDFields() []permissionsIDFieldMapping { - SIMPLE := func(ctx context.Context, client *common.DatabricksClient, id string) (string, error) { + SIMPLE := func(ctx context.Context, w *databricks.WorkspaceClient, id string) (string, error) { return id, nil } - PATH := func(ctx context.Context, client *common.DatabricksClient, path string) (string, error) { - info, err := workspace.NewNotebooksAPI(ctx, client).Read(path) + PATH := func(ctx context.Context, w *databricks.WorkspaceClient, path string) (string, error) { + info, err := w.Workspace.GetStatusByPath(ctx, path) if err != nil { return "", fmt.Errorf("cannot load path %s: %s", path, err) } - return strconv.FormatInt(info.ObjectID, 10), nil + return strconv.FormatInt(info.ObjectId, 10), nil } return []permissionsIDFieldMapping{ {"cluster_policy_id", "cluster-policy", "cluster-policies", []string{"CAN_USE"}, SIMPLE}, @@ -382,11 +383,15 @@ func ResourcePermissions() *schema.Resource { Schema: s, CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, c any) error { client := c.(*common.DatabricksClient) - if client.Host == "" { + if client.DatabricksClient.Config.Host == "" { log.Printf("[WARN] cannot validate permission levels, because host is not known yet") return nil } - me, err := scim.NewUsersAPI(ctx, client).Me() + w, err := client.WorkspaceClient() + if err != nil { + return err + } + me, err := w.CurrentUser.Me(ctx) if err != nil { return err } @@ -411,11 +416,15 @@ func ResourcePermissions() *schema.Resource { }, Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { id := d.Id() + w, err := c.WorkspaceClient() + if err != nil { + return err + } objectACL, err := NewPermissionsAPI(ctx, c).Read(id) if err != nil { return err } - me, err := scim.NewUsersAPI(ctx, c).Me() + me, err := w.CurrentUser.Me(ctx) if err != nil { return err } @@ -435,7 +444,11 @@ func ResourcePermissions() *schema.Resource { common.DataToStructPointer(d, s, &entity) for _, mapping := range permissionsResourceIDFields() { if v, ok := d.GetOk(mapping.field); ok { - id, err := mapping.idRetriever(ctx, c, v.(string)) + w, err := c.WorkspaceClient() + if err != nil { + return err + } + id, err := mapping.idRetriever(ctx, w, v.(string)) if err != nil { return err } diff --git a/permissions/resource_permissions_test.go b/permissions/resource_permissions_test.go index 108b89f1c1..71fc5b3d27 100644 --- a/permissions/resource_permissions_test.go +++ b/permissions/resource_permissions_test.go @@ -2,9 +2,14 @@ package permissions import ( "context" + "fmt" "net/http" "testing" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/jobs" "github.com/databricks/terraform-provider-databricks/scim" @@ -107,7 +112,7 @@ func TestResourcePermissionsRead_RemovedCluster(t *testing.T) { Method: http.MethodGet, Resource: "/api/2.0/permissions/clusters/abc", Status: 400, - Response: common.APIError{ + Response: apierr.APIError{ ErrorCode: "INVALID_STATE", Message: "Cannot access cluster X that was terminated or unpinned more than Y days ago.", }, @@ -369,7 +374,7 @@ func TestResourcePermissionsRead_NotFound(t *testing.T) { { Method: http.MethodGet, Resource: "/api/2.0/permissions/clusters/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Cluster does not exist", }, @@ -391,7 +396,7 @@ func TestResourcePermissionsRead_some_error(t *testing.T) { { Method: http.MethodGet, Resource: "/api/2.0/permissions/clusters/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -439,7 +444,7 @@ func TestResourcePermissionsCustomizeDiff_ErrorOnScimMe(t *testing.T) { { Method: http.MethodGet, Resource: "/api/2.0/preview/scim/v2/Me", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -476,7 +481,7 @@ func TestResourcePermissionsRead_ErrorOnScimMe(t *testing.T) { { Method: http.MethodGet, Resource: "/api/2.0/preview/scim/v2/Me", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -630,7 +635,7 @@ func TestResourcePermissionsDelete_error(t *testing.T) { }, }, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -884,7 +889,7 @@ func TestResourcePermissionsCreate_NotebookPath_NotExists(t *testing.T) { { Method: http.MethodGet, Resource: "/api/2.0/workspace/get-status?path=%2FDevelopment%2FInit", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -982,13 +987,13 @@ func TestResourcePermissionsCreate_NotebookPath(t *testing.T) { } func TestResourcePermissionsCreate_error(t *testing.T) { - _, err := qa.ResourceFixture{ + qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ me, { Method: http.MethodPut, Resource: "/api/2.0/permissions/clusters/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -1006,12 +1011,7 @@ func TestResourcePermissionsCreate_error(t *testing.T) { }, }, Create: true, - }.Apply(t) - if assert.Error(t, err) { - if e, ok := err.(common.APIError); ok { - assert.Equal(t, "INVALID_REQUEST", e.ErrorCode) - } - } + }.ExpectError(t, "permission_level CAN_USE is not supported with cluster_id objects") } func TestResourcePermissionsCreate_PathIdRetriever_Error(t *testing.T) { @@ -1264,7 +1264,11 @@ func TestShouldKeepAdminsOnAnythingExceptPasswordsAndAssignsOwnerForPipeline(t * } func TestCustomizeDiffNoHostYet(t *testing.T) { - assert.Nil(t, ResourcePermissions().CustomizeDiff(context.TODO(), nil, &common.DatabricksClient{})) + assert.Nil(t, ResourcePermissions().CustomizeDiff(context.TODO(), nil, &common.DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{}, + }, + })) } func TestPathPermissionsResourceIDFields(t *testing.T) { @@ -1274,11 +1278,11 @@ func TestPathPermissionsResourceIDFields(t *testing.T) { m = x } } - _, err := m.idRetriever(context.Background(), &common.DatabricksClient{ - Host: "localhost", - Token: "x", - }, "x") - assert.EqualError(t, err, "cannot load path x: DatabricksClient is not configured") + _, err := m.idRetriever(context.Background(), databricks.Must(databricks.NewWorkspaceClient( + (*databricks.Config)(config.NewMockConfig(func(r *http.Request) error { + return fmt.Errorf("nope") + })))), "x") + assert.EqualError(t, err, "cannot load path x: nope") } func TestObjectACLToPermissionsEntityCornerCases(t *testing.T) { @@ -1307,7 +1311,7 @@ func TestDeleteMissing(t *testing.T) { { MatchAny: true, Status: 404, - Response: common.NotFound("missing"), + Response: apierr.NotFound("missing"), }, }, func(ctx context.Context, client *common.DatabricksClient) { p := ResourcePermissions() diff --git a/pipelines/resource_pipeline.go b/pipelines/resource_pipeline.go index 3441d6db10..86b5cd8dc2 100644 --- a/pipelines/resource_pipeline.go +++ b/pipelines/resource_pipeline.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/libraries" @@ -213,7 +214,7 @@ func (a PipelinesAPI) Delete(id string, timeout time.Duration) error { func() *resource.RetryError { i, err := a.Read(id) if err != nil { - if common.IsMissing(err) { + if apierr.IsMissing(err) { return nil } return resource.NonRetryableError(err) diff --git a/pipelines/resource_pipeline_test.go b/pipelines/resource_pipeline_test.go index f56132b6f3..692ebfc3ac 100644 --- a/pipelines/resource_pipeline_test.go +++ b/pipelines/resource_pipeline_test.go @@ -4,8 +4,7 @@ import ( "context" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -116,7 +115,7 @@ func TestResourcePipelineCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/pipelines", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -167,7 +166,7 @@ func TestResourcePipelineCreate_ErrorWhenWaitingFailedCleanup(t *testing.T) { { Method: "GET", Resource: "/api/2.0/pipelines/abcd", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INTERNAL_ERROR", Message: "Internal error", }, @@ -218,7 +217,7 @@ func TestResourcePipelineCreate_ErrorWhenWaitingSuccessfulCleanup(t *testing.T) { Method: "GET", Resource: "/api/2.0/pipelines/abcd", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "No such resource", }, @@ -274,7 +273,7 @@ func TestResourcePipelineRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/pipelines/abcd", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -294,7 +293,7 @@ func TestResourcePipelineRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/pipelines/abcd", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -382,7 +381,7 @@ func TestResourcePipelineUpdate_Error(t *testing.T) { { // read log output for better stub url... Method: "PUT", Resource: "/api/2.0/pipelines/abcd", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -489,7 +488,7 @@ func TestResourcePipelineDelete(t *testing.T) { { Method: "GET", Resource: "/api/2.0/pipelines/abcd", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "No such resource", }, @@ -510,7 +509,7 @@ func TestResourcePipelineDelete_Error(t *testing.T) { { Method: "DELETE", Resource: "/api/2.0/pipelines/abcd", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/policies/data_cluster_policy_test.go b/policies/data_cluster_policy_test.go index be23a83b43..20405e22c5 100644 --- a/policies/data_cluster_policy_test.go +++ b/policies/data_cluster_policy_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -46,7 +47,7 @@ func TestDataSourceClusterPolicyNotFound(t *testing.T) { Method: "GET", Resource: "/api/2.0/policies/clusters/list", Status: 404, - Response: common.APIError{ + Response: apierr.APIError{ Message: "searching_error", }, }, diff --git a/policies/resource_cluster_policy.go b/policies/resource_cluster_policy.go index 7b7840b9ea..f2e9f58eba 100644 --- a/policies/resource_cluster_policy.go +++ b/policies/resource_cluster_policy.go @@ -126,7 +126,7 @@ func ResourceClusterPolicy() *schema.Resource { Type: schema.TypeInt, Optional: true, Description: "Max number of clusters per user that can be active\n" + - "using this policy. If not present, there is no max limit.", + "using this policy. If not present, there is no max limit.", ValidateFunc: validation.IntAtLeast(1), }, }, diff --git a/policies/resource_cluster_policy_test.go b/policies/resource_cluster_policy_test.go index ba027bc8b7..7bb61c1a0e 100644 --- a/policies/resource_cluster_policy_test.go +++ b/policies/resource_cluster_policy_test.go @@ -3,8 +3,7 @@ package policies import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) @@ -42,7 +41,7 @@ func TestResourceClusterPolicyRead_NotFound(t *testing.T) { { // read log output for correct url... Method: "GET", Resource: "/api/2.0/policies/clusters/get?policy_id=abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -62,7 +61,7 @@ func TestResourceClusterPolicyRead_Error(t *testing.T) { { // read log output for correct url... Method: "GET", Resource: "/api/2.0/policies/clusters/get?policy_id=abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -124,7 +123,7 @@ func TestResourceClusterPolicyCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/policies/clusters/create", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -184,7 +183,7 @@ func TestResourceClusterPolicyUpdate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/policies/clusters/edit", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -228,7 +227,7 @@ func TestResourceClusterPolicyDelete_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/policies/clusters/delete", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/pools/data_instance_pool_test.go b/pools/data_instance_pool_test.go index 957292ce80..4907feaf43 100644 --- a/pools/data_instance_pool_test.go +++ b/pools/data_instance_pool_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -47,7 +48,7 @@ func TestDataSourceInstancePoolsGetPool(t *testing.T) { Method: "GET", Resource: "/api/2.0/instance-pools/list", Status: 404, - Response: common.APIError{ + Response: apierr.APIError{ Message: "searching_error", }, }, diff --git a/pools/resource_instance_pool_test.go b/pools/resource_instance_pool_test.go index cdb2791a0a..11f6a1807b 100644 --- a/pools/resource_instance_pool_test.go +++ b/pools/resource_instance_pool_test.go @@ -1,74 +1,14 @@ package pools import ( - "context" - "os" "testing" - "github.com/databricks/terraform-provider-databricks/clusters" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) -func TestAccInstancePools(t *testing.T) { - t.Parallel() - cloud := os.Getenv("CLOUD_ENV") - if cloud == "" { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - client := common.NewClientFromEnvironment() - - clustersAPI := clusters.NewClustersAPI(context.Background(), client) - sparkVersion := clustersAPI.LatestSparkVersionOrDefault(clusters.SparkVersionRequest{Latest: true, LongTermSupport: true}) - nodeType := clustersAPI.GetSmallestNodeType(clusters.NodeTypeRequest{}) - pool := InstancePool{ - InstancePoolName: "Terraform Integration Test", - MinIdleInstances: 0, - NodeTypeID: nodeType, - IdleInstanceAutoTerminationMinutes: 20, - PreloadedSparkVersions: []string{ - sparkVersion, - }, - } - if client.IsAws() { - pool.DiskSpec = &InstancePoolDiskSpec{ - DiskType: &InstancePoolDiskType{ - EbsVolumeType: clusters.EbsVolumeTypeGeneralPurposeSsd, - }, - DiskCount: 1, - DiskSize: 32, - } - pool.AwsAttributes = &InstancePoolAwsAttributes{ - Availability: clusters.AwsAvailabilitySpot, - } - } - poolInfo, err := NewInstancePoolsAPI(context.Background(), client).Create(pool) - assert.NoError(t, err) - - defer func() { - err := NewInstancePoolsAPI(context.Background(), client).Delete(poolInfo.InstancePoolID) - assert.NoError(t, err) - }() - - poolReadInfo, err := NewInstancePoolsAPI(context.Background(), client).Read(poolInfo.InstancePoolID) - assert.NoError(t, err) - assert.Equal(t, poolInfo.InstancePoolID, poolReadInfo.InstancePoolID) - assert.Equal(t, pool.InstancePoolName, poolReadInfo.InstancePoolName) - assert.Equal(t, pool.NodeTypeID, poolReadInfo.NodeTypeID) - assert.Equal(t, pool.IdleInstanceAutoTerminationMinutes, poolReadInfo.IdleInstanceAutoTerminationMinutes) - - poolReadInfo.InstancePoolName = "Terraform Integration Test Updated" - poolReadInfo.MaxCapacity = 20 - err = NewInstancePoolsAPI(context.Background(), client).Update(poolReadInfo) - assert.NoError(t, err) - - poolReadInfo, err = NewInstancePoolsAPI(context.Background(), client).Read(poolInfo.InstancePoolID) - assert.NoError(t, err) - assert.Equal(t, poolReadInfo.MaxCapacity, int32(20)) -} - func TestResourceInstancePoolCreate(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -121,7 +61,7 @@ func TestResourceInstancePoolCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/instance-pools/create", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -179,7 +119,7 @@ func TestResourceInstancePoolRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/instance-pools/get?instance_pool_id=abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -199,7 +139,7 @@ func TestResourceInstancePoolRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/instance-pools/get?instance_pool_id=abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -268,7 +208,7 @@ func TestResourceInstancePoolUpdate_Error(t *testing.T) { { // read log output for better stub url... Method: "POST", Resource: "/api/2.0/instance-pools/edit", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -316,7 +256,7 @@ func TestResourceInstancePoolDelete_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/instance-pools/delete", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/provider/generate_test.go b/provider/generate_test.go index 4bba20c1f1..5c095b5327 100644 --- a/provider/generate_test.go +++ b/provider/generate_test.go @@ -59,7 +59,7 @@ func (stub *resourceTestStub) Reads(t *testing.T) { { // read log output for correct url... Method: "GET", Resource: "/api/2.0/...", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -79,7 +79,7 @@ func (stub *resourceTestStub) Reads(t *testing.T) { { // read log output for correct url... Method: "GET", Resource: "/api/2.0/...", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -120,7 +120,7 @@ func (stub *resourceTestStub) Creates(t *testing.T) { { // read log output for better stub url... Method: "POST", Resource: "/api/2.0/...", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -165,7 +165,7 @@ func (stub *resourceTestStub) Updates(t *testing.T) { { // read log output for better stub url... Method: "POST", Resource: "/api/2.0/.../edit", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -212,7 +212,7 @@ func (stub *resourceTestStub) Deletes(t *testing.T) { { Method: "POST", Resource: "/api/2.0/.../delete", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/provider/provider.go b/provider/provider.go index ff461e6e1c..253a1db8e6 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -10,6 +10,10 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" + "github.com/databricks/databricks-sdk-go/useragent" + "github.com/databricks/terraform-provider-databricks/access" "github.com/databricks/terraform-provider-databricks/aws" "github.com/databricks/terraform-provider-databricks/catalog" @@ -32,6 +36,12 @@ import ( "github.com/databricks/terraform-provider-databricks/workspace" ) +func init() { + // IMPORTANT: this line cannot be changed, because it's used for + // internal purposes at Databricks. + useragent.WithProduct("databricks-tf-provider", common.Version()) +} + // DatabricksProvider returns the entire terraform provider object func DatabricksProvider() *schema.Provider { p := &schema.Provider{ @@ -143,7 +153,9 @@ func DatabricksProvider() *schema.Provider { Schema: providerSchema(), } p.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { - ctx = context.WithValue(ctx, common.Provider, p) + if p.TerraformVersion != "" { + useragent.WithUserAgentExtra("terraform", p.TerraformVersion) + } return configureDatabricksClient(ctx, d) } common.AddContextToAllResources(p, "databricks") @@ -158,8 +170,7 @@ func providerSchema() map[string]*schema.Schema { // other values will immediately fail unit tests } ps := map[string]*schema.Schema{} - attrs := common.ClientAttributes() // TODO: pass by argument - for _, attr := range attrs { + for _, attr := range config.ConfigAttributes { fieldSchema := &schema.Schema{ Type: kindMap[attr.Kind], Optional: true, @@ -170,24 +181,19 @@ func providerSchema() map[string]*schema.Schema { fieldSchema.DefaultFunc = schema.MultiEnvDefaultFunc(attr.EnvVars, nil) } } - ps["rate_limit"].DefaultFunc = schema.EnvDefaultFunc("DATABRICKS_RATE_LIMIT", - common.DefaultRateLimitPerSecond) - ps["debug_truncate_bytes"].DefaultFunc = schema.EnvDefaultFunc("DATABRICKS_DEBUG_TRUNCATE_BYTES", - common.DefaultTruncateBytes) + // TODO: check if still relevant + ps["rate_limit"].DefaultFunc = schema.EnvDefaultFunc("DATABRICKS_RATE_LIMIT", 15) + ps["debug_truncate_bytes"].DefaultFunc = schema.EnvDefaultFunc("DATABRICKS_DEBUG_TRUNCATE_BYTES", 96) return ps } func configureDatabricksClient(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { - prov := ctx.Value(common.Provider).(*schema.Provider) - pc := common.DatabricksClient{ - Provider: prov, - } + cfg := &config.Config{} attrsUsed := []string{} authsUsed := map[string]bool{} - attrs := common.ClientAttributes() // todo: pass by argument - for _, attr := range attrs { + for _, attr := range config.ConfigAttributes { if value, ok := d.GetOk(attr.Name); ok { - err := attr.Set(&pc, value) + err := attr.Set(cfg, value) if err != nil { return nil, diag.FromErr(err) } @@ -201,22 +207,15 @@ func configureDatabricksClient(ctx context.Context, d *schema.ResourceData) (any } sort.Strings(attrsUsed) log.Printf("[INFO] Explicit and implicit attributes: %s", strings.Join(attrsUsed, ", ")) - authorizationMethodsUsed := []string{} - for name, used := range authsUsed { - if used { - authorizationMethodsUsed = append(authorizationMethodsUsed, name) - } - } - if pc.AuthType == "" && len(authorizationMethodsUsed) > 1 { - sort.Strings(authorizationMethodsUsed) - return nil, diag.Errorf("More than one authorization method configured: %s", - strings.Join(authorizationMethodsUsed, " and ")) - } - if err := pc.Configure(attrsUsed...); err != nil { + client, err := client.New(cfg) + if err != nil { return nil, diag.FromErr(err) } + pc := &common.DatabricksClient{ + DatabricksClient: client, + } pc.WithCommandExecutor(func(ctx context.Context, client *common.DatabricksClient) common.CommandExecutor { return commands.NewCommandsAPI(ctx, client) }) - return &pc, nil + return pc, nil } diff --git a/provider/provider_test.go b/provider/provider_test.go index 92b48d0123..558fdd02ce 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -3,7 +3,7 @@ package provider import ( "context" "fmt" - "os" + "net/http" "path/filepath" "strings" "testing" @@ -84,13 +84,13 @@ func (tc providerFixture) apply(t *testing.T) { return } assert.Equal(t, tc.assertAzure, c.IsAzure()) - assert.Equal(t, tc.assertAuth, c.AuthType) - assert.Equal(t, tc.assertHost, c.Host) + assert.Equal(t, tc.assertAuth, c.Config.AuthType) + assert.Equal(t, tc.assertHost, c.Config.Host) } func TestConfig_NoParams(t *testing.T) { providerFixture{ - assertError: "authentication is not configured for provider", + assertError: "default auth: cannot configure default credentials", }.apply(t) } @@ -99,7 +99,7 @@ func TestConfig_HostEnv(t *testing.T) { env: map[string]string{ "DATABRICKS_HOST": "x", }, - assertError: "authentication is not configured for provider", + assertError: "default auth: cannot configure default credentials", }.apply(t) } @@ -108,7 +108,7 @@ func TestConfig_TokenEnv(t *testing.T) { env: map[string]string{ "DATABRICKS_TOKEN": "x", }, - assertError: "authentication is not configured for provider. Environment variables used: DATABRICKS_TOKEN", + assertError: "default auth: cannot configure default credentials. Config: token=***. Env: DATABRICKS_TOKEN", }.apply(t) } @@ -140,8 +140,8 @@ func TestConfig_UserPasswordEnv(t *testing.T) { "DATABRICKS_USERNAME": "x", "DATABRICKS_PASSWORD": "x", }, - assertError: "authentication is not configured for provider." + - " Environment variables used: DATABRICKS_USERNAME, DATABRICKS_PASSWORD", + assertError: "default auth: cannot configure default credentials. " + + "Config: username=x, password=***. Env: DATABRICKS_USERNAME, DATABRICKS_PASSWORD", assertHost: "https://x", }.apply(t) } @@ -212,7 +212,9 @@ func TestConfig_ConflictingEnvs(t *testing.T) { "DATABRICKS_USERNAME": "x", "DATABRICKS_PASSWORD": "x", }, - assertError: "More than one authorization method configured: password and token", + assertError: "validate: more than one authorization method configured: basic and pat. " + + "Config: host=x, token=***, username=x, password=***. " + + "Env: DATABRICKS_HOST, DATABRICKS_TOKEN, DATABRICKS_USERNAME, DATABRICKS_PASSWORD", }.apply(t) } @@ -235,7 +237,7 @@ func TestConfig_ConfigFile(t *testing.T) { env: map[string]string{ "CONFIG_FILE": "x", }, - assertError: "authentication is not configured for provider", + assertError: "default auth: cannot configure default credentials", }.apply(t) } @@ -246,7 +248,7 @@ func TestConfig_PatFromDatabricksCfg(t *testing.T) { "HOME": "../common/testdata", }, assertHost: "https://dbc-XXXXXXXX-YYYY.cloud.databricks.com", - assertAuth: "databricks-cli", + assertAuth: "pat", }.apply(t) } @@ -257,8 +259,8 @@ func TestConfig_PatFromDatabricksCfg_NohostProfile(t *testing.T) { "HOME": "../common/testdata", "DATABRICKS_CONFIG_PROFILE": "nohost", }, - assertError: "cannot configure databricks-cli auth: config " + - "file ../common/testdata/.databrickscfg is corrupt: cannot find host in nohost profile", + assertError: "default auth: cannot configure default credentials. " + + "Config: token=***, profile=nohost. Env: DATABRICKS_CONFIG_PROFILE", }.apply(t) } @@ -269,7 +271,8 @@ func TestConfig_ConfigProfileAndToken(t *testing.T) { "DATABRICKS_CONFIG_PROFILE": "nohost", "HOME": "../common/testdata", }, - assertError: "More than one authorization method configured: config profile and token", + assertError: "default auth: cannot configure default credentials. " + + "Config: token=***, profile=nohost. Env: DATABRICKS_TOKEN, DATABRICKS_CONFIG_PROFILE", }.apply(t) } @@ -280,7 +283,8 @@ func TestConfig_ConfigProfileAndPassword(t *testing.T) { "DATABRICKS_CONFIG_PROFILE": "nohost", "HOME": "../common/testdata", }, - assertError: "More than one authorization method configured: config profile and password", + assertError: "validate: more than one authorization method configured: basic and pat. " + + "Config: token=***, username=x, profile=nohost. Env: DATABRICKS_USERNAME, DATABRICKS_CONFIG_PROFILE", }.apply(t) } @@ -313,8 +317,7 @@ func TestConfig_AzureCliHost_Fail(t *testing.T) { "HOME": p, "FAIL": "yes", }, - assertError: "cannot configure azure-cli auth: Invoking Azure CLI " + - "failed with the following error: This is just a failing script.", + assertError: "default auth: azure-cli: cannot get access token: This is just a failing script.", }.apply(t) } @@ -326,7 +329,7 @@ func TestConfig_AzureCliHost_AzNotInstalled(t *testing.T) { "PATH": "whatever", "HOME": "../common/testdata", }, - assertError: "cannot configure azure-cli auth: most likely Azure CLI is not installed.", + assertError: "default auth: cannot configure default credentials.", }.apply(t) } @@ -340,7 +343,7 @@ func TestConfig_AzureCliHost_PatConflict(t *testing.T) { "PATH": p, "HOME": p, }, - assertError: "More than one authorization method configured: azure and token", + assertError: "validate: more than one authorization method configured: azure and pat.", }.apply(t) } @@ -372,7 +375,7 @@ func TestConfig_AzureAndPasswordConflict(t *testing.T) { "HOME": p, "DATABRICKS_USERNAME": "x", }, - assertError: "More than one authorization method configured: azure and password", + assertError: "validate: more than one authorization method configured: azure and basic.", }.apply(t) } @@ -381,15 +384,14 @@ func TestConfig_CorruptConfig(t *testing.T) { env: map[string]string{ "HOME": "../common/testdata/corrupt", }, - assertError: "cannot configure databricks-cli auth: " + - "../common/testdata/corrupt/.databrickscfg has no DEFAULT profile configured", + assertError: "default auth: cannot configure default credentials", }.apply(t) } func configureProviderAndReturnClient(t *testing.T, tt providerFixture) (*common.DatabricksClient, error) { defer common.CleanupEnvironment()() for k, v := range tt.env { - os.Setenv(k, v) + t.Setenv(k, v) } p := DatabricksProvider() ctx := context.Background() @@ -402,7 +404,8 @@ func configureProviderAndReturnClient(t *testing.T, tt providerFixture) (*common return nil, fmt.Errorf(strings.Join(issues, ", ")) } client := p.Meta().(*common.DatabricksClient) - err := client.Authenticate(ctx) + req, _ := http.NewRequest("GET", client.Config.Host, nil) + err := client.Config.Authenticate(req) if err != nil { return nil, err } diff --git a/qa/testing.go b/qa/testing.go index 98f1e9dc0b..43dd50ec0b 100644 --- a/qa/testing.go +++ b/qa/testing.go @@ -15,7 +15,9 @@ import ( "strings" "testing" - "github.com/Azure/go-autorest/autorest/azure" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/go-cty/cty" @@ -159,18 +161,18 @@ func (f ResourceFixture) Apply(t *testing.T) (*schema.ResourceData, error) { client.WithCommandMock(f.CommandMock) } if f.Azure { - client.AzureResourceID = "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c" + client.Config.AzureResourceID = "/subscriptions/a/resourceGroups/b/providers/Microsoft.Databricks/workspaces/c" } if f.AzureSPN { - client.AzureClientID = "a" - client.AzureClientSecret = "b" - client.AzureTenantID = "c" + client.Config.AzureClientID = "a" + client.Config.AzureClientSecret = "b" + client.Config.AzureTenantID = "c" } if f.Gcp { - client.GoogleServiceAccount = "sa@prj.iam.gserviceaccount.com" + client.Config.GoogleServiceAccount = "sa@prj.iam.gserviceaccount.com" } if f.AccountID != "" { - client.AccountID = f.AccountID + client.Config.AccountID = f.AccountID } if len(f.HCL) > 0 { var out any @@ -313,7 +315,7 @@ var HTTPFailures = []HTTPFixture{ MatchAny: true, ReuseRequest: true, Status: 418, - Response: common.APIError{ + Response: apierr.APIError{ ErrorCode: "NONSENSE", StatusCode: 418, Message: "I'm a teapot", @@ -342,7 +344,7 @@ func ResourceCornerCases(t *testing.T, resource *schema.Resource, cc ...CornerCa } HTTPFixturesApply(t, HTTPFailures, func(ctx context.Context, client *common.DatabricksClient) { validData := resource.TestResourceData() - client.AccountID = config["account_id"] + client.Config.AccountID = config["account_id"] for n, v := range m { if v == nil { continue @@ -405,8 +407,8 @@ func HttpFixtureClient(t *testing.T, fixtures []HTTPFixture) (client *common.Dat } // HttpFixtureClientWithToken creates client for emulated HTTP server -func HttpFixtureClientWithToken(t *testing.T, fixtures []HTTPFixture, token string) (client *common.DatabricksClient, server *httptest.Server, err error) { - server = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { +func HttpFixtureClientWithToken(t *testing.T, fixtures []HTTPFixture, token string) (*common.DatabricksClient, *httptest.Server, error) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { found := false for i, fixture := range fixtures { if (req.Method == fixture.Method && req.RequestURI == fixture.Resource) || fixture.MatchAny { @@ -425,7 +427,7 @@ func HttpFixtureClientWithToken(t *testing.T, fixtures []HTTPFixture, token stri } if fixture.Response != nil { if alreadyJSON, ok := fixture.Response.(string); ok { - _, err = rw.Write([]byte(alreadyJSON)) + _, err := rw.Write([]byte(alreadyJSON)) assert.NoError(t, err) } else { responseBytes, err := json.Marshal(fixture.Response) @@ -486,13 +488,18 @@ func HttpFixtureClientWithToken(t *testing.T, fixtures []HTTPFixture, token stri t.FailNow() } })) - client = &common.DatabricksClient{ + cfg := &config.Config{ Host: server.URL, Token: token, - AzureEnvironment: &azure.PublicCloud, + AzureEnvironment: "PUBLIC", } - err = client.Configure() - return client, server, err + c, err := client.New(cfg) + if err != nil { + return nil, nil, err + } + return &common.DatabricksClient{ + DatabricksClient: c, + }, server, nil } // HTTPFixturesApply is a helper method diff --git a/repos/resource_git_credential_test.go b/repos/resource_git_credential_test.go index d2dbc8f9fd..26dc492ce7 100644 --- a/repos/resource_git_credential_test.go +++ b/repos/resource_git_credential_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" ) @@ -40,7 +40,7 @@ func TestResourceGitCredentialRead_Error(t *testing.T) { { Method: http.MethodGet, Resource: fmt.Sprintf("/api/2.0/git-credentials/%d", credID), - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "Git credential with the given ID could not be found.", }, @@ -128,7 +128,7 @@ func TestResourceGitCredentialUpdate_Error(t *testing.T) { UserName: user, PAT: token, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "Git credential with the given ID could not be found.", }, @@ -202,7 +202,7 @@ func TestResourceGitCredentialCreate_Error(t *testing.T) { UserName: user, PAT: token, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_STATE", Message: "Only one Git credential is supported at this time. If you would like to update your credential, please use the PATCH endpoint.", }, @@ -238,7 +238,7 @@ func TestResourceGitCredentialCreateWithForce(t *testing.T) { UserName: user, PAT: token, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_STATE", Message: "Only one Git credential is supported at this time. If you would like to update your credential, please use the PATCH endpoint.", }, @@ -292,7 +292,7 @@ func TestResourceGitCredentialCreateWithForce_Error_List(t *testing.T) { UserName: user, PAT: token, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_STATE", Message: "Only one Git credential is supported at this time. If you would like to update your credential, please use the PATCH endpoint.", }, @@ -301,7 +301,7 @@ func TestResourceGitCredentialCreateWithForce_Error_List(t *testing.T) { { Method: http.MethodGet, Resource: "/api/2.0/git-credentials", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "No such endpoint", }, @@ -333,7 +333,7 @@ func TestResourceGitCredentialCreateWithForce_ErrorEmptyList(t *testing.T) { UserName: user, PAT: token, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_STATE", Message: "Only one Git credential is supported at this time. If you would like to update your credential, please use the PATCH endpoint.", }, @@ -375,7 +375,7 @@ func TestResourceGitCredentialCreateWithForce_ErrorUpdate(t *testing.T) { UserName: user, PAT: token, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_STATE", Message: "Only one Git credential is supported at this time. If you would like to update your credential, please use the PATCH endpoint.", }, @@ -391,7 +391,7 @@ func TestResourceGitCredentialCreateWithForce_ErrorUpdate(t *testing.T) { { Method: http.MethodPatch, Resource: fmt.Sprintf("/api/2.0/git-credentials/%d", resp.ID), - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "Git credential with the given ID could not be found.", }, diff --git a/repos/resource_repo_test.go b/repos/resource_repo_test.go index bedbdf4e73..7a66c2c357 100644 --- a/repos/resource_repo_test.go +++ b/repos/resource_repo_test.go @@ -9,8 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" ) @@ -61,7 +60,7 @@ func TestResourceRepoRead_NotFound(t *testing.T) { { Method: http.MethodGet, Resource: fmt.Sprintf("/api/2.0/repos/%s", repoID), - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "Repo could not be found", }, @@ -182,7 +181,7 @@ func TestResourceRepoCreateCustomDirectoryError(t *testing.T) { ExpectedRequest: map[string]string{ "path": "/Repos/Production", }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/scim/acceptance/user_test.go b/scim/acceptance/user_test.go index 8bb18453b9..8c66d11852 100644 --- a/scim/acceptance/user_test.go +++ b/scim/acceptance/user_test.go @@ -23,9 +23,8 @@ func TestAccForceUserImport(t *testing.T) { if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") } - t.Parallel() username := qa.RandomEmail() - os.Setenv("TEST_USERNAME", username) + t.Setenv("TEST_USERNAME", username) ctx := context.Background() client := common.CommonEnvironmentClient() usersAPI := scim.NewUsersAPI(ctx, client) diff --git a/scim/data_current_user.go b/scim/data_current_user.go index 30f4c61fcb..cd98c2bf05 100644 --- a/scim/data_current_user.go +++ b/scim/data_current_user.go @@ -55,7 +55,7 @@ func DataSourceCurrentUser() *schema.Resource { norm := nonAlphanumeric.ReplaceAllLiteralString(splits[0], "_") norm = strings.ToLower(norm) d.Set("alphanumeric", norm) - d.Set("workspace_url", usersAPI.client.Host) + d.Set("workspace_url", usersAPI.client.Config.Host) d.SetId(me.ID) return nil }, diff --git a/scim/data_user_test.go b/scim/data_user_test.go index b51ec76e0b..cee7ae47d0 100644 --- a/scim/data_user_test.go +++ b/scim/data_user_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -54,7 +55,7 @@ func TestDataSourceUserGerUser(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Users?filter=userName%20eq%20%27searching_error%27", Status: 404, - Response: common.APIError{ + Response: apierr.APIError{ Message: "searching_error", }, }, diff --git a/scim/groups_test.go b/scim/groups_test.go deleted file mode 100644 index c19c690ac3..0000000000 --- a/scim/groups_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package scim - -import ( - "context" - "os" - "testing" - - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/qa" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAccGroup(t *testing.T) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - client := common.NewClientFromEnvironment() - - ctx := context.Background() - usersAPI := NewUsersAPI(ctx, client) - groupsAPI := NewGroupsAPI(ctx, client) - - user, err := usersAPI.Create(User{UserName: qa.RandomEmail()}) - require.NoError(t, err) - defer usersAPI.Delete(user.ID) - - user2, err := usersAPI.Create(User{UserName: qa.RandomEmail()}) - require.NoError(t, err) - defer usersAPI.Delete(user2.ID) - - group, err := groupsAPI.Create(Group{ - DisplayName: qa.RandomName("tf-"), - }) - require.NoError(t, err) - defer groupsAPI.Delete(group.ID) - - group, err = groupsAPI.Read(group.ID) - require.NoError(t, err) - assert.True(t, len(group.Members) == 0) - - err = groupsAPI.Patch(group.ID, PatchRequest("add", "members", user.ID)) - assert.NoError(t, err) - - group, err = groupsAPI.Read(group.ID) - assert.NoError(t, err) - assert.True(t, len(group.Members) == 1) - - err = groupsAPI.Patch(group.ID, PatchRequest("add", "members", user2.ID)) - assert.NoError(t, err) - - group, err = groupsAPI.Read(group.ID) - assert.NoError(t, err) - assert.True(t, len(group.Members) == 2) -} - -func TestAccFilterGroup(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode.") - } - t.Parallel() - client := common.NewClientFromEnvironment() - ctx := context.Background() - groupsAPI := NewGroupsAPI(ctx, client) - groupList, err := groupsAPI.Filter("displayName eq admins") - assert.NoError(t, err) - assert.NotNil(t, groupList) - assert.Len(t, groupList.Resources, 1) -} diff --git a/scim/resource_entitlement_test.go b/scim/resource_entitlement_test.go index 5944e6f148..b7725ab429 100644 --- a/scim/resource_entitlement_test.go +++ b/scim/resource_entitlement_test.go @@ -3,7 +3,7 @@ package scim import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -156,7 +156,7 @@ func TestResourceEntitlementsGroupRead_Error(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", Status: 400, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ScimDetail: "Something", ScimStatus: "Else", }, @@ -375,7 +375,7 @@ func TestResourceEntitlementsUserRead_Error(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Users/abc", Status: 400, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ScimDetail: "Something", ScimStatus: "Else", }, @@ -396,7 +396,7 @@ func TestResourceEntitlementsUserUpdate_Error(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Users/abc", Status: 400, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ScimDetail: "Something", ScimStatus: "Else", }, @@ -406,7 +406,7 @@ func TestResourceEntitlementsUserUpdate_Error(t *testing.T) { Resource: "/api/2.0/preview/scim/v2/Users/abc", ExpectedRequest: updateRequest, Status: 400, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ScimDetail: "Something", ScimStatus: "Else", }, @@ -595,7 +595,7 @@ func TestResourceEntitlementsSPNRead_Error(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc", Status: 400, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ScimDetail: "Something", ScimStatus: "Else", }, diff --git a/scim/resource_group_member.go b/scim/resource_group_member.go index 4a301bbc98..55e3c45584 100644 --- a/scim/resource_group_member.go +++ b/scim/resource_group_member.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -19,7 +20,7 @@ func ResourceGroupMember() *schema.Resource { group, err := NewGroupsAPI(ctx, c).Read(groupID) hasMember := ComplexValues(group.Members).HasValue(memberID) if err == nil && !hasMember { - return common.NotFound("Group has no member") + return apierr.NotFound("Group has no member") } return err }, diff --git a/scim/resource_group_member_test.go b/scim/resource_group_member_test.go index b5aadfb0e7..3e4e2ff699 100644 --- a/scim/resource_group_member_test.go +++ b/scim/resource_group_member_test.go @@ -3,8 +3,7 @@ package scim import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) @@ -52,7 +51,7 @@ func TestResourceGroupMemberCreate_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -122,7 +121,7 @@ func TestResourceGroupMemberRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -142,7 +141,7 @@ func TestResourceGroupMemberRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -183,7 +182,7 @@ func TestResourceGroupMemberDelete_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/scim/resource_group_role.go b/scim/resource_group_role.go index 879f615347..46f215833b 100644 --- a/scim/resource_group_role.go +++ b/scim/resource_group_role.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -18,7 +19,7 @@ func ResourceGroupRole() *schema.Resource { group, err := NewGroupsAPI(ctx, c).Read(groupID) hasRole := ComplexValues(group.Roles).HasValue(role) if err == nil && !hasRole { - return common.NotFound("Group has no role") + return apierr.NotFound("Group has no role") } return err }, diff --git a/scim/resource_group_role_test.go b/scim/resource_group_role_test.go index f15f842628..d8f65ab278 100644 --- a/scim/resource_group_role_test.go +++ b/scim/resource_group_role_test.go @@ -3,8 +3,7 @@ package scim import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" ) @@ -49,7 +48,7 @@ func TestResourceGroupRoleCreate_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -115,7 +114,7 @@ func TestResourceGroupRoleRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -135,7 +134,7 @@ func TestResourceGroupRoleRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -172,7 +171,7 @@ func TestResourceGroupRoleDelete_Error(t *testing.T) { { Method: "PATCH", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/scim/resource_group_test.go b/scim/resource_group_test.go index eba0fcffd2..93ccae4699 100644 --- a/scim/resource_group_test.go +++ b/scim/resource_group_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" @@ -80,7 +81,7 @@ func TestResourceGroupCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/preview/scim/v2/Groups", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -162,7 +163,7 @@ func TestResourceGroupRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -182,7 +183,7 @@ func TestResourceGroupRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -298,7 +299,7 @@ func TestResourceGroupUpdate_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -336,7 +337,7 @@ func TestResourceGroupDelete_Error(t *testing.T) { { Method: "DELETE", Resource: "/api/2.0/preview/scim/v2/Groups/abc", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -355,7 +356,7 @@ func TestCreateForceOverwriteCannotListGroups(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Groups?filter=displayName%20eq%20%27abc%27", Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "cannot find group", }, }, diff --git a/scim/resource_service_principal_test.go b/scim/resource_service_principal_test.go index f4e6983889..5a7c3b474d 100644 --- a/scim/resource_service_principal_test.go +++ b/scim/resource_service_principal_test.go @@ -3,54 +3,15 @@ package scim import ( "context" "fmt" - "os" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestAccServicePrincipalOnAzure(t *testing.T) { - if cloud, ok := os.LookupEnv("CLOUD_ENV"); !ok || cloud != "azure" { - t.Skip("Test will only run with CLOUD_ENV=azure") - } - t.Parallel() - client := common.NewClientFromEnvironment() - ctx := context.Background() - - spAPI := NewServicePrincipalsAPI(ctx, client) - - sp, err := spAPI.Create(User{ - ApplicationID: "00000000-0000-0000-0000-000000000001", - Entitlements: entitlements{ - { - Value: "allow-cluster-create", - }, - }, - DisplayName: "ABC SP", - Active: true, - }) - require.NoError(t, err) - defer func() { - err := spAPI.Delete(sp.ID) - require.NoError(t, err) - }() - - err = spAPI.Update(sp.ID, User{ - ApplicationID: sp.ApplicationID, - Entitlements: entitlements{ - { - Value: "allow-instance-pool-create", - }, - }, - DisplayName: "BCD", - }) - require.NoError(t, err) -} - func TestResourceServicePrincipalRead(t *testing.T) { qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -109,7 +70,7 @@ func TestResourceServicePrincipalRead_Error(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc", Status: 400, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ScimDetail: "Something", ScimStatus: "Else", }, @@ -444,7 +405,7 @@ func TestCreateForceOverwriteCannotListServicePrincipals(t *testing.T) { Method: "GET", Resource: fmt.Sprintf("/api/2.0/preview/scim/v2/ServicePrincipals?filter=applicationId%%20eq%%20%%27%s%%27", appID), Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "cannot find service principal", }, }, diff --git a/scim/resource_user_test.go b/scim/resource_user_test.go index 3b4b33ec72..e65ac869dd 100644 --- a/scim/resource_user_test.go +++ b/scim/resource_user_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" @@ -73,7 +74,7 @@ func TestResourceUserRead_Error(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Users/abc", Status: 400, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ScimDetail: "Something", ScimStatus: "Else", }, @@ -455,7 +456,7 @@ func TestCreateForceOverwriteCannotListUsers(t *testing.T) { Method: "GET", Resource: "/api/2.0/preview/scim/v2/Users?filter=userName%20eq%20%27me%40example.com%27", Status: 417, - Response: common.APIError{ + Response: apierr.APIError{ Message: "cannot find user", }, }, diff --git a/scim/users_test.go b/scim/users_test.go index cf32240711..47f63c457f 100644 --- a/scim/users_test.go +++ b/scim/users_test.go @@ -2,62 +2,14 @@ package scim import ( "context" - "os" - "strings" "testing" - "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestAccReadUser(t *testing.T) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - client := common.NewClientFromEnvironment() - ctx := context.Background() - usersAPI := NewUsersAPI(ctx, client) - me, err := usersAPI.Me() - assert.NoError(t, err) - - if strings.Contains(me.UserName, "@") { - // let's assume that service principals do not look like emails - ru, err := usersAPI.Read(me.ID) - assert.NoError(t, err) - assert.NotNil(t, ru) - } -} - -func TestAccCreateUser(t *testing.T) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - client := common.NewClientFromEnvironment() - ctx := context.Background() - usersAPI := NewUsersAPI(ctx, client) - user, err := usersAPI.Create(User{ - UserName: qa.RandomEmail(), - }) - assert.NoError(t, err) - require.True(t, len(user.ID) > 0, "User id is empty") - defer usersAPI.Delete(user.ID) - - user, err = usersAPI.Read(user.ID) - t.Log(user) - assert.NoError(t, err) - - err = usersAPI.Update(user.ID, User{ - UserName: qa.RandomEmail(), - DisplayName: "TestAccCreateUser", - }) - assert.NoError(t, err) -} - func TestUsersFilter(t *testing.T) { client, server, err := qa.HttpFixtureClient(t, []qa.HTTPFixture{ { diff --git a/secrets/acceptance/secret_acl_test.go b/secrets/acceptance/secret_acl_test.go index e999d7561d..68f8f07465 100644 --- a/secrets/acceptance/secret_acl_test.go +++ b/secrets/acceptance/secret_acl_test.go @@ -5,7 +5,7 @@ import ( "os" "testing" - "github.com/databricks/terraform-provider-databricks/scim" + "github.com/databricks/databricks-sdk-go" "github.com/databricks/terraform-provider-databricks/secrets" "github.com/databricks/terraform-provider-databricks/common" @@ -37,20 +37,18 @@ func TestAccSecretAclResource(t *testing.T) { scope = databricks_secret_scope.app.name }`), Check: func(s *terraform.State) error { - client := common.CommonEnvironmentClient() + w := databricks.Must(databricks.NewWorkspaceClient()) ctx := context.Background() - usersAPI := scim.NewUsersAPI(ctx, client) - me, err := usersAPI.Me() + me, err := w.CurrentUser.Me(ctx) require.NoError(t, err) - secretACLAPI := secrets.NewSecretAclsAPI(ctx, client) scope := s.RootModule().Resources["databricks_secret_scope.app"].Primary.ID - acls, err := secretACLAPI.List(scope) + acls, err := w.Secrets.ListAclsByScope(ctx, scope) require.NoError(t, err) - assert.Equal(t, 2, len(acls)) + assert.Equal(t, 2, len(acls.Items)) m := map[string]string{} - for _, acl := range acls { + for _, acl := range acls.Items { m[acl.Principal] = string(acl.Permission) } diff --git a/secrets/acceptance/secret_scope_test.go b/secrets/acceptance/secret_scope_test.go index 72866f004d..4ccc9bab03 100644 --- a/secrets/acceptance/secret_scope_test.go +++ b/secrets/acceptance/secret_scope_test.go @@ -24,7 +24,7 @@ func TestAzureAccKeyVaultSimple(t *testing.T) { DNSName := qa.GetEnvOrSkipTest(t, "TEST_KEY_VAULT_DNS_NAME") client := common.CommonEnvironmentClient() - if client.IsAzureClientSecretSet() { + if client.IsAzure() { t.Skip("AKV scopes don't work for SP auth yet") } scopesAPI := secrets.NewSecretScopesAPI(context.Background(), client) diff --git a/secrets/acceptance/secret_test.go b/secrets/acceptance/secret_test.go index e6a64003c1..1f19649fde 100644 --- a/secrets/acceptance/secret_test.go +++ b/secrets/acceptance/secret_test.go @@ -5,10 +5,10 @@ import ( "os" "testing" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/service/secrets" "github.com/databricks/terraform-provider-databricks/qa" - "github.com/databricks/terraform-provider-databricks/secrets" - "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/internal/acceptance" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -33,6 +33,9 @@ func TestAccSecretResource(t *testing.T) { key := qa.FirstKeyValue(t, config, "key") secret := qa.FirstKeyValue(t, config, "string_value") + ctx := context.Background() + w := databricks.Must(databricks.NewWorkspaceClient()) + acceptance.AccTest(t, resource.TestCase{ Steps: []resource.TestStep{ { @@ -40,8 +43,10 @@ func TestAccSecretResource(t *testing.T) { }, { PreConfig: func() { - client := common.CommonEnvironmentClient() - err := secrets.NewSecretsAPI(context.Background(), client).Delete(scope, key) + err := w.Secrets.DeleteSecret(ctx, secrets.DeleteSecret{ + Scope: scope, + Key: key, + }) assert.NoError(t, err) }, Config: config, diff --git a/secrets/resource_secret.go b/secrets/resource_secret.go index 15cdedaa6d..e0ee217cc6 100644 --- a/secrets/resource_secret.go +++ b/secrets/resource_secret.go @@ -3,8 +3,8 @@ package secrets import ( "context" "fmt" - "net/http" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -109,12 +109,8 @@ func (a SecretsAPI) Read(scope string, key string) (SecretMetadata, error) { return secret, nil } } - return secretMeta, common.APIError{ - ErrorCode: "NOT_FOUND", - Message: fmt.Sprintf("no secret Scope found with secret metadata scope name: %s and key: %s", scope, key), - Resource: "/api/2.0/secrets/scopes/list", - StatusCode: http.StatusNotFound, - } + return secretMeta, apierr.NotFound( + fmt.Sprintf("no secret Scope found with secret metadata scope name: %s and key: %s", scope, key)) } // ResourceSecret manages secrets diff --git a/secrets/resource_secret_acl_test.go b/secrets/resource_secret_acl_test.go index 089395f3d2..5c40d62c83 100644 --- a/secrets/resource_secret_acl_test.go +++ b/secrets/resource_secret_acl_test.go @@ -1,84 +1,13 @@ package secrets import ( - "context" - "os" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) -func TestSecretsScopesAclsIntegration(t *testing.T) { - cloud := os.Getenv("CLOUD_ENV") - if cloud == "" { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - client := common.NewClientFromEnvironment() - ctx := context.Background() - scopesAPI := NewSecretScopesAPI(ctx, client) - secretsAPI := NewSecretsAPI(ctx, client) - secretAclsAPI := NewSecretAclsAPI(ctx, client) - - testScope := "my-test-scope" - testKey := "my-test-key" - testSecret := "my-test-secret" - initialManagePrincipal := "" - // TODO: on random group - testPrincipal := "users" - - err := scopesAPI.Create(SecretScope{ - Name: testScope, - InitialManagePrincipal: initialManagePrincipal, - }) - assert.NoError(t, err) - - defer func() { - // Deleting scope deletes everything else - err := scopesAPI.Delete(testScope) - assert.NoError(t, err) - }() - - scopes, err := scopesAPI.List() - assert.NoError(t, err) - assert.True(t, len(scopes) >= 1, "Scopes are empty list") - - scope, err := scopesAPI.Read(testScope) - assert.NoError(t, err) - assert.Equal(t, testScope, scope.Name, "Scope lookup does not yield same scope") - - err = secretsAPI.Create(testSecret, testScope, testKey) - assert.NoError(t, err) - - secrets, err := secretsAPI.List(testScope) - assert.NoError(t, err) - assert.True(t, len(secrets) > 0, "Secrets are empty list") - - secret, err := secretsAPI.Read(testScope, testKey) - assert.NoError(t, err) - assert.Equal(t, testKey, secret.Key, "Secret lookup does not yield same key") - - err = secretAclsAPI.Create(testScope, testPrincipal, ACLPermissionManage) - assert.NoError(t, err) - - secretAcls, err := secretAclsAPI.List(testScope) - assert.NoError(t, err) - assert.True(t, len(secretAcls) > 0, "Secrets acls are empty list") - - secretACL, err := secretAclsAPI.Read(testScope, testPrincipal) - assert.NoError(t, err) - assert.Equal(t, testPrincipal, secretACL.Principal, "Secret lookup does not yield same key") - assert.Equal(t, ACLPermissionManage, secretACL.Permission, "Secret lookup does not yield same key") - - err = secretsAPI.Delete(testScope, testKey) - assert.NoError(t, err) - - err = secretAclsAPI.Delete(testScope, testPrincipal) - assert.NoError(t, err) -} - func TestResourceSecretACLRead(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -107,7 +36,7 @@ func TestResourceSecretACLRead_NotFound(t *testing.T) { { Method: "GET", Resource: "/api/2.0/secrets/acls/get?principal=something&scope=global", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -127,7 +56,7 @@ func TestResourceSecretACLRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/secrets/acls/get?principal=something&scope=global", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -212,7 +141,7 @@ func TestResourceSecretACLCreate_Error(t *testing.T) { { // read log output for better stub url... Method: "POST", Resource: "/api/2.0/secrets/acls/put", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -257,7 +186,7 @@ func TestResourceSecretACLDelete_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/secrets/acls/delete", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/secrets/resource_secret_scope.go b/secrets/resource_secret_scope.go index 8de741922b..c3906b2cd3 100644 --- a/secrets/resource_secret_scope.go +++ b/secrets/resource_secret_scope.go @@ -3,9 +3,9 @@ package secrets import ( "context" "fmt" - "net/http" "regexp" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -59,7 +59,8 @@ func (a SecretScopesAPI) Create(s SecretScope) error { BackendType: "DATABRICKS", } if s.KeyvaultMetadata != nil { - if err := a.client.Authenticate(a.context); err != nil { + err := a.client.Config.EnsureResolved() + if err != nil { return err } if !a.client.IsAzure() { @@ -98,12 +99,8 @@ func (a SecretScopesAPI) Read(scopeName string) (SecretScope, error) { return scope, nil } } - return secretScope, common.APIError{ - ErrorCode: "NOT_FOUND", - Message: fmt.Sprintf("no Secret Scope found with scope name %s", scopeName), - Resource: "/api/2.0/secrets/scopes/list", - StatusCode: http.StatusNotFound, - } + return secretScope, apierr.NotFound( + fmt.Sprintf("no Secret Scope found with scope name %s", scopeName)) } var validScope = validation.StringMatch(regexp.MustCompile(`^[\w\.@_/-]{1,128}$`), diff --git a/secrets/resource_secret_scope_test.go b/secrets/resource_secret_scope_test.go index 1bde4b3da1..48e74f7f29 100644 --- a/secrets/resource_secret_scope_test.go +++ b/secrets/resource_secret_scope_test.go @@ -4,8 +4,7 @@ import ( "net/http" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -104,7 +103,7 @@ func TestResourceSecretScopeRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/secrets/scopes/list", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -244,7 +243,7 @@ func TestResourceSecretScopeCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/secrets/scopes/create", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -287,7 +286,7 @@ func TestResourceSecretScopeDelete_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/secrets/scopes/delete", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/secrets/resource_secret_test.go b/secrets/resource_secret_test.go index 80b17c2bf1..549f1905a1 100644 --- a/secrets/resource_secret_test.go +++ b/secrets/resource_secret_test.go @@ -3,8 +3,7 @@ package secrets import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) @@ -67,7 +66,7 @@ func TestResourceSecretRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/secrets/list?scope=foo", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -125,7 +124,7 @@ func TestResourceSecretCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/secrets/put", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -170,7 +169,7 @@ func TestResourceSecretDelete_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/secrets/delete", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/sql/resource_sql_endpoint_test.go b/sql/resource_sql_endpoint_test.go index 78e75cd7bd..7632201e22 100644 --- a/sql/resource_sql_endpoint_test.go +++ b/sql/resource_sql_endpoint_test.go @@ -3,10 +3,10 @@ package sql import ( "context" "encoding/json" - "os" "testing" "time" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" @@ -14,38 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestAccSQLEndpoints(t *testing.T) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - ctx := context.Background() - client := common.NewClientFromEnvironment() - endpoitsAPI := NewSQLEndpointsAPI(ctx, client) - se := SQLEndpoint{ - Name: qa.RandomName("tf-"), - ClusterSize: "Small", - AutoStopMinutes: 10, - MaxNumClusters: 1, - Tags: &Tags{ - CustomTags: []Tag{ - {"Country", "Netherlands"}, - {"City", "Amsterdam"}, - }, - }, - } - err := endpoitsAPI.Create(&se, 20*time.Minute) - require.NoError(t, err) - defer func() { - err = endpoitsAPI.Delete(se.ID) - assert.NoError(t, err) - }() - - se.Name = "renamed-" + se.Name - err = endpoitsAPI.Edit(se) - require.NoError(t, err) -} - // Define fixture for retrieving all data sources. // Shared between tests that end up performing a read operation. var dataSourceListHTTPFixture = qa.HTTPFixture{ @@ -166,7 +134,7 @@ func TestResourceSQLEndpointCreate_ErrorDisabled(t *testing.T) { Method: "POST", Resource: "/api/2.0/sql/warehouses", Status: 404, - Response: common.APIError{ + Response: apierr.APIError{ ErrorCode: "FEATURE_DISABLED", Message: "Databricks SQL is not supported", }, @@ -291,7 +259,7 @@ func TestSQLEnpointAPI(t *testing.T) { Method: "POST", Resource: "/api/2.0/sql/warehouses/failstart/start", Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, { Method: "POST", @@ -315,7 +283,7 @@ func TestSQLEnpointAPI(t *testing.T) { Method: "GET", Resource: "/api/2.0/sql/warehouses/cantwait", Status: 500, - Response: common.APIError{ + Response: apierr.APIError{ Message: "does not compute", }, }, diff --git a/sql/resource_sql_global_config.go b/sql/resource_sql_global_config.go index 3e059bf09d..4d3f946add 100644 --- a/sql/resource_sql_global_config.go +++ b/sql/resource_sql_global_config.go @@ -54,8 +54,8 @@ func (a globalConfigAPI) Set(gc GlobalConfig) error { "security_policy": gc.SecurityPolicy, "enable_serverless_compute": gc.EnableServerlessCompute, } - if a.client.Host == "" { - err := a.client.Authenticate(a.context) + if a.client.Config.Host == "" { + err := a.client.Config.EnsureResolved() if err != nil { return err } diff --git a/sql/resource_sql_visualization.go b/sql/resource_sql_visualization.go index ac4a3d3642..64cc7bca59 100644 --- a/sql/resource_sql_visualization.go +++ b/sql/resource_sql_visualization.go @@ -6,9 +6,9 @@ import ( "encoding/json" "fmt" "log" - "net/http" "strings" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/sql/api" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -101,11 +101,8 @@ func (a VisualizationAPI) Read(queryID, visualizationID string) (*api.Visualizat } } - return nil, common.APIError{ - ErrorCode: "NOT_FOUND", - StatusCode: http.StatusNotFound, - Message: fmt.Sprintf("Cannot find visualization %s attached to query %s", visualizationID, queryID), - } + return nil, apierr.NotFound( + fmt.Sprintf("Cannot find visualization %s attached to query %s", visualizationID, queryID)) } // Update ... diff --git a/sql/resource_sql_widget.go b/sql/resource_sql_widget.go index f5414b891d..782866c5a7 100644 --- a/sql/resource_sql_widget.go +++ b/sql/resource_sql_widget.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" "fmt" - "net/http" "sort" "strings" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/sql/api" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -237,11 +237,8 @@ func (a WidgetAPI) Read(dashboardID, widgetID string) (*api.Widget, error) { } } - return nil, common.APIError{ - ErrorCode: "NOT_FOUND", - StatusCode: http.StatusNotFound, - Message: fmt.Sprintf("Cannot find widget %s attached to dashboard %s", widgetID, dashboardID), - } + return nil, apierr.NotFound( + fmt.Sprintf("Cannot find widget %s attached to dashboard %s", widgetID, dashboardID)) } // Update ... diff --git a/storage/acceptance/aws_s3_mount_test.go b/storage/acceptance/aws_s3_mount_test.go deleted file mode 100644 index e9357f6b62..0000000000 --- a/storage/acceptance/aws_s3_mount_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package acceptance - -import ( - "context" - "fmt" - "testing" - - "github.com/databricks/terraform-provider-databricks/aws" - "github.com/databricks/terraform-provider-databricks/clusters" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/internal/acceptance" - "github.com/databricks/terraform-provider-databricks/internal/compute" - "github.com/databricks/terraform-provider-databricks/qa" - "github.com/databricks/terraform-provider-databricks/storage" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func getRunningClusterWithInstanceProfile(t *testing.T, client *common.DatabricksClient) (clusters.ClusterInfo, error) { - clusterName := "TerraformIntegrationTestIAM" - ctx := context.Background() - clustersAPI := clusters.NewClustersAPI(ctx, client) - instanceProfile := qa.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE") - return clustersAPI.GetOrCreateRunningCluster(clusterName, clusters.Cluster{ - NumWorkers: 1, - ClusterName: clusterName, - SparkVersion: clustersAPI.LatestSparkVersionOrDefault(clusters.SparkVersionRequest{Latest: true, LongTermSupport: true}), - InstancePoolID: compute.CommonInstancePoolID(), - AutoterminationMinutes: 10, - AwsAttributes: &clusters.AwsAttributes{ - InstanceProfileArn: instanceProfile, - }, - }) -} - -func TestAccAwsS3IamMount_WithCluster(t *testing.T) { - client := common.NewClientFromEnvironment() - arn := qa.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE") - ctx := context.WithValue(context.Background(), common.Current, t.Name()) - instanceProfilesAPI := aws.NewInstanceProfilesAPI(ctx, client) - instanceProfilesAPI.Synchronized(arn, func() bool { - if instanceProfilesAPI.IsRegistered(arn) { - return false - } - config := acceptance.EnvironmentTemplate(t, ` - resource "databricks_instance_profile" "this" { - instance_profile_arn = "{env.TEST_EC2_INSTANCE_PROFILE}" - } - data "databricks_spark_version" "latest" { - } - resource "databricks_cluster" "this" { - cluster_name = "ready-{var.RANDOM}" - spark_version = data.databricks_spark_version.latest.id - instance_pool_id = "{env.TEST_INSTANCE_POOL_ID}" - autotermination_minutes = 5 - num_workers = 1 - aws_attributes { - instance_profile_arn = databricks_instance_profile.this.id - } - } - resource "databricks_aws_s3_mount" "mount" { - cluster_id = databricks_cluster.this.id - mount_name = "{var.RANDOM}" - s3_bucket_name = "{env.TEST_S3_BUCKET}" - }`) - acceptance.AccTest(t, resource.TestCase{ - Steps: []resource.TestStep{ - { - Config: config, - Check: mountResourceCheck("databricks_aws_s3_mount.mount", - func(client *common.DatabricksClient, mp storage.MountPoint) error { - source, err := mp.Source() - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf("s3a://%s", - qa.FirstKeyValue(t, config, "s3_bucket_name")), source) - return nil - }), - }, - }, - }) - return true - }) -} - -func TestAccAwsS3IamMount_NoClusterGiven(t *testing.T) { - client := common.NewClientFromEnvironment() - arn := qa.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE") - ctx := context.WithValue(context.Background(), common.Current, t.Name()) - instanceProfilesAPI := aws.NewInstanceProfilesAPI(ctx, client) - instanceProfilesAPI.Synchronized(arn, func() bool { - config := acceptance.EnvironmentTemplate(t, ` - resource "databricks_instance_profile" "this" { - instance_profile_arn = "{env.TEST_EC2_INSTANCE_PROFILE}" - } - resource "databricks_aws_s3_mount" "mount" { - mount_name = "{var.RANDOM}" - s3_bucket_name = "{env.TEST_S3_BUCKET}" - instance_profile = databricks_instance_profile.this.id - }`) - acceptance.AccTest(t, resource.TestCase{ - Steps: []resource.TestStep{ - { - Config: config, - Check: mountResourceCheck("databricks_aws_s3_mount.mount", - func(client *common.DatabricksClient, mp storage.MountPoint) error { - source, err := mp.Source() - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf("s3a://%s", - qa.FirstKeyValue(t, config, "s3_bucket_name")), source) - return nil - }), - }, - { - PreConfig: func() { - client := compute.CommonEnvironmentClientWithRealCommandExecutor() - clusterInfo, err := getRunningClusterWithInstanceProfile(t, client) - assert.NoError(t, err) - - ctx := context.Background() - mp := storage.NewMountPoint(client.CommandExecutor(ctx), - qa.FirstKeyValue(t, config, "mount_name"), - clusterInfo.ClusterID) - err = mp.Delete() - assert.NoError(t, err) - }, - Config: config, - PlanOnly: true, - ExpectNonEmptyPlan: true, - }, - { - // Prior PreConfig deleted the mount so this one should attempt to recreate the mount - Config: config, - }, - }, - }) - return true - }) -} - -func TestAccAwsS3Mount(t *testing.T) { - client := common.NewClientFromEnvironment() - instanceProfile := qa.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE") - ctx := context.WithValue(context.Background(), common.Current, t.Name()) - instanceProfilesAPI := aws.NewInstanceProfilesAPI(ctx, client) - instanceProfilesAPI.Synchronized(instanceProfile, func() bool { - if err := instanceProfilesAPI.Create(aws.InstanceProfileInfo{ - InstanceProfileArn: instanceProfile, - }); err != nil { - return false - } - bucket := qa.GetEnvOrSkipTest(t, "TEST_S3_BUCKET") - client := compute.CommonEnvironmentClientWithRealCommandExecutor() - clustersAPI := clusters.NewClustersAPI(ctx, client) - clusterInfo, err := storage.GetOrCreateMountingClusterWithInstanceProfile( - clustersAPI, instanceProfile) - require.NoError(t, err) - defer func() { - err = clustersAPI.PermanentDelete(clusterInfo.ClusterID) - assert.NoError(t, err) - err = instanceProfilesAPI.Delete(instanceProfile) - assert.NoError(t, err) - }() - testMounting(t, storage.MountPoint{ - Exec: client.CommandExecutor(ctx), - ClusterID: clusterInfo.ClusterID, - Name: qa.RandomName("t"), - }, storage.AWSIamMount{ - S3BucketName: bucket, - }) - return true - }) -} diff --git a/storage/acceptance/azure_adls_gen2_mount_test.go b/storage/acceptance/azure_adls_gen2_mount_test.go deleted file mode 100644 index c5e748e0c5..0000000000 --- a/storage/acceptance/azure_adls_gen2_mount_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package acceptance - -import ( - "os" - "testing" - - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/internal/acceptance" - "github.com/databricks/terraform-provider-databricks/storage" - - "github.com/databricks/terraform-provider-databricks/qa" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" -) - -func TestAzureAccAdlsGen2Mount_correctly_mounts(t *testing.T) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - if !common.CommonEnvironmentClient().IsAzureClientSecretSet() { - t.Skip("Test is meant only for client-secret conf Azure") - } - acceptance.AccTest(t, resource.TestCase{ - Steps: []resource.TestStep{ - { - Config: acceptance.EnvironmentTemplate(t, ` - resource "databricks_secret_scope" "terraform" { - name = "terraform-{var.RANDOM}" - initial_manage_principal = "users" - } - resource "databricks_secret" "client_secret" { - key = "datalake_sp_secret" - string_value = "{env.ARM_CLIENT_SECRET}" - scope = databricks_secret_scope.terraform.name - } - resource "databricks_azure_adls_gen2_mount" "mount" { - storage_account_name = "{env.TEST_STORAGE_V2_ACCOUNT}" - container_name = "{env.TEST_STORAGE_V2_ABFSS}" - mount_name = "localdir{var.RANDOM}" - tenant_id = "{env.ARM_TENANT_ID}" - client_id = "{env.ARM_CLIENT_ID}" - client_secret_scope = databricks_secret_scope.terraform.name - client_secret_key = databricks_secret.client_secret.key - initialize_file_system = true - }`), - }, - }, - }) -} - -func TestAzureAccADLSv2Mount(t *testing.T) { - client, mp := mountPointThroughReusedCluster(t) - if !client.IsAzureClientSecretSet() { - t.Skip("Test is meant only for client-secret conf Azure") - } - storageAccountName := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_ACCOUNT") - container := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_ABFSS") - testWithNewSecretScope(t, func(scope, key string) { - testMounting(t, mp, storage.AzureADLSGen2Mount{ - ClientID: client.AzureClientID, - TenantID: client.AzureTenantID, - StorageAccountName: storageAccountName, - ContainerName: container, - SecretScope: scope, - SecretKey: key, - InitializeFileSystem: true, - Directory: "/", - }) - }, client, mp.Name, client.AzureClientSecret) -} diff --git a/storage/acceptance/azure_blob_mount_test.go b/storage/acceptance/azure_blob_mount_test.go deleted file mode 100644 index d5715ab413..0000000000 --- a/storage/acceptance/azure_blob_mount_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package acceptance - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/internal/acceptance" - "github.com/databricks/terraform-provider-databricks/internal/compute" - "github.com/databricks/terraform-provider-databricks/storage" - - "github.com/databricks/terraform-provider-databricks/qa" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/stretchr/testify/assert" -) - -func TestAzureAccBlobMount(t *testing.T) { - client, mp := mountPointThroughReusedCluster(t) - storageAccountName := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_ACCOUNT") - accountKey := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_KEY") - container := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_WASBS") - testWithNewSecretScope(t, func(scope, key string) { - testMounting(t, mp, storage.AzureBlobMount{ - StorageAccountName: storageAccountName, - ContainerName: container, - SecretScope: scope, - SecretKey: key, - Directory: "/", - }) - }, client, mp.Name, accountKey) -} - -func TestAzureAccBlobMount_correctly_mounts(t *testing.T) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - config := acceptance.EnvironmentTemplate(t, ` - resource "databricks_secret_scope" "terraform" { - name = "terraform-{var.RANDOM}" - initial_manage_principal = "users" - } - resource "databricks_secret" "storage_key" { - key = "blob_storage_key" - string_value = "{env.TEST_STORAGE_V2_KEY}" - scope = databricks_secret_scope.terraform.name - } - resource "databricks_azure_blob_mount" "mount" { - storage_account_name = "{env.TEST_STORAGE_V2_ACCOUNT}" - container_name = "{env.TEST_STORAGE_V2_WASBS}" - mount_name = "{var.RANDOM}" - auth_type = "ACCESS_KEY" - token_secret_scope = databricks_secret_scope.terraform.name - token_secret_key = databricks_secret.storage_key.key - }`) - acceptance.AccTest(t, resource.TestCase{ - Steps: []resource.TestStep{ - { - Config: config, - Check: mountResourceCheck("databricks_azure_blob_mount.mount", - func(client *common.DatabricksClient, mp storage.MountPoint) error { - source, err := mp.Source() - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf( - "wasbs://%s@%s.blob.core.windows.net/", - qa.FirstKeyValue(t, config, "container_name"), - qa.FirstKeyValue(t, config, "storage_account_name")), source) - return nil - }), - }, - { - PreConfig: func() { - client := compute.CommonEnvironmentClientWithRealCommandExecutor() - clusterInfo := compute.NewTinyClusterInCommonPoolPossiblyReused() - mp := storage.NewMountPoint(client.CommandExecutor(context.Background()), - qa.FirstKeyValue(t, config, "mount_name"), - clusterInfo.ClusterID) - err := mp.Delete() - assert.NoError(t, err) - }, - Config: config, - // Destroy: true, - }, - }, - }) -} diff --git a/storage/acceptance/common_test.go b/storage/acceptance/common_test.go deleted file mode 100644 index 05e57a3f7b..0000000000 --- a/storage/acceptance/common_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package acceptance - -import ( - "context" - "os" - "testing" - - "github.com/databricks/terraform-provider-databricks/commands" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/internal/acceptance" - "github.com/databricks/terraform-provider-databricks/internal/compute" - "github.com/databricks/terraform-provider-databricks/qa" - "github.com/databricks/terraform-provider-databricks/secrets" - "github.com/databricks/terraform-provider-databricks/storage" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mountResourceCheck(name string, - cb func(*common.DatabricksClient, storage.MountPoint) error) resource.TestCheckFunc { - return acceptance.ResourceCheck(name, - func(ctx context.Context, client *common.DatabricksClient, id string) error { - client.WithCommandExecutor(func(ctx context.Context, client *common.DatabricksClient) common.CommandExecutor { - return commands.NewCommandsAPI(ctx, client) - }) - clusterInfo := compute.NewTinyClusterInCommonPoolPossiblyReused() - mp := storage.NewMountPoint(client.CommandExecutor(context.Background()), id, clusterInfo.ClusterID) - return cb(client, mp) - }) -} - -func testMounting(t *testing.T, mp storage.MountPoint, m storage.Mount) { - client := common.CommonEnvironmentClient() - source, err := mp.Mount(m, client) - assert.Equal(t, m.Source(), source) - assert.NoError(t, err) - defer func() { - err = mp.Delete() - assert.NoError(t, err) - }() - source, err = mp.Source() - require.Equalf(t, m.Source(), source, "Error: %v", err) -} - -func mountPointThroughReusedCluster(t *testing.T) (*common.DatabricksClient, storage.MountPoint) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - ctx := context.Background() - client := common.CommonEnvironmentClient() - clusterInfo := compute.NewTinyClusterInCommonPoolPossiblyReused() - randomName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - return client, storage.MountPoint{ - Exec: client.CommandExecutor(ctx), - ClusterID: clusterInfo.ClusterID, - Name: randomName, - } -} - -func testWithNewSecretScope(t *testing.T, callback func(string, string), - client *common.DatabricksClient, suffix, secret string) { - randomScope := "test" + suffix - randomKey := "key" + suffix - - ctx := context.Background() - secretScopes := secrets.NewSecretScopesAPI(ctx, client) - err := secretScopes.Create(secrets.SecretScope{ - Name: randomScope, - InitialManagePrincipal: "users", - }) - require.NoError(t, err) - defer func() { - err = secretScopes.Delete(randomScope) - assert.NoError(t, err) - }() - - secrets := secrets.NewSecretsAPI(ctx, client) - err = secrets.Create(secret, randomScope, randomKey) - require.NoError(t, err) - - callback(randomScope, randomKey) -} - -func TestAccSourceOnInvalidMountFails(t *testing.T) { - _, mp := mountPointThroughReusedCluster(t) - source, err := mp.Source() - assert.Equal(t, "", source) - qa.AssertErrorStartsWith(t, err, "Mount not found") -} - -func TestAccInvalidSecretScopeFails(t *testing.T) { - _, mp := mountPointThroughReusedCluster(t) - client := common.CommonEnvironmentClient() - source, err := mp.Mount(storage.AzureADLSGen1Mount{ - ClientID: "abc", - TenantID: "bcd", - PrefixType: "dfs.adls", - StorageResource: "def", - Directory: "/", - SecretKey: "key", - SecretScope: "y", - }, client) - assert.Equal(t, "", source) - qa.AssertErrorStartsWith(t, err, "Secret does not exist with scope: y and key: key") -} diff --git a/storage/acceptance/mount_test.go b/storage/acceptance/mount_test.go deleted file mode 100644 index f613cb3dea..0000000000 --- a/storage/acceptance/mount_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package acceptance - -import ( - "context" - "testing" - - "github.com/databricks/terraform-provider-databricks/aws" - "github.com/databricks/terraform-provider-databricks/clusters" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/databricks/terraform-provider-databricks/internal/compute" - "github.com/databricks/terraform-provider-databricks/qa" - "github.com/databricks/terraform-provider-databricks/storage" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAccAwsS3MountGeneric(t *testing.T) { - client := common.NewClientFromEnvironment() - instanceProfile := qa.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE") - ctx := context.WithValue(context.Background(), common.Current, t.Name()) - instanceProfilesAPI := aws.NewInstanceProfilesAPI(ctx, client) - instanceProfilesAPI.Synchronized(instanceProfile, func() bool { - if err := instanceProfilesAPI.Create(aws.InstanceProfileInfo{ - InstanceProfileArn: instanceProfile, - }); err != nil { - return false - } - bucket := qa.GetEnvOrSkipTest(t, "TEST_S3_BUCKET") - client := compute.CommonEnvironmentClientWithRealCommandExecutor() - clustersAPI := clusters.NewClustersAPI(ctx, client) - clusterInfo, err := storage.GetOrCreateMountingClusterWithInstanceProfile( - clustersAPI, instanceProfile) - require.NoError(t, err) - defer func() { - err = clustersAPI.PermanentDelete(clusterInfo.ClusterID) - assert.NoError(t, err) - err = instanceProfilesAPI.Delete(instanceProfile) - assert.NoError(t, err) - }() - testMounting(t, storage.MountPoint{ - Exec: client.CommandExecutor(ctx), - ClusterID: clusterInfo.ClusterID, - Name: qa.RandomName("t"), - }, storage.GenericMount{ - S3: &storage.S3IamMount{ - BucketName: bucket, - }, - }) - return true - }) -} - -func TestAzureAccADLSv2MountGeneric(t *testing.T) { - client, mp := mountPointThroughReusedCluster(t) - if !client.IsAzureClientSecretSet() { - t.Skip("Test is meant only for client-secret conf Azure") - } - storageAccountName := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_ACCOUNT") - container := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_ABFSS") - testWithNewSecretScope(t, func(scope, key string) { - testMounting(t, mp, storage.GenericMount{ - Abfs: &storage.AzureADLSGen2MountGeneric{ - ClientID: client.AzureClientID, - TenantID: client.AzureTenantID, - StorageAccountName: storageAccountName, - ContainerName: container, - SecretScope: scope, - SecretKey: key, - InitializeFileSystem: true, - Directory: "/", - }, - }) - }, client, mp.Name, client.AzureClientSecret) -} - -func TestAccAzureBlobMountGeneric(t *testing.T) { - client, mp := mountPointThroughReusedCluster(t) - storageAccountName := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_ACCOUNT") - accountKey := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_KEY") - container := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_WASBS") - testWithNewSecretScope(t, func(scope, key string) { - testMounting(t, mp, storage.GenericMount{ - Wasb: &storage.AzureBlobMountGeneric{ - StorageAccountName: storageAccountName, - ContainerName: container, - SecretScope: scope, - SecretKey: key, - Directory: "/", - }}) - }, client, mp.Name, accountKey) -} - -// TODO: implement it -// func TestGcpAccGcsMount(t *testing.T) { -// client, mp := mountPointThroughReusedCluster(t) -// storageAccountName := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_ACCOUNT") -// accountKey := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_KEY") -// container := qa.GetEnvOrSkipTest(t, "TEST_STORAGE_V2_WASBS") -// testWithNewSecretScope(t, func(scope, key string) { -// testMounting(t, mp, GenericMount{Wasb: &AzureBlobMount{ -// StorageAccountName: storageAccountName, -// ContainerName: container, -// SecretScope: scope, -// SecretKey: key, -// Directory: "/", -// }}) -// }, client, mp.name, accountKey) -// } diff --git a/storage/adls_gen1_mount.go b/storage/adls_gen1_mount.go index 4fb4fb2c34..d94593ca2d 100644 --- a/storage/adls_gen1_mount.go +++ b/storage/adls_gen1_mount.go @@ -34,7 +34,11 @@ func (m AzureADLSGen1Mount) ValidateAndApplyDefaults(d *schema.ResourceData, cli // Config ... func (m AzureADLSGen1Mount) Config(client *common.DatabricksClient) map[string]string { - aadEndpoint := client.AzureEnvironment.ActiveDirectoryEndpoint + env, err := client.Config.GetAzureEnvironment() + if err != nil { + panic(err) // TODO: change interface + } + aadEndpoint := env.ActiveDirectoryEndpoint return map[string]string{ m.PrefixType + ".oauth2.access.token.provider.type": "ClientCredential", diff --git a/storage/adls_gen2_mount.go b/storage/adls_gen2_mount.go index d2f47fc028..51ca2fda72 100644 --- a/storage/adls_gen2_mount.go +++ b/storage/adls_gen2_mount.go @@ -35,7 +35,11 @@ func (m AzureADLSGen2Mount) ValidateAndApplyDefaults(d *schema.ResourceData, cli // Config returns mount configurations func (m AzureADLSGen2Mount) Config(client *common.DatabricksClient) map[string]string { - aadEndpoint := client.AzureEnvironment.ActiveDirectoryEndpoint + env, err := client.Config.GetAzureEnvironment() + if err != nil { + panic(err) // TODO: change interface + } + aadEndpoint := env.ActiveDirectoryEndpoint return map[string]string{ "fs.azure.account.auth.type": "OAuth", "fs.azure.account.oauth.provider.type": "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider", diff --git a/storage/dbfs_test.go b/storage/dbfs_test.go index fd48b0caa1..b55126363a 100644 --- a/storage/dbfs_test.go +++ b/storage/dbfs_test.go @@ -1,15 +1,12 @@ package storage import ( - "bytes" "context" - "crypto/md5" - "os" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/stretchr/testify/assert" ) @@ -23,7 +20,7 @@ func TestCreateFileFails(t *testing.T) { Overwrite: true, }, Status: 404, - Response: common.NotFound("fails"), + Response: apierr.NotFound("fails"), }, { Method: "POST", @@ -55,7 +52,7 @@ func TestCreateFile_AddBlockFails(t *testing.T) { Handle: 123, }, Status: 404, - Response: common.NotFound("fails"), + Response: apierr.NotFound("fails"), }, { Method: "POST", @@ -82,7 +79,7 @@ func TestCreateFile_CloseFails(t *testing.T) { Method: "POST", Resource: "/api/2.0/dbfs/close", Status: 404, - Response: common.NotFound("fails"), + Response: apierr.NotFound("fails"), }, }, func(ctx context.Context, client *common.DatabricksClient) { a := NewDbfsAPI(ctx, client) @@ -97,7 +94,7 @@ func TestDbfsListRecursiveFails(t *testing.T) { Method: "GET", Resource: "/api/2.0/dbfs/list?path=abc", Status: 404, - Response: common.NotFound("fails"), + Response: apierr.NotFound("fails"), }, { Method: "GET", @@ -116,7 +113,7 @@ func TestDbfsListRecursiveFails(t *testing.T) { ReuseRequest: true, Resource: "/api/2.0/dbfs/list?path=bcd", Status: 404, - Response: common.NotFound("fails"), + Response: apierr.NotFound("fails"), }, }, func(ctx context.Context, client *common.DatabricksClient) { a := NewDbfsAPI(ctx, client) @@ -134,7 +131,7 @@ func TestDbfsReadFails(t *testing.T) { { MatchAny: true, Status: 404, - Response: common.NotFound("fails"), + Response: apierr.NotFound("fails"), }, }, func(ctx context.Context, client *common.DatabricksClient) { a := NewDbfsAPI(ctx, client) @@ -142,54 +139,3 @@ func TestDbfsReadFails(t *testing.T) { assert.EqualError(t, err, "cannot read abc: fails") }) } - -func genString(times int) []byte { - var buf bytes.Buffer - for i := 0; i < times; i++ { - buf.WriteString("Hello world how are you doing?\n") - } - return buf.Bytes() -} - -func TestAccCreateFile(t *testing.T) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - randomName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - dir := "/client-test/" + randomName - path := dir + "/randomfile" - path2 := dir + "/dir2/randomfile" - path3 := dir + "/dir2/randomfile2" - - randomStr := genString(10) - client := common.NewClientFromEnvironment() - - dbfsAPI := NewDbfsAPI(context.Background(), client) - - err := dbfsAPI.Create(path, randomStr, true) - assert.NoError(t, err) - - err = dbfsAPI.Create(path2, randomStr, true) - assert.NoError(t, err) - - err = dbfsAPI.Create(path3, randomStr, true) - assert.NoError(t, err) - - defer func() { - err := dbfsAPI.Delete(dir, true) - assert.NoError(t, err) - }() - - resp, err := dbfsAPI.Read(path) - assert.NoError(t, err) - assert.True(t, md5.Sum(randomStr) == md5.Sum(resp)) - - items, err := dbfsAPI.List(dir, false) - assert.NoError(t, err) - assert.Len(t, items, 2) - - items, err = dbfsAPI.List(dir, true) - assert.NoError(t, err) - assert.Len(t, items, 3) -} diff --git a/storage/generic_mounts.go b/storage/generic_mounts.go index 5433b721e1..e7cb89abee 100644 --- a/storage/generic_mounts.go +++ b/storage/generic_mounts.go @@ -107,8 +107,8 @@ func getContainerDefaults(d *schema.ResourceData, allowed_schemas []string, suff } func getTenantID(client *common.DatabricksClient) (string, error) { - if client.AzureTenantID != "" { - return client.AzureTenantID, nil + if client.Config.AzureTenantID != "" { + return client.Config.AzureTenantID, nil } v, err := client.GetAzureJwtProperty("tid") if err != nil { @@ -170,7 +170,11 @@ func (m *AzureADLSGen2MountGeneric) ValidateAndApplyDefaults(d *schema.ResourceD // Config returns mount configurations func (m *AzureADLSGen2MountGeneric) Config(client *common.DatabricksClient) map[string]string { - aadEndpoint := client.AzureEnvironment.ActiveDirectoryEndpoint + env, err := client.Config.GetAzureEnvironment() + if err != nil { + panic(err) // TODO: change interface + } + aadEndpoint := env.ActiveDirectoryEndpoint return map[string]string{ "fs.azure.account.auth.type": "OAuth", "fs.azure.account.oauth.provider.type": "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider", @@ -235,7 +239,11 @@ func (m *AzureADLSGen1MountGeneric) ValidateAndApplyDefaults(d *schema.ResourceD // Config ... func (m *AzureADLSGen1MountGeneric) Config(client *common.DatabricksClient) map[string]string { - aadEndpoint := client.AzureEnvironment.ActiveDirectoryEndpoint + env, err := client.Config.GetAzureEnvironment() + if err != nil { + panic(err) // TODO: change interface + } + aadEndpoint := env.ActiveDirectoryEndpoint return map[string]string{ m.PrefixType + ".oauth2.access.token.provider.type": "ClientCredential", diff --git a/storage/gs.go b/storage/gs.go index 7916efa53c..2c47b6b2b3 100644 --- a/storage/gs.go +++ b/storage/gs.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -63,7 +64,7 @@ func createOrValidateClusterForGoogleStorage(ctx context.Context, m any, clustersAPI := clusters.NewClustersAPI(ctx, m) if clusterID != "" { clusterInfo, err := clustersAPI.Get(clusterID) - if common.IsMissing(err) { + if apierr.IsMissing(err) { cluster, err := GetOrCreateMountingClusterWithGcpServiceAccount(clustersAPI, serviceAccount) if err != nil { return fmt.Errorf("cannot re-create mounting cluster: %w", err) diff --git a/storage/gs_test.go b/storage/gs_test.go index edb79aedf1..c2440d4981 100644 --- a/storage/gs_test.go +++ b/storage/gs_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" @@ -16,7 +17,7 @@ func TestCreateOrValidateClusterForGoogleStorage_Failures(t *testing.T) { MatchAny: true, ReuseRequest: true, Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { d := ResourceMount().TestResourceData() @@ -34,7 +35,7 @@ func TestCreateOrValidateClusterForGoogleStorage_WorksOnDeletedCluster(t *testin Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=removed-cluster", Status: 404, - Response: common.NotFound("cluster deleted"), + Response: apierr.NotFound("cluster deleted"), }, { Method: "GET", @@ -101,7 +102,7 @@ func TestCreateOrValidateClusterForGoogleStorage_FailsOnErrorGettingCluster(t *t Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=my-cluster", Status: 500, - Response: common.APIError{ + Response: apierr.APIError{ ErrorCode: "SERVER_ERROR", StatusCode: 500, Message: "Server error", diff --git a/storage/mounts.go b/storage/mounts.go index d2e756ea74..7730caaf0c 100644 --- a/storage/mounts.go +++ b/storage/mounts.go @@ -9,6 +9,7 @@ import ( "regexp" "strings" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" @@ -170,7 +171,7 @@ func getMountingClusterID(ctx context.Context, client *common.DatabricksClient, return getOrCreateMountingCluster(clustersAPI) } clusterInfo, err := clustersAPI.Get(clusterID) - if common.IsMissing(err) { + if apierr.IsMissing(err) { return getOrCreateMountingCluster(clustersAPI) } if err != nil { diff --git a/storage/mounts_test.go b/storage/mounts_test.go index d9ab174938..a4032407af 100644 --- a/storage/mounts_test.go +++ b/storage/mounts_test.go @@ -5,6 +5,9 @@ import ( "fmt" "testing" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/commands" @@ -37,12 +40,13 @@ const expectedCommandResp = "done" func testMountFuncHelper(t *testing.T, mountFunc func(mp MountPoint, mount Mount) (string, error), mount Mount, mountName, expectedCommand string) { c := common.DatabricksClient{ - Host: ".", - Token: ".", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: ".", + Token: ".", + }, + }, } - err := c.Configure() - assert.NoError(t, err) - var called bool c.WithCommandMock(func(commandStr string) common.CommandResults { @@ -104,8 +108,12 @@ func TestMountPoint_Mount(t *testing.T) { `, mountName, expectedMountSource, expectedMountConfig) testMountFuncHelper(t, func(mp MountPoint, mount Mount) (s string, e error) { client := common.DatabricksClient{ - Host: ".", - Token: ".", + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: ".", + Token: ".", + }, + }, } return mp.Mount(mount, &client) }, mount, mountName, expectedCommand) @@ -279,7 +287,7 @@ func TestGetMountingClusterID_Failures(t *testing.T) { Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=def", Status: 518, - Response: common.APIError{ + Response: apierr.APIError{ Message: "😤", }, }, @@ -295,7 +303,7 @@ func TestGetMountingClusterID_Failures(t *testing.T) { MatchAny: true, ReuseRequest: true, Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { // no mounting cluster given, try creating it @@ -321,7 +329,7 @@ func TestMountCRD(t *testing.T) { MatchAny: true, ReuseRequest: true, Status: 404, - Response: common.NotFound("nope"), + Response: apierr.NotFound("nope"), }, }, func(ctx context.Context, client *common.DatabricksClient) { r := ResourceMount() diff --git a/storage/s3.go b/storage/s3.go index 2639af94ad..e6f18457c9 100644 --- a/storage/s3.go +++ b/storage/s3.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -62,7 +63,7 @@ func preprocessS3MountGeneric(ctx context.Context, s map[string]*schema.Schema, clustersAPI := clusters.NewClustersAPI(ctx, m) if clusterID != "" { clusterInfo, err := clustersAPI.Get(clusterID) - if common.IsMissing(err) { + if apierr.IsMissing(err) { if instanceProfile == "" { return fmt.Errorf("instance profile is required to re-create mounting cluster") } diff --git a/storage/s3_test.go b/storage/s3_test.go index 9db4fecefd..1fe7162098 100644 --- a/storage/s3_test.go +++ b/storage/s3_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/clusters" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/qa" @@ -16,7 +17,7 @@ func TestPreprocessS3MountOnDeletedClusterNoInstanceProfileSpecifiedError(t *tes Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=removed-cluster", Status: 404, - Response: common.NotFound("cluster deleted"), + Response: apierr.NotFound("cluster deleted"), }, }, func(ctx context.Context, client *common.DatabricksClient) { r := ResourceMount() @@ -34,7 +35,7 @@ func TestPreprocessS3MountOnDeletedClusterWorks(t *testing.T) { Method: "GET", Resource: "/api/2.0/clusters/get?cluster_id=removed-cluster", Status: 404, - Response: common.NotFound("cluster deleted"), + Response: apierr.NotFound("cluster deleted"), }, { Method: "GET", diff --git a/tokens/resource_obo_token_test.go b/tokens/resource_obo_token_test.go index 77e8c5815b..bbd6fbba3d 100644 --- a/tokens/resource_obo_token_test.go +++ b/tokens/resource_obo_token_test.go @@ -1,64 +1,14 @@ package tokens import ( - "context" "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" - "github.com/databricks/terraform-provider-databricks/scim" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestAccAwsOboFlow(t *testing.T) { - qa.RequireCloudEnv(t, "aws") - ctx := context.Background() - client := common.CommonEnvironmentClient() - tmAPI := NewTokenManagementAPI(ctx, client) - spAPI := scim.NewServicePrincipalsAPI(ctx, client) - groupsAPI := scim.NewGroupsAPI(ctx, client) - - sp, err := spAPI.Create(scim.User{ - DisplayName: qa.RandomName("tf"), - Active: true, - }) - require.NoError(t, err) - defer spAPI.Delete(sp.ID) - - admins, err := groupsAPI.ReadByDisplayName("admins") - require.NoError(t, err) - - // add new SP to admins, as it's the easiest way to give it permissions to do so, - // because importing access package will create circular import dependency - err = groupsAPI.Patch(admins.ID, scim.PatchRequest("add", "members", sp.ID)) - require.NoError(t, err) - - oboToken, err := tmAPI.CreateTokenOnBehalfOfServicePrincipal(OboToken{ - ApplicationID: sp.ApplicationID, - Comment: qa.RandomLongName(), - LifetimeSeconds: 60, - }) - require.NoError(t, err) - defer tmAPI.Delete(oboToken.TokenInfo.TokenID) - - spClient := &common.DatabricksClient{ - Host: client.Host, - Token: oboToken.TokenValue, - } - err = spClient.Configure() - require.NoError(t, err) - - newMe, err := scim.NewUsersAPI(ctx, spClient).Me() - require.NoError(t, err) - assert.Equal(t, newMe.DisplayName, sp.DisplayName) - - r, err := tmAPI.Read(oboToken.TokenInfo.TokenID) - require.NoError(t, err) - assert.Equal(t, r.TokenInfo.TokenID, oboToken.TokenInfo.TokenID) -} - func TestResourceOboTokenRead(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -90,7 +40,7 @@ func TestResourceOboTokenRead_Error(t *testing.T) { Method: "GET", Resource: "/api/2.0/token-management/tokens/abc", Status: 500, - Response: common.APIError{ + Response: apierr.APIError{ Message: "nope", }, }, @@ -109,7 +59,7 @@ func TestResourceOboTokenCreate_Error(t *testing.T) { Method: "POST", Resource: "/api/2.0/token-management/on-behalf-of/tokens", Status: 500, - Response: common.APIError{ + Response: apierr.APIError{ Message: "nope", }, }, diff --git a/tokens/resource_service_principal_secret.go b/tokens/resource_service_principal_secret.go index 4ad7a184d9..e3be4aa8e1 100644 --- a/tokens/resource_service_principal_secret.go +++ b/tokens/resource_service_principal_secret.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -31,19 +32,19 @@ type ServicePrincipalSecretAPI struct { } func (a ServicePrincipalSecretAPI) createServicePrincipalSecret(spnID string) (secret *ServicePrincipalSecret, err error) { - path := fmt.Sprintf("/accounts/%s/servicePrincipals/%s/credentials/secrets", a.client.AccountID, spnID) + path := fmt.Sprintf("/accounts/%s/servicePrincipals/%s/credentials/secrets", a.client.Config.AccountID, spnID) err = a.client.Post(a.context, path, map[string]any{}, &secret) return } func (a ServicePrincipalSecretAPI) listServicePrincipalSecrets(spnID string) (secrets ListServicePrincipalSecrets, err error) { - path := fmt.Sprintf("/accounts/%s/servicePrincipals/%s/credentials/secrets", a.client.AccountID, spnID) + path := fmt.Sprintf("/accounts/%s/servicePrincipals/%s/credentials/secrets", a.client.Config.AccountID, spnID) err = a.client.Get(a.context, path, nil, &secrets) return } func (a ServicePrincipalSecretAPI) deleteServicePrincipalSecret(spnID, secretID string) error { // FIXME - path := fmt.Sprintf("/accounts/%s/servicePrincipals/%s/credentials/secrets/%s", a.client.AccountID, spnID, secretID) + path := fmt.Sprintf("/accounts/%s/servicePrincipals/%s/credentials/secrets/%s", a.client.Config.AccountID, spnID, secretID) return a.client.Delete(a.context, path, nil) } @@ -61,7 +62,7 @@ func ResourceServicePrincipalSecret() *schema.Resource { return common.Resource{ Schema: spnSecretSchema, Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - if c.AccountID == "" { + if c.Config.AccountID == "" { return errors.New("must have `account_id` on provider") } idSeen := map[string]bool{} @@ -92,7 +93,7 @@ func ResourceServicePrincipalSecret() *schema.Resource { return d.Set("secret", secret.Secret) }, Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - if c.AccountID == "" { + if c.Config.AccountID == "" { return errors.New("must have `account_id` on provider") } api := NewServicePrincipalSecretAPI(ctx, c) @@ -107,10 +108,10 @@ func ResourceServicePrincipalSecret() *schema.Resource { } return d.Set("status", v.Status) } - return common.NotFound("client secret not found") + return apierr.NotFound("client secret not found") }, Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - if c.AccountID == "" { + if c.Config.AccountID == "" { return errors.New("must have `account_id` on provider") } api := NewServicePrincipalSecretAPI(ctx, c) diff --git a/tokens/resource_token.go b/tokens/resource_token.go index 9a41f36e32..022f3c8471 100644 --- a/tokens/resource_token.go +++ b/tokens/resource_token.go @@ -3,9 +3,9 @@ package tokens import ( "context" "fmt" - "net/http" "time" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -81,12 +81,7 @@ func (a TokensAPI) Read(tokenID string) (TokenInfo, error) { return tokenInfoRecord, nil } } - return tokenInfo, common.APIError{ - ErrorCode: "NOT_FOUND", - Message: fmt.Sprintf("Unable to locate token: %s", tokenID), - Resource: "/api/2.0/token/list", - StatusCode: http.StatusNotFound, - } + return tokenInfo, apierr.NotFound(fmt.Sprintf("Unable to locate token: %s", tokenID)) } // Delete will delete the token given a token id @@ -94,7 +89,7 @@ func (a TokensAPI) Delete(tokenID string) error { err := a.client.Post(a.context, "/token/delete", map[string]string{ "token_id": tokenID, }, nil) - if common.IsMissing(err) { + if apierr.IsMissing(err) { return nil } return err diff --git a/tokens/resource_token_test.go b/tokens/resource_token_test.go index a516eed291..d67275ee77 100644 --- a/tokens/resource_token_test.go +++ b/tokens/resource_token_test.go @@ -1,12 +1,9 @@ package tokens import ( - "context" - "os" "testing" - "time" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -74,7 +71,7 @@ func TestResourceTokenRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/token/list", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -140,7 +137,7 @@ func TestResourceTokenCreate_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/token/create", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -224,7 +221,7 @@ func TestResourceTokenDelete_NotFoundNoError(t *testing.T) { { Method: "POST", Resource: "/api/2.0/token/delete", - Response: common.NotFound("RESOURCE_DOES_NOT_EXIST"), // per documentation this is the error response + Response: apierr.NotFound("RESOURCE_DOES_NOT_EXIST"), // per documentation this is the error response Status: 404, }, }, @@ -241,7 +238,7 @@ func TestResourceTokenDelete_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/token/delete", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -255,57 +252,3 @@ func TestResourceTokenDelete_Error(t *testing.T) { qa.AssertErrorStartsWith(t, err, "Internal error happened") assert.Equal(t, "abc", d.Id()) } - -func TestAccCreateToken(t *testing.T) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - client := common.NewClientFromEnvironment() - tokensAPI := NewTokensAPI(context.Background(), client) - - //lint:ignore ST1011 it's a test here - lifeTimeSeconds := time.Duration(30) * time.Second - comment := "Hello world" - - token, err := tokensAPI.Create(lifeTimeSeconds, comment) - assert.NoError(t, err) - assert.True(t, len(token.TokenValue) > 0, "Token value is empty") - - defer func() { - err := tokensAPI.Delete(token.TokenInfo.TokenID) - assert.NoError(t, err) - }() - - _, err = tokensAPI.Read(token.TokenInfo.TokenID) - assert.NoError(t, err) - - tokenList, err := tokensAPI.List() - assert.NoError(t, err) - assert.True(t, len(tokenList) > 0, "Token list is empty") -} - -func TestAccCreateToken_NoExpiration(t *testing.T) { - if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { - t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") - } - t.Parallel() - client := common.NewClientFromEnvironment() - tokensAPI := NewTokensAPI(context.Background(), client) - - token, err := tokensAPI.Create(0, "") - assert.NoError(t, err) - assert.True(t, len(token.TokenValue) > 0, "Token value is empty") - - defer func() { - err := tokensAPI.Delete(token.TokenInfo.TokenID) - assert.NoError(t, err) - }() - - _, err = tokensAPI.Read(token.TokenInfo.TokenID) - assert.NoError(t, err) - - tokenList, err := tokensAPI.List() - assert.NoError(t, err) - assert.True(t, len(tokenList) > 0, "Token list is empty") -} diff --git a/workspace/data_notebook_test.go b/workspace/data_notebook_test.go index ed69b564a9..9c33cf89e1 100644 --- a/workspace/data_notebook_test.go +++ b/workspace/data_notebook_test.go @@ -3,7 +3,7 @@ package workspace import ( "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -75,7 +75,7 @@ func TestDataSourceNotebook_ErrorStatus(t *testing.T) { Method: "GET", Resource: "/api/2.0/workspace/export?format=SOURCE&path=%2Fa%2Fb%2Fc", Status: 401, - Response: common.APIError{ + Response: apierr.APIError{ ErrorCode: "Unauthorized", StatusCode: 401, Message: "Unauthorized", diff --git a/workspace/resource_directory_test.go b/workspace/resource_directory_test.go index fb03d8541f..995ef10709 100644 --- a/workspace/resource_directory_test.go +++ b/workspace/resource_directory_test.go @@ -6,7 +6,7 @@ import ( "net/url" "testing" - "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -69,7 +69,7 @@ func TestResourceDirectoryRead_NotFound(t *testing.T) { { // read log output for correct url... Method: "GET", Resource: fmt.Sprintf("/api/2.0/workspace/get-status?path=%s", url.PathEscape(path)), - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -90,7 +90,7 @@ func TestResourceDirectoryRead_Error(t *testing.T) { { Method: "GET", Resource: fmt.Sprintf("/api/2.0/workspace/get-status?path=%s", url.PathEscape(path)), - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -148,7 +148,7 @@ func TestResourceDirectoryCreate_Error(t *testing.T) { ExpectedRequest: map[string]string{ "path": path, }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -173,7 +173,7 @@ func TestResourceDirectoryDelete_Error(t *testing.T) { Method: "POST", Resource: "/api/2.0/workspace/delete", ExpectedRequest: DeletePath{Path: path, Recursive: false}, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/workspace/resource_global_init_script_test.go b/workspace/resource_global_init_script_test.go index 5ad36a0bc6..06dab5aef4 100644 --- a/workspace/resource_global_init_script_test.go +++ b/workspace/resource_global_init_script_test.go @@ -6,8 +6,7 @@ import ( "strings" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -71,7 +70,7 @@ func TestResourceGlobalInitScriptRead_NotFound(t *testing.T) { { // read log output for correct url... Method: "GET", Resource: "/api/2.0/global-init-scripts/1234", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "RESOURCE_DOES_NOT_EXIST", Message: "The global unit script with ID 1234 does not exist.", }, diff --git a/workspace/resource_notebook_test.go b/workspace/resource_notebook_test.go index 92b94a3088..65dc3fd181 100644 --- a/workspace/resource_notebook_test.go +++ b/workspace/resource_notebook_test.go @@ -4,8 +4,7 @@ import ( "net/http" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" @@ -64,7 +63,7 @@ func TestResourceNotebookRead_NotFound(t *testing.T) { { // read log output for correct url... Method: "GET", Resource: "/api/2.0/workspace/get-status?path=%2Ftest%2Fpath", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "NOT_FOUND", Message: "Item not found", }, @@ -84,7 +83,7 @@ func TestResourceNotebookRead_Error(t *testing.T) { { Method: "GET", Resource: "/api/2.0/workspace/get-status?path=%2Ftest%2Fpath", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -248,7 +247,7 @@ func TestResourceNotebookCreate_Error(t *testing.T) { { Method: http.MethodPost, Resource: "/api/2.0/workspace/import", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -273,7 +272,7 @@ func TestResourceNotebookDelete_Error(t *testing.T) { { Method: "POST", Resource: "/api/2.0/workspace/delete", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, diff --git a/workspace/resource_workspace_conf_test.go b/workspace/resource_workspace_conf_test.go index a5ebeeb29a..592298e620 100644 --- a/workspace/resource_workspace_conf_test.go +++ b/workspace/resource_workspace_conf_test.go @@ -4,8 +4,7 @@ import ( "net/http" "testing" - "github.com/databricks/terraform-provider-databricks/common" - + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" ) @@ -48,7 +47,7 @@ func TestWorkspaceConfCreate_Error(t *testing.T) { ExpectedRequest: map[string]string{ "enableIpAccessLists": "true", }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -121,7 +120,7 @@ func TestWorkspaceConfUpdate_Error(t *testing.T) { ExpectedRequest: map[string]string{ "enableIpAccessLists": "true", }, - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -160,7 +159,7 @@ func TestWorkspaceConfRead_Error(t *testing.T) { { Method: http.MethodGet, Resource: "/api/2.0/workspace-conf?", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", }, @@ -209,7 +208,7 @@ func TestWorkspaceConfDelete_Error(t *testing.T) { { Method: http.MethodPatch, Resource: "/api/2.0/workspace-conf", - Response: common.APIErrorBody{ + Response: apierr.APIErrorBody{ ErrorCode: "INVALID_REQUEST", Message: "Internal error happened", },