Skip to content

Add secret scopes support in assets bundling #2744

New issue

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

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

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions acceptance/bundle/deploy/secret-scope/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

bundle:
name: deploy-secret-scope-test-$UNIQUE_NAME

resources:
secret_scopes:
secret_scope1:
name: my-secrets
initial_manage_principal: users
44 changes: 44 additions & 0 deletions acceptance/bundle/deploy/secret-scope/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-secret-scope-test-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] bundle summary --output json
{
"initial_manage_principal": "users",
"modified_status": "created",
"name": "my-secrets"
}

>>> [CLI] secrets list-scopes -o json
{
"backend_type": "DATABRICKS",
"name": "my-secrets"
}

>>> [CLI] secrets list-acls my-secrets
[
{
"permission": "MANAGE",
"principal": "[USERNAME]"
}
]

>>> [CLI] secrets put-secret my-secrets my-key --string-value my-secret-value

>>> [CLI] secrets get-secret my-secrets my-key
{
"key":"my-key",
"value":"bXktc2VjcmV0LXZhbHVl"
}

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete secret_scope secret_scope1

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-secret-scope-test-[UNIQUE_NAME]/default

Deleting files...
Destroy complete!
15 changes: 15 additions & 0 deletions acceptance/bundle/deploy/secret-scope/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
envsubst < databricks.yml.tmpl > databricks.yml

cleanup() {
trace $CLI bundle destroy --auto-approve
}
trap cleanup EXIT

trace $CLI bundle deploy
trace $CLI bundle summary --output json | jq '.resources.secret_scopes.secret_scope1'

trace $CLI secrets list-scopes -o json | jq '.[] | select(.name == "my-secrets")'
trace $CLI secrets list-acls my-secrets

trace $CLI secrets put-secret my-secrets my-key --string-value "my-secret-value"
trace $CLI secrets get-secret my-secrets my-key
50 changes: 50 additions & 0 deletions acceptance/bundle/deploy/secret-scope/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Cloud = true
Local = true

Ignore = [
"databricks.yml",
]

[[Server]]
Pattern = "POST /api/2.0/secrets/scopes/create"

[[Server]]
Pattern = "GET /api/2.0/secrets/scopes/list"
Response.Body = '''
{
"scopes": [
{
"backend_type": "DATABRICKS",
"name": "my-secrets"
}
]
}
'''

[[Server]]
Pattern = "POST /api/2.0/secrets/scopes/delete"

[[Server]]
Pattern = "POST /api/2.0/secrets/put"

[[Server]]
Pattern = "GET /api/2.0/secrets/get"
Response.Body = '''
{
"key":"my-key",
"value":"bXktc2VjcmV0LXZhbHVl"
}
'''

[[Server]]
Pattern = "GET /api/2.0/secrets/acls/list"
Response.Body = '''
{
"items": [
{
"permission": "MANAGE",
"principal": "[USERNAME]"
}
]
}
'''
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bundle:
name: bind-dashboard-test-$UNIQUE_NAME

resources:
secret_scopes:
secret_scope1:
name: $SECRET_SCOPE_NAME
35 changes: 35 additions & 0 deletions acceptance/bundle/deployment/bind/secret-scope/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

>>> [CLI] secrets create-scope test-secret-scope-[UUID]

>>> [CLI] bundle deployment bind secret_scope1 test-secret-scope-[UUID] --auto-approve
Updating deployment state...
Successfully bound secret_scope with an id 'test-secret-scope-[UUID]'. Run 'bundle deploy' to deploy changes to your workspace

>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/bind-dashboard-test-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] secrets list-scopes -o json
{
"backend_type": "DATABRICKS",
"name": "test-secret-scope-[UUID]"
}

>>> [CLI] bundle deployment unbind secret_scope1
Updating deployment state...

>>> [CLI] bundle destroy --auto-approve
All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/bind-dashboard-test-[UNIQUE_NAME]/default

Deleting files...
Destroy complete!

>>> [CLI] secrets list-scopes -o json
{
"backend_type": "DATABRICKS",
"name": "test-secret-scope-[UUID]"
}

>>> [CLI] secrets delete-scope test-secret-scope-[UUID]
26 changes: 26 additions & 0 deletions acceptance/bundle/deployment/bind/secret-scope/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
SECRET_SCOPE_NAME="test-secret-scope-$(uuid)"
if [ -z "$CLOUD_ENV" ]; then
SECRET_SCOPE_NAME="test-secret-scope-6260d50f-e8ff-4905-8f28-812345678903" # use hard-coded uuid when running locally
fi
export SECRET_SCOPE_NAME
envsubst < databricks.yml.tmpl > databricks.yml

# Create a pre-defined volume:
trace $CLI secrets create-scope "${SECRET_SCOPE_NAME}"

cleanup() {
trace $CLI secrets delete-scope "${SECRET_SCOPE_NAME}"
}
trap cleanup EXIT

trace $CLI bundle deployment bind secret_scope1 "${SECRET_SCOPE_NAME}" --auto-approve

trace $CLI bundle deploy

trace $CLI secrets list-scopes -o json | jq --arg value ${SECRET_SCOPE_NAME} '.[] | select(.name == $value)'

trace $CLI bundle deployment unbind secret_scope1

trace $CLI bundle destroy --auto-approve

trace $CLI secrets list-scopes -o json | jq --arg value ${SECRET_SCOPE_NAME} '.[] | select(.name == $value)'
26 changes: 26 additions & 0 deletions acceptance/bundle/deployment/bind/secret-scope/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Local = true
Cloud = true
RequiresUnityCatalog = true

Ignore = [
"databricks.yml",
]

[[Server]]
Pattern = "POST /api/2.0/secrets/scopes/create"

