Skip to content
Open
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
18 changes: 18 additions & 0 deletions acceptance/bundle/invariant/configs/secret.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
bundle:
name: test-bundle-$UNIQUE_NAME

variables:
secret_value:
description: The value of the secret

resources:
secrets:
foo:
catalog_name: main
schema_name: default
name: test-secret-$UNIQUE_NAME
value: ${var.secret_value}
grants:
- principal: account users
privileges:
- MANAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export BUNDLE_VAR_secret_value="secret-value"
3 changes: 3 additions & 0 deletions acceptance/bundle/invariant/continue_293/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ EnvMatrixExclude.no_volume_path_job_ref = ["INPUT_CONFIG=volume_path_job_ref.yml
# The 1000-task scale case is covered by no_drift. Running it here adds ~1.5 min
# per variant (two full deploys at 1000 tasks) without incremental coverage.
EnvMatrixExclude.no_pydabs_1000_tasks = ["INPUT_CONFIG=job_pydabs_1000_tasks.yml.tmpl"]

# secret resource is not supported on v0.293.0
EnvMatrixExclude.no_secret = ["INPUT_CONFIG=secret.yml.tmpl"]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions acceptance/bundle/invariant/delete_idempotent/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ EnvMatrix.READPLAN = ["", "1"]
# Snapshot of pre-delete state used to re-run the delete on state that still
# references the (now-gone) resources; may linger if the test fails mid-run.
Ignore = [".databricks.backup"]

[EnvMatrixExclude]
# Secrets have sensitive fields (json:"-") that are stripped when a plan is serialized to JSON,
# so deploying from a pre-computed plan file creates the secret with an empty value.
no_secret_with_readplan = ["READPLAN=1", "INPUT_CONFIG=secret.yml.tmpl"]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions acceptance/bundle/invariant/destroy_idempotent/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ EnvMatrix.READPLAN = ["", "1"]
# Snapshot of pre-destroy state used to re-run destroy on state that still
# references the (now-gone) resources; may linger if the test fails mid-run.
Ignore = [".databricks.backup"]

[EnvMatrixExclude]
# Secrets have sensitive fields (json:"-") that are stripped when a plan is serialized to JSON,
# so deploying from a pre-computed plan file creates the secret with an empty value.
no_secret_with_readplan = ["READPLAN=1", "INPUT_CONFIG=secret.yml.tmpl"]
3 changes: 2 additions & 1 deletion acceptance/bundle/invariant/migrate/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ EnvMatrixExclude.no_pydabs_1000_tasks = ["INPUT_CONFIG=job_pydabs_1000_tasks.yml
# volume schema_name ("inconsistent final plan"). Covered by no_drift on direct.
EnvMatrixExclude.no_volume_uppercase = ["INPUT_CONFIG=volume_uppercase_name.yml.tmpl"]

# Terraform types sampling_fraction as an integer and truncates 0.5; covered by no_drift.
EnvMatrixExclude.no_secret = ["INPUT_CONFIG=secret.yml.tmpl"]

EnvMatrixExclude.no_model_serving_endpoint_telemetry = ["INPUT_CONFIG=model_serving_endpoint_telemetry.yml.tmpl"]
1 change: 1 addition & 0 deletions acceptance/bundle/invariant/no_drift/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions acceptance/bundle/invariant/no_drift/test.toml
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
EnvMatrix.READPLAN = ["", "1"]

[EnvMatrixExclude]
# Secrets have sensitive fields (json:"-") that are stripped when a plan is serialized to JSON,
Comment thread
andrewnester marked this conversation as resolved.
# so deploying from a pre-computed plan file creates the secret with an empty value.
no_secret_with_readplan = ["READPLAN=1", "INPUT_CONFIG=secret.yml.tmpl"]
1 change: 1 addition & 0 deletions acceptance/bundle/invariant/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ EnvMatrix.INPUT_CONFIG = [
"schema_empty_grants.yml.tmpl",
"schema_grant_ref.yml.tmpl",
"schema_uppercase_name.yml.tmpl",
"secret.yml.tmpl",
"secret_scope.yml.tmpl",
"secret_scope_default_backend_type.yml.tmpl",
"sql_warehouse.yml.tmpl",
Expand Down
18 changes: 10 additions & 8 deletions acceptance/bundle/resources/secrets/basic/output.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@

>>> [CLI] bundle validate --var secret_value=initial-secret-value
Name: test-bundle-[UNIQUE_NAME]
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default

Validation OK!
>>> [CLI] bundle validate --var secret_value=initial-secret-value -o json
{
"secret1": {
"catalog_name": "main",
"comment": "Test secret for acceptance testing",
"name": "test_secret_[UNIQUE_NAME]",
"schema_name": "default",
"value": "[redacted]"
}
}

>>> [CLI] bundle summary --var secret_value=initial-secret-value
Name: test-bundle-[UNIQUE_NAME]
Expand Down
2 changes: 1 addition & 1 deletion acceptance/bundle/resources/secrets/basic/script
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ cleanup() {
}
trap cleanup EXIT

trace $CLI bundle validate --var secret_value=initial-secret-value
trace $CLI bundle validate --var secret_value=initial-secret-value -o json | jq ".resources.secrets"
trace $CLI bundle summary --var secret_value=initial-secret-value
trace $CLI bundle deploy --var secret_value=initial-secret-value

Expand Down
53 changes: 52 additions & 1 deletion acceptance/bundle/resources/secrets/update-value/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"comment": "Test secret",
"name": "test_secret",
"schema_name": "default",
"value": ""
"value": "[redacted]"
}
}
}
Expand Down Expand Up @@ -40,6 +40,57 @@ Deployment complete!
secrets secret1 catalog_name='main' schema_name='default' name='test_secret' comment='Test secret' value=''

