-
Notifications
You must be signed in to change notification settings - Fork 386
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added databricks_service_principal resource. (#386)
Directly creates service principal, that could be added to a group within workspace. ```hcl resource "databricks_service_principal" "sp" { application_id = "00000000-0000-0000-0000-000000000000" } ``` Co-authored-by: tcz001 <fan.torchz@gmail.com> Co-authored-by: Angel Villalain Garcia <avillalaingarcia@humana.com> Co-authored-by: Serge Smertin <serge.smertin@databricks.com>
- Loading branch information
1 parent
9685caf
commit 7a3ee43
Showing
8 changed files
with
660 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# databricks_service_principal Resource | ||
|
||
Directly creates service principal, that could be added to [databricks_group](group.md) within workspace. | ||
|
||
## Example Usage | ||
|
||
Creating regular service principal: | ||
|
||
```hcl | ||
resource "databricks_service_principal" "sp" { | ||
application_id = "00000000-0000-0000-0000-000000000000" | ||
} | ||
``` | ||
|
||
Creating service principal with administrative permissions - referencing special `admins` [databricks_group](../data-sources/group.md) in [databricks_group_member](group_member.md) resource: | ||
|
||
```hcl | ||
data "databricks_group" "admins" { | ||
display_name = "admins" | ||
} | ||
resource "databricks_service_principal" "sp" { | ||
application_id = "00000000-0000-0000-0000-000000000000" | ||
} | ||
resource "databricks_group_member" "i-am-admin" { | ||
group_id = data.databricks_group.admins.id | ||
member_id = databricks_service_principal.sp.id | ||
} | ||
``` | ||
|
||
Creating service principal with cluster create permissions: | ||
|
||
```hcl | ||
resource "databricks_service_principal" "sp" { | ||
application_id = "00000000-0000-0000-0000-000000000000" | ||
display_name = "Example service principal" | ||
allow_cluster_create = true | ||
} | ||
``` | ||
|
||
## Argument Reference | ||
|
||
The following arguments are available: | ||
|
||
* `application_id` - (Required) This is the application id of the given service principal and will be their form of access and identity. | ||
* `display_name` - (Optional) This is an alias for the service principal can be the full name of the service principal. | ||
* `allow_cluster_create` - (Optional) Allow the service principal to have [cluster](cluster.md) create priviliges. Defaults to false. More fine grained permissions could be assigned with [databricks_permissions](permissions.md#Cluster-usage) and `cluster_id` argument. Everyone without `allow_cluster_create` arugment set, but with [permission to use](permissions.md#Cluster-Policy-usage) Cluster Policy would be able to create clusters, but within boundaries of that specific policy. | ||
* `allow_instance_pool_create` - (Optional) Allow the service principal to have [instance pool](instance_pool.md) create priviliges. Defaults to false. More fine grained permissions could be assigned with [databricks_permissions](permissions.md#Instance-Pool-usage) and [instance_pool_id](permissions.md#instance_pool_id) argument. | ||
* `active` - (Optional) Either service principal is active or not. True by default, but can be set to false in case of service principal deactivation with preserving service principal assets. | ||
|
||
## Attribute Reference | ||
|
||
In addition to all arguments above, the following attributes are exported: | ||
|
||
* `id` - Canonical unique identifier for the service principal. | ||
|
||
## Import | ||
|
||
The resource scim service principal can be imported using id: | ||
|
||
```bash | ||
$ terraform import databricks_service_principal.me <service-principal-id> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package acceptance | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
|
||
"github.com/databrickslabs/databricks-terraform/internal/acceptance" | ||
"github.com/databrickslabs/databricks-terraform/internal/qa" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" | ||
) | ||
|
||
func TestAccServicePrincipalResource(t *testing.T) { | ||
if _, ok := os.LookupEnv("CLOUD_ENV"); !ok { | ||
t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set") | ||
} | ||
config := qa.EnvironmentTemplate(t, ` | ||
data "databricks_group" "admins" { | ||
display_name = "admins" | ||
} | ||
resource "databricks_service_principal" "sp_first" { | ||
application_id = "00000000-1234-5678-0000-000000000001" | ||
display_name = "Eerste {var.RANDOM}" | ||
} | ||
resource "databricks_service_principal" "sp_second" { | ||
application_id = "00000000-1234-5678-0000-000000000002" | ||
display_name = "Tweede {var.RANDOM}" | ||
allow_cluster_create = true | ||
} | ||
resource "databricks_service_principal" "sp_third" { | ||
application_id = "00000000-1234-5678-0000-000000000003" | ||
allow_instance_pool_create = true | ||
}`) | ||
acceptance.AccTest(t, resource.TestCase{ | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: config, | ||
Check: resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttr("databricks_service_principal.sp_first", "allow_cluster_create", "false"), | ||
resource.TestCheckResourceAttr("databricks_service_principal.sp_first", "allow_instance_pool_create", "false"), | ||
resource.TestCheckResourceAttr("databricks_service_principal.sp_second", "allow_cluster_create", "true"), | ||
resource.TestCheckResourceAttr("databricks_service_principal.sp_second", "allow_instance_pool_create", "false"), | ||
resource.TestCheckResourceAttr("databricks_service_principal.sp_third", "allow_cluster_create", "false"), | ||
resource.TestCheckResourceAttr("databricks_service_principal.sp_third", "allow_instance_pool_create", "true"), | ||
), | ||
Destroy: false, | ||
}, | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
package identity | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
|
||
"github.com/databrickslabs/databricks-terraform/common" | ||
"github.com/databrickslabs/databricks-terraform/internal" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
) | ||
|
||
// NewServicePrincipalsAPI creates ServicePrincipalsAPI instance from provider meta | ||
func NewServicePrincipalsAPI(m interface{}) ServicePrincipalsAPI { | ||
return ServicePrincipalsAPI{client: m.(*common.DatabricksClient)} | ||
} | ||
|
||
// ServicePrincipalsAPI exposes the scim servicePrincipal API | ||
type ServicePrincipalsAPI struct { | ||
client *common.DatabricksClient | ||
} | ||
|
||
// ServicePrincipalEntity entity from which resource schema is made | ||
type ServicePrincipalEntity struct { | ||
ApplicationID string `json:"application_id"` | ||
DisplayName string `json:"display_name,omitempty" tf:"computed"` | ||
Active bool `json:"active,omitempty"` | ||
AllowClusterCreate bool `json:"allow_cluster_create,omitempty"` | ||
AllowInstancePoolCreate bool `json:"allow_instance_pool_create,omitempty"` | ||
} | ||
|
||
func (sp ServicePrincipalEntity) toRequest() ScimUser { | ||
entitlements := []entitlementsListItem{} | ||
if sp.AllowClusterCreate { | ||
entitlements = append(entitlements, entitlementsListItem{ | ||
Value: Entitlement("allow-cluster-create"), | ||
}) | ||
} | ||
if sp.AllowInstancePoolCreate { | ||
entitlements = append(entitlements, entitlementsListItem{ | ||
Value: Entitlement("allow-instance-pool-create"), | ||
}) | ||
} | ||
return ScimUser{ | ||
Schemas: []URN{ServicePrincipalSchema}, | ||
ApplicationID: sp.ApplicationID, | ||
Active: sp.Active, | ||
DisplayName: sp.DisplayName, | ||
Entitlements: entitlements, | ||
} | ||
} | ||
|
||
// CreateR .. | ||
func (a ServicePrincipalsAPI) CreateR(rsp ServicePrincipalEntity) (sp ScimUser, err error) { | ||
err = a.client.Scim(http.MethodPost, "/preview/scim/v2/ServicePrincipals", rsp.toRequest(), &sp) | ||
return sp, err | ||
} | ||
|
||
// ReadR reads resource-friendly entity | ||
func (a ServicePrincipalsAPI) ReadR(servicePrincipalID string) (rsp ServicePrincipalEntity, err error) { | ||
servicePrincipal, err := a.read(servicePrincipalID) | ||
if err != nil { | ||
return | ||
} | ||
rsp.ApplicationID = servicePrincipal.ApplicationID | ||
rsp.DisplayName = servicePrincipal.DisplayName | ||
rsp.Active = servicePrincipal.Active | ||
for _, ent := range servicePrincipal.Entitlements { | ||
switch ent.Value { | ||
case AllowClusterCreateEntitlement: | ||
rsp.AllowClusterCreate = true | ||
case AllowInstancePoolCreateEntitlement: | ||
rsp.AllowInstancePoolCreate = true | ||
} | ||
} | ||
return | ||
} | ||
|
||
func (a ServicePrincipalsAPI) read(servicePrincipalID string) (sp ScimUser, err error) { | ||
servicePrincipalPath := fmt.Sprintf("/preview/scim/v2/ServicePrincipals/%v", servicePrincipalID) | ||
err = a.client.Scim(http.MethodGet, servicePrincipalPath, nil, &sp) | ||
return | ||
} | ||
|
||
// UpdateR replaces resource-friendly-entity | ||
func (a ServicePrincipalsAPI) UpdateR(servicePrincipalID string, rsp ServicePrincipalEntity) error { | ||
servicePrincipal, err := a.read(servicePrincipalID) | ||
if err != nil { | ||
return err | ||
} | ||
updateRequest := rsp.toRequest() | ||
updateRequest.Groups = servicePrincipal.Groups | ||
return a.client.Scim(http.MethodPut, | ||
fmt.Sprintf("/preview/scim/v2/ServicePrincipals/%v", servicePrincipalID), | ||
updateRequest, nil) | ||
} | ||
|
||
// PatchR updates resource-friendly entity | ||
func (a ServicePrincipalsAPI) PatchR(servicePrincipalID string, r patchRequest) error { | ||
return a.client.Scim(http.MethodPatch, fmt.Sprintf("/preview/scim/v2/ServicePrincipals/%v", servicePrincipalID), r, nil) | ||
} | ||
|
||
// Delete will delete the servicePrincipal given the servicePrincipal id | ||
func (a ServicePrincipalsAPI) Delete(servicePrincipalID string) error { | ||
servicePrincipalPath := fmt.Sprintf("/preview/scim/v2/ServicePrincipals/%v", servicePrincipalID) | ||
return a.client.Scim(http.MethodDelete, servicePrincipalPath, nil, nil) | ||
} | ||
|
||
// ResourceServicePrincipal manages service principals within workspace | ||
func ResourceServicePrincipal() *schema.Resource { | ||
servicePrincipalSchema := internal.StructToSchema(ServicePrincipalEntity{}, func( | ||
s map[string]*schema.Schema) map[string]*schema.Schema { | ||
s["application_id"].ForceNew = true | ||
s["active"].Default = true | ||
return s | ||
}) | ||
readContext := func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { | ||
servicePrincipal, err := NewServicePrincipalsAPI(m).ReadR(d.Id()) | ||
if e, ok := err.(common.APIError); ok && e.IsMissing() { | ||
log.Printf("missing resource due to error: %v\n", e) | ||
d.SetId("") | ||
return nil | ||
} | ||
if err != nil { | ||
return diag.FromErr(err) | ||
} | ||
err = internal.StructToData(servicePrincipal, servicePrincipalSchema, d) | ||
if err != nil { | ||
return diag.FromErr(err) | ||
} | ||
return nil | ||
} | ||
return &schema.Resource{ | ||
Schema: servicePrincipalSchema, | ||
ReadContext: readContext, | ||
CreateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { | ||
var ru ServicePrincipalEntity | ||
err := internal.DataToStructPointer(d, servicePrincipalSchema, &ru) | ||
if err != nil { | ||
return diag.FromErr(err) | ||
} | ||
servicePrincipal, err := NewServicePrincipalsAPI(m).CreateR(ru) | ||
if err != nil { | ||
return diag.FromErr(err) | ||
} | ||
d.SetId(servicePrincipal.ID) | ||
return readContext(ctx, d, m) | ||
}, | ||
UpdateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { | ||
var ru ServicePrincipalEntity | ||
err := internal.DataToStructPointer(d, servicePrincipalSchema, &ru) | ||
if err != nil { | ||
return diag.FromErr(err) | ||
} | ||
err = NewServicePrincipalsAPI(m).UpdateR(d.Id(), ru) | ||
if err != nil { | ||
return diag.FromErr(err) | ||
} | ||
return readContext(ctx, d, m) | ||
}, | ||
DeleteContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { | ||
err := NewServicePrincipalsAPI(m).Delete(d.Id()) | ||
if err != nil { | ||
return diag.FromErr(err) | ||
} | ||
return nil | ||
}, | ||
Importer: &schema.ResourceImporter{ | ||
StateContext: schema.ImportStatePassthroughContext, | ||
}, | ||
} | ||
} |
Oops, something went wrong.