-
Notifications
You must be signed in to change notification settings - Fork 207
Fix drift for UC secrets values #6192
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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, | ||
| # 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"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,18 @@ type ResourceSecret struct { | |
| client *databricks.WorkspaceClient | ||
| } | ||
|
|
||
| // SecretRemote is the remote read type for a UC secret. It embeds the SDK Secret | ||
| // struct and adds SecretValue, populated from EffectiveValue by DoRead, so that | ||
| // drift detection can compare the live secret value against the desired config value. | ||
| type SecretRemote struct { | ||
| catalog.Secret | ||
|
|
||
| // SecretValue mirrors EffectiveValue and is populated by DoRead when include_value=true. | ||
| // It uses the same field name as SecretState.SecretValue so RemapState can copy it directly. | ||
| // bundle:"sensitive" makes GetStructDiff include it despite json:"-", enabling drift detection. | ||
| SecretValue string `json:"-" bundle:"sensitive"` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why bother with this annotation
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the concern here? We need to annotate this field, and just being on cautious side I'd like to keep it sensitive and not mark "-". Not marking it sensitive will let framework skip it though |
||
| } | ||
|
|
||
| // SecretState is the persisted state type for a UC secret. It extends the SDK | ||
| // Secret struct with a Fingerprint field so that value changes can be detected | ||
| // across deploys without storing the plaintext value on disk. The Value field | ||
|
|
@@ -62,7 +74,7 @@ func (*ResourceSecret) PrepareState(input *resources.Secret) *SecretState { | |
| } | ||
| } | ||
|
|
||
| func (*ResourceSecret) RemapState(remote *catalog.Secret) *SecretState { | ||
| func (*ResourceSecret) RemapState(remote *SecretRemote) *SecretState { | ||
| return &SecretState{ | ||
| Secret: catalog.Secret{ | ||
| CatalogName: remote.CatalogName, | ||
|
|
@@ -82,12 +94,12 @@ func (*ResourceSecret) RemapState(remote *catalog.Secret) *SecretState { | |
| UpdatedBy: "", | ||
| ForceSendFields: utils.FilterFields[catalog.Secret](remote.ForceSendFields), | ||
| }, | ||
| SecretValue: remote.EffectiveValue, | ||
| SecretValue: remote.SecretValue, | ||
| } | ||
| } | ||
|
|
||
| // DoRead fetches the secret by full name. | ||
| func (r *ResourceSecret) DoRead(ctx context.Context, id string) (*catalog.Secret, error) { | ||
| func (r *ResourceSecret) DoRead(ctx context.Context, id string) (*SecretRemote, error) { | ||
| apiClient, err := client.New(r.client.Config) | ||
| if err != nil { | ||
| return nil, err | ||
|
|
@@ -101,26 +113,30 @@ func (r *ResourceSecret) DoRead(ctx context.Context, id string) (*catalog.Secret | |
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return &secret, nil | ||
| // Populate SecretValue from EffectiveValue so drift detection can compare | ||
| // the live secret value against the desired config value via RemapState. | ||
| return &SecretRemote{ | ||
| Secret: secret, | ||
| SecretValue: secret.EffectiveValue, | ||
| }, nil | ||
| } | ||
|
|
||
| // DoCreate creates a new UC secret. | ||
| func (r *ResourceSecret) DoCreate(ctx context.Context, state *SecretState) (string, *catalog.Secret, error) { | ||
| func (r *ResourceSecret) DoCreate(ctx context.Context, state *SecretState) (string, *SecretRemote, error) { | ||
| state.Value = state.SecretValue | ||
| response, err := r.client.SecretsUc.CreateSecret(ctx, catalog.CreateSecretRequest{ | ||
| Secret: state.Secret, | ||
| }) | ||
| // Clear the plaintext so it is not written to the state file. | ||
| // Fingerprint already captures whether the value changed. | ||
| state.Value = "" | ||
| if err != nil || response == nil { | ||
| return "", nil, err | ||
| } | ||
| return response.FullName, response, nil | ||
| return response.FullName, &SecretRemote{Secret: *response, SecretValue: state.SecretValue}, nil | ||
| } | ||
|
|
||
| // DoUpdate updates the secret in place and returns remote state. | ||
| func (r *ResourceSecret) DoUpdate(ctx context.Context, id string, state *SecretState, _ *PlanEntry) (*catalog.Secret, error) { | ||
| func (r *ResourceSecret) DoUpdate(ctx context.Context, id string, state *SecretState, _ *PlanEntry) (*SecretRemote, error) { | ||
| state.Value = state.SecretValue | ||
| response, err := r.client.SecretsUc.UpdateSecret(ctx, catalog.UpdateSecretRequest{ | ||
| FullName: id, | ||
|
|
@@ -134,7 +150,7 @@ func (r *ResourceSecret) DoUpdate(ctx context.Context, id string, state *SecretS | |
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return response, nil | ||
| return &SecretRemote{Secret: *response, SecretValue: state.SecretValue}, nil | ||
| } | ||
|
|
||
| // DoDelete deletes the secret. | ||
|
|
@@ -144,15 +160,28 @@ func (r *ResourceSecret) DoDelete(ctx context.Context, id string, _ *SecretState | |
| }) | ||
| } | ||
|
|
||
| // MarshalJSON serializes SecretRemote as a merged JSON object: the fields from | ||
| // catalog.Secret (via its own MarshalJSON) plus any SecretRemote-specific fields. | ||
| // Without this, the embedded catalog.Secret.MarshalJSON takes over and drops them. | ||
| func (s SecretRemote) MarshalJSON() ([]byte, error) { | ||
| return marshal.Marshal(s) | ||
| } | ||
|
|
||
| // UnmarshalJSON deserializes SecretRemote, restoring both the embedded | ||
| // catalog.Secret fields and any SecretRemote-specific fields. | ||
| func (s *SecretRemote) UnmarshalJSON(b []byte) error { | ||
| return marshal.Unmarshal(b, s) | ||
| } | ||
|
|
||
| // MarshalJSON serializes SecretState as a merged JSON object: the fields from | ||
| // catalog.Secret (via its own MarshalJSON) plus "fingerprint". Without this, | ||
| // the embedded catalog.Secret.MarshalJSON takes over and drops Fingerprint. | ||
| // catalog.Secret (via its own MarshalJSON) plus SecretState-specific fields. | ||
| // Without this, the embedded catalog.Secret.MarshalJSON takes over and drops them. | ||
| func (s SecretState) MarshalJSON() ([]byte, error) { | ||
| return marshal.Marshal(s) | ||
| } | ||
|
|
||
| // UnmarshalJSON deserializes SecretState, restoring both the embedded | ||
| // catalog.Secret fields and Fingerprint. | ||
| // catalog.Secret fields and SecretState-specific fields. | ||
| func (s *SecretState) UnmarshalJSON(b []byte) error { | ||
| return marshal.Unmarshal(b, s) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should still support --plan though, by requiring the same variable be set during deploy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I want to make it as a follow up, it neends a bit of designing as there is no precedence of it yet