Skip to content

tailscale_federated_identity, tailscale_posture_integration: import empty string fields cleanly - #791

Open
keeleysam wants to merge 1 commit into
tailscale:mainfrom
keeleysam:federated-identity-import-empty-description
Open

tailscale_federated_identity, tailscale_posture_integration: import empty string fields cleanly#791
keeleysam wants to merge 1 commit into
tailscale:mainfrom
keeleysam:federated-identity-import-empty-description

Conversation

@keeleysam

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

Importing an existing tailscale_federated_identity that has no description shows a spurious in-place update on the first plan. description is Optional+Computed with a schema Default of "", but its Read path runs the API value through CoalesceStringEmptyOrNull, which returns a null StringValue when the value is empty. On import the prior model value is null, so an empty description reads back as null while the default plans "". That leaves a permanent null-vs-"" mismatch, which Terraform renders as an update. Because it plans an update, the computed updated_at also goes (known after apply), so importing a batch of identities that never had descriptions looks like they are all changing when nothing actually is.

tailscale_posture_integration has the same shape and the same bug on cloud_id, client_id, and tenant_id (all Optional+Computed with Default "", all read through CoalesceStringEmptyOrNull).

The fix sets these with types.StringValue so an empty value stays "", matching the schema default, and imports cleanly. StringValueNullIfEmpty is unchanged and still used for the fields that are genuinely null-when-empty and have no default (logstream s3_authentication_type, webhook provider_type).

Which issue this PR fixes (use fixes #<issue number>(, fixes #<issue_number>, ...) format, will close that issue when PR gets merged):

Fixes #790

Special notes for your reviewer:

  • Added a unit test that imports a tailscale_federated_identity with no description and asserts the imported state matches (it fails without the fix on ImportStateVerify, passes with it). The posture case is already covered by TestAccTailscalePostureIntegration, which imports a Falcon integration with no tenant_id and runs ImportStateVerify.
  • tailscale_oauth_client's description is on the SDKv2 code path, which does not exhibit this, so it is untouched.
  • After this change CoalesceStringEmptyOrNull has no remaining callers. I left it in place since it is the correct helper for its documented pattern (a null-when-empty field with no default); its two Default: "" call sites were the wrong use. Happy to drop it if you would rather not carry an unused helper.
  • Interim workaround for anyone hitting this before a release: set a non-empty description (and apply) before importing, so the field is non-empty on both sides and imports clean.
  • go test ./tailscale/, go build, go vet, and gofmt are all clean.

optional+computed string fields as "" instead of null

The `description` attribute on tailscale_federated_identity and the
`cloud_id`, `client_id`, and `tenant_id` attributes on
tailscale_posture_integration are Optional+Computed with a schema Default
of "". Their Read paths ran the API value through CoalesceStringEmptyOrNull,
which returns a null StringValue when the value is empty. On import the
prior model value is null, so an empty field reads back as null while the
schema default plans "", leaving a permanent null-vs-"" mismatch that
Terraform renders as a spurious in-place update. For federated identities
that update also flips the computed updated_at to "(known after apply)",
so importing an existing identity that never had a description shows a
noisy diff.

Set these fields with types.StringValue directly, so an empty value is
represented as "", consistent with the schema Default, and imports
cleanly. Add a unit test that imports a federated identity with no
description and asserts the imported state matches. The existing
tailscale_posture_integration acceptance test already imports a falcon
integration with no tenant_id and verifies state, so it covers the posture
case.

tailscale_oauth_client's description is SDKv2, which does not exhibit this
behaviour, so it is left unchanged.

Signed-off-by: Samuel Keeley <samuel@keeley.net>
// "" rather than null. Otherwise an empty description reads back as null on
// import, leaving a permanent null-vs-"" mismatch that plans as a spurious
// in-place update (which also flips the computed updated_at to known-after-apply).
data.Description = types.StringValue(key.Description)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can drop the leading comment here and leave the comment in the regression test since this won't be a special case after this fix!

// empty values as "" rather than null. Otherwise an empty value reads back as
// null on import, leaving a permanent null-vs-"" mismatch (a spurious in-place
// update).
state.CloudID = types.StringValue(integration.CloudID)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same RE: the leading comment here!

@mpminardi

Copy link
Copy Markdown
Member

Thank you for the PR @keeleysam ! And apologies for this having slipped through.

After this change CoalesceStringEmptyOrNull has no remaining callers. I left it in place since it is the correct helper for its documented pattern (a null-when-empty field with no default); its two Default: "" call sites were the wrong use. Happy to drop it if you would rather not carry an unused helper.

I think we can drop the helper at this point! We've standardized instead on providing a default value and the helper is now vestigial.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Importing federated_identity or posture_integration with empty string fields plans a spurious update

2 participants