Skip to content

Commit 06f8f32

Browse files
authored
resource_tailscale_oauth_client: add import support (#519)
resource_tailscale_key: handle expiry on imports to avoid systematic recreates Fixes #515 Signed-off-by: mcoulombe <[email protected]>
1 parent e900873 commit 06f8f32

File tree

8 files changed

+52
-10
lines changed

8 files changed

+52
-10
lines changed

docs/resources/oauth_client.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,12 @@ resource "tailscale_oauth_client" "sample_client" {
3838
- `id` (String) The client ID, also known as the key id. Used with the client secret to generate access tokens.
3939
- `key` (String, Sensitive) The client secret, also known as the key. Used with the client ID to generate access tokens.
4040
- `user_id` (String) ID of the user who created this key, empty for OAuth clients created by other OAuth clients.
41+
42+
## Import
43+
44+
Import is supported using the following syntax:
45+
46+
```shell
47+
# Note: Sensitive fields such as the secret key are not returned by the API and will be unset in the Terraform state after import.
48+
terraform import tailscale_oauth_client.example k1234511CNTRL
49+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Note: Sensitive fields such as the secret key are not returned by the API and will be unset in the Terraform state after import.
2+
terraform import tailscale_oauth_client.example k1234511CNTRL

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
1313
golang.org/x/tools v0.34.0
1414
tailscale.com v1.84.2
15-
tailscale.com/client/tailscale/v2 v2.0.0-20250602205246-d51fc603f5ea
15+
tailscale.com/client/tailscale/v2 v2.0.0-20250616133344-8dcb33eb281b
1616
)
1717

1818
require github.com/pkg/errors v0.9.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,5 +310,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
310310
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
311311
tailscale.com v1.84.2 h1:v6aM4RWUgYiV52LRAx6ET+dlGnvO/5lnqPXb7/pMnR0=
312312
tailscale.com v1.84.2/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo=
313-
tailscale.com/client/tailscale/v2 v2.0.0-20250602205246-d51fc603f5ea h1:lXgaPz+scY0fqkoXfy6TpX9lpP4+dRa3Sv+YHQujFOk=
314-
tailscale.com/client/tailscale/v2 v2.0.0-20250602205246-d51fc603f5ea/go.mod h1:nzqx3Hs59z2W8Gnmq2ChavPButcyvtxAxRpNc+ZVy7s=
313+
tailscale.com/client/tailscale/v2 v2.0.0-20250616133344-8dcb33eb281b h1:Cyso/184f0BUfEDUWVmM65t6byUEB8wuTBGpB8Tv1PA=
314+
tailscale.com/client/tailscale/v2 v2.0.0-20250616133344-8dcb33eb281b/go.mod h1:nzqx3Hs59z2W8Gnmq2ChavPButcyvtxAxRpNc+ZVy7s=

tailscale/resource_oauth_client.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ func resourceOAuthClient() *schema.Resource {
2121
CreateContext: resourceOAuthClientCreate,
2222
DeleteContext: resourceOAuthClientDelete,
2323
UpdateContext: nil,
24-
// Importer: &schema.ResourceImporter{StateContext: schema.ImportStatePassthroughContext}, no import support - the key is not returned by the API so it'd serve no purpose
24+
Importer: &schema.ResourceImporter{
25+
StateContext: schema.ImportStatePassthroughContext,
26+
},
2527
Schema: map[string]*schema.Schema{
2628
"description": {
2729
Type: schema.TypeString,
@@ -90,6 +92,14 @@ func resourceOAuthClientRead(ctx context.Context, d *schema.ResourceData, m inte
9092
return diagnosticsError(err, "Failed to set description")
9193
}
9294

95+
if err = d.Set("scopes", key.Scopes); err != nil {
96+
return diagnosticsError(err, "Failed to set 'scopes'")
97+
}
98+
99+
if err = d.Set("tags", key.Tags); err != nil {
100+
return diagnosticsError(err, "Failed to set 'tags'")
101+
}
102+
93103
if err = d.Set("created_at", key.Created.Format(time.RFC3339)); err != nil {
94104
return diagnosticsError(err, "Failed to set created_at")
95105
}

tailscale/resource_oauth_client_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,14 @@ func TestAccTailscaleOAuthClient(t *testing.T) {
6060
var expectedOAuthClientCreated tailscale.Key
6161
expectedOAuthClientCreated.Description = "Test client"
6262
expectedOAuthClientCreated.KeyType = "client"
63+
expectedOAuthClientCreated.Scopes = []string{"auth_keys", "devices:core"}
64+
expectedOAuthClientCreated.Tags = []string{"tag:test"}
6365

6466
var expectedOAuthClientUpdated tailscale.Key
6567
expectedOAuthClientUpdated.Description = "Updated description"
6668
expectedOAuthClientUpdated.KeyType = "client"
69+
expectedOAuthClientUpdated.Scopes = []string{"auth_keys:read"}
70+
expectedOAuthClientUpdated.Tags = nil
6771

6872
checkProperties := func(expected *tailscale.Key) func(client *tailscale.Client, rs *terraform.ResourceState) error {
6973
return func(client *tailscale.Client, rs *terraform.ResourceState) error {
@@ -153,6 +157,12 @@ func TestAccTailscaleOAuthClient(t *testing.T) {
153157
resource.TestCheckResourceAttrSet(resourceName, "user_id"),
154158
),
155159
},
160+
{
161+
ResourceName: resourceName,
162+
ImportState: true,
163+
ImportStateVerify: true,
164+
ImportStateVerifyIgnore: []string{"key"}, // sensitive material not returned by the API
165+
},
156166
},
157167
})
158168
}

tailscale/resource_tailnet_key.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ func resourceTailnetKeyRead(ctx context.Context, d *schema.ResourceData, m inter
255255
return diagnosticsError(err, "Failed to set ephemeral")
256256
}
257257

258+
if err = d.Set("expiry", key.ExpirySeconds); err != nil {
259+
return diagnosticsError(err, "Failed to set expiry")
260+
}
261+
258262
if err = d.Set("created_at", key.Created.Format(time.RFC3339)); err != nil {
259263
return diagnosticsError(err, "Failed to set created_at")
260264
}

tailscale/resource_tailnet_key_test.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,12 @@ func testTailnetKeyStruct(reusable bool) tailscale.Key {
6464
}`), &keyCapabilities)
6565
keyCapabilities.Devices.Create.Reusable = reusable
6666
return tailscale.Key{
67-
ID: "test",
68-
KeyType: "auth",
69-
Key: "thisisatestkey",
70-
Description: "Example key",
71-
Capabilities: keyCapabilities,
67+
ID: "test",
68+
KeyType: "auth",
69+
Key: "thisisatestkey",
70+
Description: "Example key",
71+
ExpirySeconds: toPtr(time.Duration(3600)),
72+
Capabilities: keyCapabilities,
7273
}
7374
}
7475

