Skip to content

resource_tailscale_oauth_client: add import support #519

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

Merged
merged 1 commit into from
Jun 16, 2025
Merged
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
9 changes: 9 additions & 0 deletions docs/resources/oauth_client.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,12 @@ resource "tailscale_oauth_client" "sample_client" {
- `id` (String) The client ID, also known as the key id. Used with the client secret to generate access tokens.
- `key` (String, Sensitive) The client secret, also known as the key. Used with the client ID to generate access tokens.
- `user_id` (String) ID of the user who created this key, empty for OAuth clients created by other OAuth clients.

## Import

Import is supported using the following syntax:

```shell
# Note: Sensitive fields such as the secret key are not returned by the API and will be unset in the Terraform state after import.
terraform import tailscale_oauth_client.example k1234511CNTRL
```
2 changes: 2 additions & 0 deletions examples/resources/tailscale_oauth_client/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Note: Sensitive fields such as the secret key are not returned by the API and will be unset in the Terraform state after import.
terraform import tailscale_oauth_client.example k1234511CNTRL
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
golang.org/x/tools v0.34.0
tailscale.com v1.84.2
tailscale.com/client/tailscale/v2 v2.0.0-20250602205246-d51fc603f5ea
tailscale.com/client/tailscale/v2 v2.0.0-20250616133344-8dcb33eb281b
)

require github.com/pkg/errors v0.9.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -310,5 +310,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tailscale.com v1.84.2 h1:v6aM4RWUgYiV52LRAx6ET+dlGnvO/5lnqPXb7/pMnR0=
tailscale.com v1.84.2/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo=
tailscale.com/client/tailscale/v2 v2.0.0-20250602205246-d51fc603f5ea h1:lXgaPz+scY0fqkoXfy6TpX9lpP4+dRa3Sv+YHQujFOk=
tailscale.com/client/tailscale/v2 v2.0.0-20250602205246-d51fc603f5ea/go.mod h1:nzqx3Hs59z2W8Gnmq2ChavPButcyvtxAxRpNc+ZVy7s=
tailscale.com/client/tailscale/v2 v2.0.0-20250616133344-8dcb33eb281b h1:Cyso/184f0BUfEDUWVmM65t6byUEB8wuTBGpB8Tv1PA=
tailscale.com/client/tailscale/v2 v2.0.0-20250616133344-8dcb33eb281b/go.mod h1:nzqx3Hs59z2W8Gnmq2ChavPButcyvtxAxRpNc+ZVy7s=
12 changes: 11 additions & 1 deletion tailscale/resource_oauth_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ func resourceOAuthClient() *schema.Resource {
CreateContext: resourceOAuthClientCreate,
DeleteContext: resourceOAuthClientDelete,
UpdateContext: nil,
// Importer: &schema.ResourceImporter{StateContext: schema.ImportStatePassthroughContext}, no import support - the key is not returned by the API so it'd serve no purpose
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"description": {
Type: schema.TypeString,
Expand Down Expand Up @@ -90,6 +92,14 @@ func resourceOAuthClientRead(ctx context.Context, d *schema.ResourceData, m inte
return diagnosticsError(err, "Failed to set description")
}

if err = d.Set("scopes", key.Scopes); err != nil {
return diagnosticsError(err, "Failed to set 'scopes'")
}

if err = d.Set("tags", key.Tags); err != nil {
return diagnosticsError(err, "Failed to set 'tags'")
}

if err = d.Set("created_at", key.Created.Format(time.RFC3339)); err != nil {
return diagnosticsError(err, "Failed to set created_at")
}
Expand Down
10 changes: 10 additions & 0 deletions tailscale/resource_oauth_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,14 @@ func TestAccTailscaleOAuthClient(t *testing.T) {
var expectedOAuthClientCreated tailscale.Key
expectedOAuthClientCreated.Description = "Test client"
expectedOAuthClientCreated.KeyType = "client"
expectedOAuthClientCreated.Scopes = []string{"auth_keys", "devices:core"}
expectedOAuthClientCreated.Tags = []string{"tag:test"}

var expectedOAuthClientUpdated tailscale.Key
expectedOAuthClientUpdated.Description = "Updated description"
expectedOAuthClientUpdated.KeyType = "client"
expectedOAuthClientUpdated.Scopes = []string{"auth_keys:read"}
expectedOAuthClientUpdated.Tags = nil

checkProperties := func(expected *tailscale.Key) func(client *tailscale.Client, rs *terraform.ResourceState) error {
return func(client *tailscale.Client, rs *terraform.ResourceState) error {
Expand Down Expand Up @@ -153,6 +157,12 @@ func TestAccTailscaleOAuthClient(t *testing.T) {
resource.TestCheckResourceAttrSet(resourceName, "user_id"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"key"}, // sensitive material not returned by the API
},
},
})
}
4 changes: 4 additions & 0 deletions tailscale/resource_tailnet_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ func resourceTailnetKeyRead(ctx context.Context, d *schema.ResourceData, m inter
return diagnosticsError(err, "Failed to set ephemeral")
}

if err = d.Set("expiry", key.ExpirySeconds); err != nil {
return diagnosticsError(err, "Failed to set expiry")
}

if err = d.Set("created_at", key.Created.Format(time.RFC3339)); err != nil {
return diagnosticsError(err, "Failed to set created_at")
}
Expand Down
19 changes: 13 additions & 6 deletions tailscale/resource_tailnet_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,12 @@ func testTailnetKeyStruct(reusable bool) tailscale.Key {
}`), &keyCapabilities)
keyCapabilities.Devices.Create.Reusable = reusable
return tailscale.Key{
ID: "test",
KeyType: "auth",
Key: "thisisatestkey",
Description: "Example key",
Capabilities: keyCapabilities,
ID: "test",
KeyType: "auth",
Key: "thisisatestkey",
Description: "Example key",
ExpirySeconds: toPtr(time.Duration(3600)),
Capabilities: keyCapabilities,
}
}

Expand Down Expand Up @@ -230,6 +231,7 @@ func TestAccTailscaleTailnetKey(t *testing.T) {
var expectedKey tailscale.Key
expectedKey.KeyType = "auth"
expectedKey.Description = "Test key"
expectedKey.ExpirySeconds = toPtr(time.Duration(3600))
expectedKey.Capabilities.Devices.Create.Reusable = true
expectedKey.Capabilities.Devices.Create.Ephemeral = true
expectedKey.Capabilities.Devices.Create.Preauthorized = true
Expand All @@ -238,6 +240,7 @@ func TestAccTailscaleTailnetKey(t *testing.T) {
var expectedKeyUpdated tailscale.Key
expectedKeyUpdated.KeyType = "auth"
expectedKeyUpdated.Description = "Test key changed"
expectedKeyUpdated.ExpirySeconds = toPtr(time.Duration(7200))
expectedKeyUpdated.Capabilities.Devices.Create.Reusable = false
expectedKeyUpdated.Capabilities.Devices.Create.Ephemeral = false
expectedKeyUpdated.Capabilities.Devices.Create.Preauthorized = false
Expand Down Expand Up @@ -293,8 +296,12 @@ func TestAccTailscaleTailnetKey(t *testing.T) {
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"key", "expiry"},
ImportStateVerifyIgnore: []string{"key"}, // sensitive material not returned by the API
},
},
})
}

func toPtr[T any](v T) *T {
return &v
}