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
6 changes: 5 additions & 1 deletion tailscale/resource_federated_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,11 @@ func (r *federatedIdentityResource) populateFromKey(ctx context.Context, data *f
var diags diag.Diagnostics

data.ID = types.StringValue(key.ID)
data.Description = CoalesceStringEmptyOrNull(data.Description, key.Description)
// description has a schema Default of "", so represent an empty description as
// "" 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!

data.Audience = types.StringValue(key.Audience)
data.Subject = types.StringValue(key.Subject)
data.Issuer = types.StringValue(key.Issuer)
Expand Down
38 changes: 38 additions & 0 deletions tailscale/resource_federated_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,44 @@ func TestProvider_TailscaleFederatedIdentity_ReservedCustomClaimKeys(t *testing.
}
}

// TestProvider_TailscaleFederatedIdentity_ImportEmptyDescription is a regression
// test for a federated identity with no description. The description attribute has
// a Default of "", but Read null-ified an empty description, leaving a permanent
// null-vs-"" mismatch that showed up as a spurious in-place update on import.
func TestProvider_TailscaleFederatedIdentity_ImportEmptyDescription(t *testing.T) {
const testFederatedIdentityNoDescription = `
resource "tailscale_federated_identity" "example_federated_identity" {
scopes = ["auth_keys"]
issuer = "https://example.com"
subject = "example-sub-*"
}`

resource.Test(t, resource.TestCase{
IsUnitTest: true,
PreCheck: func() {
testServer.ResponseCode = http.StatusOK
testServer.ResponseBody = tailscale.Key{
ID: "test",
Scopes: []string{"auth_keys"},
Issuer: "https://example.com",
Subject: "example-sub-*",
// Description intentionally left empty.
}
},
ProtoV5ProviderFactories: testProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testFederatedIdentityNoDescription,
},
{
ResourceName: "tailscale_federated_identity.example_federated_identity",
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccTailscaleFederatedIdentity(t *testing.T) {
const resourceName = "tailscale_federated_identity.test_federated_identity"

Expand Down
10 changes: 7 additions & 3 deletions tailscale/resource_posture_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,13 @@ func (p *postureIntegrationResource) Read(ctx context.Context, req resource.Read

state.ID = types.StringValue(integration.ID)
state.PostureProvider = types.StringValue(string(integration.Provider))
state.CloudID = CoalesceStringEmptyOrNull(state.CloudID, integration.CloudID)
state.ClientID = CoalesceStringEmptyOrNull(state.ClientID, integration.ClientID)
state.TenantID = CoalesceStringEmptyOrNull(state.TenantID, integration.TenantID)
// cloud_id / client_id / tenant_id all have a schema Default of "", so represent
// 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!

state.ClientID = types.StringValue(integration.ClientID)
state.TenantID = types.StringValue(integration.TenantID)

diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
Expand Down
Loading