[[Server]]
Pattern = "GET /api/2.0/secrets/scopes/list"
Response.Body = '''
{
"scopes": [
{
"backend_type": "DATABRICKS",
"name": "test-secret-scope-6260d50f-e8ff-4905-8f28-812345678903"
}
]
}
'''

[[Server]]
Pattern = "POST /api/2.0/secrets/scopes/delete"
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ var (
permissions.CAN_MANAGE: "CAN_MANAGE",
permissions.CAN_VIEW: "CAN_USE",
},
"secret_scopes": {
permissions.CAN_MANAGE: "MANAGE",
permissions.CAN_VIEW: "READ",
},
}
)

Expand Down
13 changes: 11 additions & 2 deletions bundle/config/mutator/resourcemutator/apply_target_mode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"slices"
"testing"

"github.com/databricks/databricks-sdk-go/service/workspace"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/config/resources"
Expand Down Expand Up @@ -152,6 +154,13 @@ func mockBundle(mode config.Mode) *bundle.Bundle {
},
},
},
SecretScopes: map[string]*resources.SecretScope{
"secretScope1": {
SecretScope: &workspace.SecretScope{
Name: "secretScope1",
},
},
},
},
},
SyncRoot: vfs.MustNew("/Users/lennart.kats@databricks.com"),
Expand Down Expand Up @@ -318,8 +327,8 @@ func TestAllNonUcResourcesAreRenamed(t *testing.T) {
nameField := resource.Elem().FieldByName("Name")
resourceType := resources.Type().Field(i).Name

// Skip apps, as they are not renamed
if resourceType == "Apps" {
// Skip resources that are not renamed
if resourceType == "Apps" || resourceType == "SecretScopes" {
continue
}

Expand Down
10 changes: 10 additions & 0 deletions bundle/config/mutator/resourcemutator/run_as.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ func validateRunAs(b *bundle.Bundle) diag.Diagnostics {
))
}

// Secret Scopes do not support run_as in the API.
if len(b.Config.Resources.SecretScopes) > 0 {
diags = diags.Extend(reportRunAsNotSupported(
"secret_scopes",
b.Config.GetLocation("resources.secret_scopes"),
b.Config.Workspace.CurrentUser.UserName,
identity,
))
}

return diags
}

Expand Down
1 change: 1 addition & 0 deletions bundle/config/mutator/resourcemutator/run_as_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func allResourceTypes(t *testing.T) []string {
"quality_monitors",
"registered_models",
"schemas",
"secret_scopes",
"volumes",
},
resourceTypes,
Expand Down
9 changes: 9 additions & 0 deletions bundle/config/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Resources struct {
Clusters map[string]*resources.Cluster `json:"clusters,omitempty"`
Dashboards map[string]*resources.Dashboard `json:"dashboards,omitempty"`
Apps map[string]*resources.App `json:"apps,omitempty"`
SecretScopes map[string]*resources.SecretScope `json:"secret_scopes,omitempty"`
}

type ConfigResource interface {
Expand Down Expand Up @@ -92,6 +93,7 @@ func (r *Resources) AllResources() []ResourceGroup {
collectResourceMap(descriptions["dashboards"], r.Dashboards),
collectResourceMap(descriptions["volumes"], r.Volumes),
collectResourceMap(descriptions["apps"], r.Apps),
collectResourceMap(descriptions["secret_scopes"], r.SecretScopes),
}
}

Expand Down Expand Up @@ -163,6 +165,12 @@ func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error)
}
}

for k := range r.SecretScopes {
if k == key {
found = append(found, r.SecretScopes[k])
}
}

if len(found) == 0 {
return nil, fmt.Errorf("no such resource: %s", key)
}
Expand Down Expand Up @@ -193,5 +201,6 @@ func SupportedResources() map[string]resources.ResourceDescription {
"dashboards": (&resources.Dashboard{}).ResourceDescription(),
"volumes": (&resources.Volume{}).ResourceDescription(),
"apps": (&resources.App{}).ResourceDescription(),
"secret_scopes": (&resources.SecretScope{}).ResourceDescription(),
}
}
73 changes: 73 additions & 0 deletions bundle/config/resources/secret_scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package resources

import (
"context"
"net/url"

"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/marshal"
"github.com/databricks/databricks-sdk-go/service/workspace"
)

type SecretScope struct {
Name string `json:"name"`
InitialManagePrincipal string `json:"initial_manage_principal"`

ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`

*workspace.SecretScope
}

func (s *SecretScope) UnmarshalJSON(b []byte) error {
return marshal.Unmarshal(b, s)
}

func (s SecretScope) MarshalJSON() ([]byte, error) {
return marshal.Marshal(s)
}

func (s SecretScope) Exists(ctx context.Context, w *databricks.WorkspaceClient, name string) (bool, error) {
scopes, err := w.Secrets.ListScopesAll(ctx)
if err != nil {
return false, nil
}

for _, scope := range scopes {
if scope.Name == name {
return true, nil
}
}

return false, nil
}

func (s SecretScope) ResourceDescription() ResourceDescription {
return ResourceDescription{
SingularName: "secret_scope",
PluralName: "secret_scopes",
SingularTitle: "Secret Scope",
PluralTitle: "Secret Scope",
TerraformResourceName: "databricks_secret_scope",
}
}

func (s SecretScope) TerraformResourceName() string {
return "databricks_secret_scope"
}

func (s SecretScope) GetName() string {
return s.Name
}

func (s SecretScope) GetURL() string {
// Secret scopes do not have a URL
return ""
}

func (s SecretScope) InitializeURL(_ url.URL) {
// Secret scopes do not have a URL
}

func (s SecretScope) IsNil() bool {
return s.SecretScope == nil
}
Loading
Loading