=== Update secret value by passing a different variable value
>>> [CLI] bundle plan --var secret_value=updated-secret-value -o json
{
"plan_version": 2,
"cli_version": "[CLI_VERSION]",
"lineage": "[UUID]",
"serial": 1,
"plan": {
"resources.secrets.secret1": {
"action": "update",
"new_state": {
"value": {
"catalog_name": "main",
"comment": "Test secret",
"name": "test_secret",
"schema_name": "default",
"value": "[redacted]"
}
},
"remote_state": {
"catalog_name": "main",
"comment": "Test secret",
"create_time": "[TIMESTAMP]",
"created_by": "test-user@databricks.com",
"effective_owner": "test-user@databricks.com",
"effective_value": "[redacted]",
"full_name": "main.default.test_secret",
"metastore_id": "test-metastore-id",
"name": "test_secret",
"owner": "test-user@databricks.com",
"schema_name": "default",
"update_time": "[TIMESTAMP]",
"updated_by": "test-user@databricks.com",
"value": "[redacted]"
},
"changes": {
"owner": {
"action": "skip",
"reason": "spec:input_only",
"remote": "test-user@databricks.com"
},
"value": {
"action": "update",
"old": "",
"new": "[redacted]",
"remote": "[redacted]"
}
}
}
}
}

>>> [CLI] bundle deploy --var secret_value=updated-secret-value
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files...
Deploying resources...
Expand Down
1 change: 1 addition & 0 deletions acceptance/bundle/resources/secrets/update-value/script
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ trace print_requests.py //unity-catalog
read_state.py secrets secret1 catalog_name schema_name name comment value

title "Update secret value by passing a different variable value"
trace $CLI bundle plan --var secret_value=updated-secret-value -o json
trace $CLI bundle deploy --var secret_value=updated-secret-value
trace print_requests.py //unity-catalog
read_state.py secrets secret1 catalog_name schema_name name comment value
Expand Down
2 changes: 0 additions & 2 deletions acceptance/invariant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ const invariantConfigsDir = "bundle/invariant/configs"
// the test fails if an entry here is actually covered, so the list only shrinks.
var LackingInvariantTest = map[string]bool{
"quality_monitors": true,
"secrets": true,
"secrets.grants": true,
}

// TestInvariantConfigsCoverage ensures that the invariant test configs in
Expand Down
29 changes: 25 additions & 4 deletions bundle/direct/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (d *DeploymentUnit) Create(ctx context.Context, db *dstate.DeploymentState,
return err
}