@@ -230,6 +231,7 @@ func TestAccTailscaleTailnetKey(t *testing.T) {
230231
var expectedKey tailscale.Key
231232
expectedKey.KeyType = "auth"
232233
expectedKey.Description = "Test key"
234+
expectedKey.ExpirySeconds = toPtr(time.Duration(3600))
233235
expectedKey.Capabilities.Devices.Create.Reusable = true
234236
expectedKey.Capabilities.Devices.Create.Ephemeral = true
235237
expectedKey.Capabilities.Devices.Create.Preauthorized = true
@@ -238,6 +240,7 @@ func TestAccTailscaleTailnetKey(t *testing.T) {
238240
var expectedKeyUpdated tailscale.Key
239241
expectedKeyUpdated.KeyType = "auth"
240242
expectedKeyUpdated.Description = "Test key changed"
243+
expectedKeyUpdated.ExpirySeconds = toPtr(time.Duration(7200))
241244
expectedKeyUpdated.Capabilities.Devices.Create.Reusable = false
242245
expectedKeyUpdated.Capabilities.Devices.Create.Ephemeral = false
243246
expectedKeyUpdated.Capabilities.Devices.Create.Preauthorized = false
@@ -293,8 +296,12 @@ func TestAccTailscaleTailnetKey(t *testing.T) {
293296
ResourceName: resourceName,
294297
ImportState: true,
295298
ImportStateVerify: true,
296-
ImportStateVerifyIgnore: []string{"key", "expiry"},
299+
ImportStateVerifyIgnore: []string{"key"}, // sensitive material not returned by the API
297300
},
298301
},
299302
})
300303
}
304+
305+
func toPtr[T any](v T) *T {
306+
return &v
307+
}

0 commit comments

Comments
 (0)