diff --git a/acceptance/bundle/resources/secret_scopes/test.toml b/acceptance/bundle/resources/secret_scopes/test.toml index 347f57ba18..b647ee7873 100644 --- a/acceptance/bundle/resources/secret_scopes/test.toml +++ b/acceptance/bundle/resources/secret_scopes/test.toml @@ -2,7 +2,8 @@ Cloud = true Local = true RecordRequests = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] # secret scopes not implemented +# direct mode doesn't support secret scope permissions yet +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] Ignore = [ "databricks.yml", diff --git a/acceptance/bundle/resources/secrets/databricks.yml.tmpl b/acceptance/bundle/resources/secrets/databricks.yml.tmpl new file mode 100644 index 0000000000..1e73f240de --- /dev/null +++ b/acceptance/bundle/resources/secrets/databricks.yml.tmpl @@ -0,0 +1,7 @@ +bundle: + name: deploy-secrets-test-$UNIQUE_NAME + +resources: + secret_scopes: + test_scope: + name: $SECRET_SCOPE_NAME diff --git a/acceptance/bundle/resources/secrets/out.test.toml b/acceptance/bundle/resources/secrets/out.test.toml new file mode 100644 index 0000000000..01ed6822af --- /dev/null +++ b/acceptance/bundle/resources/secrets/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = true + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/secrets/output.txt b/acceptance/bundle/resources/secrets/output.txt new file mode 100644 index 0000000000..cd4f44b127 --- /dev/null +++ b/acceptance/bundle/resources/secrets/output.txt @@ -0,0 +1,49 @@ + +=== Deploy the secret scope first +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-secrets-test-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +=== Verify the secret scope was created +>>> [CLI] secrets list-scopes -o json +{ + "backend_type": "DATABRICKS", + "name": "secrets-test-[UUID]" +} + +=== Create a secret via CLI +>>> [CLI] secrets put-secret secrets-test-[UUID] my-secret-key --string-value my-secret-value + +=== Verify secret was created +>>> [CLI] secrets get-secret secrets-test-[UUID] my-secret-key +{ + "key":"my-secret-key", + "value":"bXktc2VjcmV0LXZhbHVl" +} + +=== Now add a secret to the bundle +=== Deploy bundle with secret (scope already exists) +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-secrets-test-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +=== Verify bundle secret was created +>>> [CLI] secrets get-secret secrets-test-[UUID] bundle_secret +{ + "key":"bundle_secret", + "value":"YnVuZGxlLXNlY3JldC12YWx1ZQ==" +} + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.secret_scopes.test_scope + delete resources.secrets.secret1 + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-secrets-test-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/resources/secrets/script b/acceptance/bundle/resources/secrets/script new file mode 100644 index 0000000000..f3fd8ccbfc --- /dev/null +++ b/acceptance/bundle/resources/secrets/script @@ -0,0 +1,47 @@ +SECRET_SCOPE_NAME="secrets-test-$(uuid)" +if [ -z "$CLOUD_ENV" ]; then + SECRET_SCOPE_NAME="secrets-test-6260d50f-e8ff-4905-8f28-812345678903" # use hard-coded uuid when running locally +fi +export SECRET_SCOPE_NAME + +envsubst < databricks.yml.tmpl > databricks.yml + +cleanup() { + trace $CLI bundle destroy --auto-approve +} +trap cleanup EXIT + +title "Deploy the secret scope first" +trace $CLI bundle deploy + +title "Verify the secret scope was created" +trace $CLI secrets list-scopes -o json | jq --arg name "${SECRET_SCOPE_NAME}" '.[] | select(.name == $name)' + +title "Create a secret via CLI" +trace $CLI secrets put-secret ${SECRET_SCOPE_NAME} my-secret-key --string-value "my-secret-value" + +title "Verify secret was created" +trace $CLI secrets get-secret ${SECRET_SCOPE_NAME} my-secret-key + +title "Now add a secret to the bundle" +cat > databricks.yml < len(deleteReq.Scope) && key[:len(deleteReq.Scope)] == deleteReq.Scope && key[len(deleteReq.Scope)] == '/' { + delete(s.Secrets, key) + } + } + + return Response{} +} + +// SecretPut handles POST /api/2.0/secrets/put +func (s *FakeWorkspace) SecretPut(req Request) Response { + defer s.LockUnlock()() + + var putReq workspace.PutSecret + if err := json.Unmarshal(req.Body, &putReq); err != nil { + return Response{ + StatusCode: 400, + Body: map[string]string{"message": err.Error()}, + } + } + + if putReq.Scope == "" || putReq.Key == "" { + return Response{ + StatusCode: 400, + Body: map[string]string{"message": "Scope and key are required"}, + } + } + + // Check if scope exists + if _, exists := s.SecretScopes[putReq.Scope]; !exists { + return Response{ + StatusCode: 404, + Body: map[string]string{"error_code": "RESOURCE_DOES_NOT_EXIST", "message": "Scope not found"}, + } + } + + secretKey := fmt.Sprintf("%s/%s", putReq.Scope, putReq.Key) + s.Secrets[secretKey] = workspace.SecretMetadata{ + Key: putReq.Key, + LastUpdatedTimestamp: nowMilli(), + } + + return Response{} +} + +// SecretGet handles GET /api/2.0/secrets/get +func (s *FakeWorkspace) SecretGet(req Request) Response { + defer s.LockUnlock()() + + scope := req.URL.Query().Get("scope") + key := req.URL.Query().Get("key") + + if scope == "" || key == "" { + return Response{ + StatusCode: 400, + Body: map[string]string{"message": "Scope and key are required"}, + } + } + + secretKey := fmt.Sprintf("%s/%s", scope, key) + secret, exists := s.Secrets[secretKey] + if !exists { + return Response{ + StatusCode: 404, + Body: map[string]string{"error_code": "RESOURCE_DOES_NOT_EXIST", "message": "Secret not found"}, + } + } + + return Response{ + Body: workspace.GetSecretResponse{ + Key: secret.Key, + Value: "", // Value is never returned for security reasons + }, + } +} + +// SecretDelete handles POST /api/2.0/secrets/delete +func (s *FakeWorkspace) SecretDelete(req Request) Response { + defer s.LockUnlock()() + + var deleteReq workspace.DeleteSecret + if err := json.Unmarshal(req.Body, &deleteReq); err != nil { + return Response{ + StatusCode: 400, + Body: map[string]string{"message": err.Error()}, + } + } + + secretKey := fmt.Sprintf("%s/%s", deleteReq.Scope, deleteReq.Key) + if _, exists := s.Secrets[secretKey]; !exists { + return Response{ + StatusCode: 404, + Body: map[string]string{"error_code": "RESOURCE_DOES_NOT_EXIST", "message": "Secret not found"}, + } + } + + delete(s.Secrets, secretKey) + + return Response{} +}