err = db.SaveState(d.ResourceKey, newID, newState, d.DependsOn)
err = d.saveStateRedacted(db, newID, newState, d.DependsOn)
if err != nil {
return fmt.Errorf("saving state after creating id=%s: %w", newID, err)
}
Expand Down Expand Up @@ -163,7 +163,7 @@ func (d *DeploymentUnit) Update(ctx context.Context, db *dstate.DeploymentState,
return fmt.Errorf("deleting state id=%s: %w", id, err)
}
} else {
err = db.SaveState(d.ResourceKey, id, newState, d.DependsOn)
err = d.saveStateRedacted(db, id, newState, d.DependsOn)
if err != nil {
return fmt.Errorf("saving state id=%s: %w", id, err)
}
Expand Down Expand Up @@ -208,7 +208,7 @@ func (d *DeploymentUnit) UpdateWithID(ctx context.Context, db *dstate.Deployment
return err
}

err = db.SaveState(d.ResourceKey, newID, newState, d.DependsOn)
err = d.saveStateRedacted(db, newID, newState, d.DependsOn)
if err != nil {
return fmt.Errorf("saving state id=%s: %w", oldID, err)
}
Expand Down Expand Up @@ -291,14 +291,35 @@ func (d *DeploymentUnit) Resize(ctx context.Context, db *dstate.DeploymentState,
return fmt.Errorf("resizing id=%s: %w", id, err)
}

err = db.SaveState(d.ResourceKey, id, newState, d.DependsOn)
err = d.saveStateRedacted(db, id, newState, d.DependsOn)
if err != nil {
return fmt.Errorf("saving state id=%s: %w", id, err)
}

return nil
}

// saveStateRedacted saves a copy of state to the deployment state file with
// sensitive fields replaced by a placeholder value so secrets are never written
// to disk in plaintext.
func (d *DeploymentUnit) saveStateRedacted(db *dstate.DeploymentState, newID string, state any, dependsOn []deployplan.DependsOnEntry) error {
// Round-trip through JSON to get an independent copy so the original struct
// (still held in memory for post-deploy use) is not modified.
data, err := json.Marshal(state)
if err != nil {
return fmt.Errorf("marshaling state for redaction: %w", err)
}
stateType := d.Adapter.StateType()
ptr := reflect.New(stateType.Elem()).Interface()
if err := json.Unmarshal(data, ptr); err != nil {
return fmt.Errorf("unmarshaling state copy for redaction: %w", err)
}
if err := zeroSensitiveFields(d.Adapter, ptr); err != nil {
return fmt.Errorf("redacting state: %w", err)
}
return db.SaveState(d.ResourceKey, newID, ptr, dependsOn)
}

func parseState(destType reflect.Type, raw json.RawMessage) (any, error) {
destPtr := reflect.New(destType).Interface()
err := json.Unmarshal(raw, destPtr)
Expand Down
10 changes: 10 additions & 0 deletions bundle/direct/bundle_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,16 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks
}
}

for resourceKey, entry := range plan.Plan {
adapter, err := b.getAdapterForKey(resourceKey)
if err != nil {
return nil, fmt.Errorf("redacting plan entry %s: %w", resourceKey, err)
}
if err := redactPlanEntry(adapter, entry); err != nil {
return nil, fmt.Errorf("redacting plan entry %s: %w", resourceKey, err)
}
}

return plan, nil
}

Expand Down
11 changes: 11 additions & 0 deletions bundle/direct/dresources/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,17 @@ func (a *Adapter) GeneratedResourceConfig() *ResourceLifecycleConfig {
return a.generatedResourceConfig
}

// IsSensitive reports whether any path in v that has this path as a prefix should
// be treated as sensitive (redacted from plan output and deployment state).
func (a *Adapter) IsSensitive(path *structpath.PathNode) bool {
for _, r := range a.resourceConfig.SensitiveFields {
if path.HasPatternPrefix(r.Field) {
return true
}
}
return false
}

// FieldTriggersRecreate reports whether a local change to the field forces a
// delete + create. Both recreate_on_changes and provided_id_fields do this, so a
// caller that knows the ID is preserved can conclude the field is unchanged.
Expand Down
4 changes: 4 additions & 0 deletions bundle/direct/dresources/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ type ResourceLifecycleConfig struct {
// BackendDefaults: fields where the backend may set defaults.
// When old and new are nil but remote is set, and the remote value matches allowed values (if specified), the change is skipped.
BackendDefaults []BackendDefaultRule `yaml:"backend_defaults,omitempty"`

// SensitiveFields: fields that are sensitive and should not be logged.
SensitiveFields []FieldRule `yaml:"sensitive_fields,omitempty"`
}

// Config is the root configuration structure for resource lifecycle behavior.
Expand All @@ -100,6 +103,7 @@ var empty = ResourceLifecycleConfig{
UpdatableIDFields: nil,
NormalizeSlash: nil,
BackendDefaults: nil,
SensitiveFields: nil,
}

func mustParseConfig(data []byte) func() *Config {
Expand Down
Loading
Loading