diff --git a/docs/data-sources/device.md b/docs/data-sources/device.md index 335751a8..835d4905 100644 --- a/docs/data-sources/device.md +++ b/docs/data-sources/device.md @@ -36,7 +36,21 @@ data "tailscale_device" "sample_device2" { ### Read-Only - `addresses` (List of String) The list of device's IPs +- `authorized` (Boolean) Whether the device is authorized to access the tailnet +- `blocks_incoming_connections` (Boolean) Whether the device blocks incoming connections +- `client_version` (String) The Tailscale client version running on the device +- `created` (String) The creation time of the device +- `expires` (String) The expiry time of the device's key - `id` (String) The ID of this resource. +- `is_external` (Boolean) Whether the device is marked as external +- `key_expiry_disabled` (Boolean) Whether the device's key expiry is disabled +- `last_seen` (String) The last seen time of the device +- `machine_key` (String) The machine key of the device - `node_id` (String) The preferred indentifier for a device. +- `node_key` (String) The node key of the device +- `os` (String) The operating system of the device - `tags` (Set of String) The tags applied to the device +- `tailnet_lock_error` (String) The tailnet lock error for the device, if any +- `tailnet_lock_key` (String) The tailnet lock key for the device, if any +- `update_available` (Boolean) Whether an update is available for the device - `user` (String) The user associated with the device diff --git a/docs/data-sources/devices.md b/docs/data-sources/devices.md index 369f2a66..8354f14a 100644 --- a/docs/data-sources/devices.md +++ b/docs/data-sources/devices.md @@ -36,9 +36,23 @@ data "tailscale_devices" "sample_devices" { Read-Only: - `addresses` (List of String) +- `authorized` (Boolean) +- `blocks_incoming_connections` (Boolean) +- `client_version` (String) +- `created` (String) +- `expires` (String) - `hostname` (String) - `id` (String) +- `is_external` (Boolean) +- `key_expiry_disabled` (Boolean) +- `last_seen` (String) +- `machine_key` (String) - `name` (String) - `node_id` (String) +- `node_key` (String) +- `os` (String) - `tags` (Set of String) +- `tailnet_lock_error` (String) +- `tailnet_lock_key` (String) +- `update_available` (Boolean) - `user` (String) diff --git a/tailscale/data_source_device.go b/tailscale/data_source_device.go index 9706dcca..dcd51395 100644 --- a/tailscale/data_source_device.go +++ b/tailscale/data_source_device.go @@ -58,6 +58,76 @@ func dataSourceDevice() *schema.Resource { Type: schema.TypeString, }, }, + "authorized": { + Type: schema.TypeBool, + Description: "Whether the device is authorized to access the tailnet", + Computed: true, + }, + "key_expiry_disabled": { + Type: schema.TypeBool, + Description: "Whether the device's key expiry is disabled", + Computed: true, + }, + "blocks_incoming_connections": { + Type: schema.TypeBool, + Description: "Whether the device blocks incoming connections", + Computed: true, + }, + "client_version": { + Type: schema.TypeString, + Description: "The Tailscale client version running on the device", + Computed: true, + }, + "created": { + Type: schema.TypeString, + Description: "The creation time of the device", + Computed: true, + }, + "expires": { + Type: schema.TypeString, + Description: "The expiry time of the device's key", + Computed: true, + }, + "is_external": { + Type: schema.TypeBool, + Description: "Whether the device is marked as external", + Computed: true, + }, + "last_seen": { + Type: schema.TypeString, + Description: "The last seen time of the device", + Computed: true, + }, + "machine_key": { + Type: schema.TypeString, + Description: "The machine key of the device", + Computed: true, + }, + "node_key": { + Type: schema.TypeString, + Description: "The node key of the device", + Computed: true, + }, + "os": { + Type: schema.TypeString, + Description: "The operating system of the device", + Computed: true, + }, + "update_available": { + Type: schema.TypeBool, + Description: "Whether an update is available for the device", + Computed: true, + }, + "tailnet_lock_error": { + Type: schema.TypeString, + Description: "The tailnet lock error for the device, if any", + Computed: true, + }, + "tailnet_lock_key": { + Type: schema.TypeString, + Description: "The tailnet lock key for the device, if any", + Computed: true, + }, "wait_for": { Type: schema.TypeString, Description: "If specified, the provider will make multiple attempts to obtain the data source until the wait_for duration is reached. Retries are made every second so this value should be greater than 1s", @@ -123,12 +193,33 @@ func dataSourceDeviceRead(ctx context.Context, d *schema.ResourceData, m interfa // resource in Terraform. This omits the "id" which is expected to be set // using [schema.ResourceData.SetId]. func deviceToMap(device *tailscale.Device) map[string]any { + var lastSeen string + if device.LastSeen == nil { + lastSeen = "" + } else { + lastSeen = device.LastSeen.Format(time.RFC3339) + } + return map[string]any{ - "name": device.Name, - "hostname": device.Hostname, - "user": device.User, - "node_id": device.NodeID, - "addresses": device.Addresses, - "tags": device.Tags, + "name": device.Name, + "hostname": device.Hostname, + "user": device.User, + "node_id": device.NodeID, + "addresses": device.Addresses, + "tags": device.Tags, + "authorized": device.Authorized, + "key_expiry_disabled": device.KeyExpiryDisabled, + "blocks_incoming_connections": device.BlocksIncomingConnections, + "client_version": device.ClientVersion, + "created": device.Created.Format(time.RFC3339), + "expires": device.Expires.Format(time.RFC3339), + "is_external": device.IsExternal, + "last_seen": lastSeen, + "machine_key": device.MachineKey, + "node_key": device.NodeKey, + "os": device.OS, + "update_available": device.UpdateAvailable, + "tailnet_lock_error": device.TailnetLockError, + "tailnet_lock_key": device.TailnetLockKey, } } diff --git a/tailscale/data_source_device_test.go b/tailscale/data_source_device_test.go new file mode 100644 index 00000000..ec709ead --- /dev/null +++ b/tailscale/data_source_device_test.go @@ -0,0 +1,119 @@ +// Copyright (c) David Bond, Tailscale Inc, & Contributors +// SPDX-License-Identifier: MIT + +package tailscale + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + tsclient "tailscale.com/client/tailscale/v2" + "tailscale.com/tstest" +) + +func TestDeviceToMap(t *testing.T) { + t.Parallel() + cl := tstest.NewClock(tstest.ClockOpts{}) + created := tsclient.Time{Time: cl.Now().Truncate(time.Second)} + expires := tsclient.Time{Time: cl.Now().Truncate(time.Second).Add(24 * time.Hour)} + lastSeen := tsclient.Time{Time: cl.Now().Truncate(time.Second).Add(-5 * time.Minute)} + + dev := &tsclient.Device{ + Name: "host.example.ts.net", + Hostname: "host", + User: "user@example.com", + NodeID: "node-123", + Addresses: []string{"100.100.100.101", "fd7a:115c:a1e0::1"}, + Tags: []string{"tag:test1", "tag:test2"}, + Authorized: true, + KeyExpiryDisabled: true, + BlocksIncomingConnections: true, + ClientVersion: "1.88.4", + Created: created, + Expires: expires, + IsExternal: false, + LastSeen: &lastSeen, + MachineKey: "machine-key", + NodeKey: "node-key", + OS: "linux", + UpdateAvailable: true, + TailnetLockError: "lock-error", + TailnetLockKey: "lock-key", + } + + m := deviceToMap(dev) + + assert.Equal(t, dev.Name, m["name"].(string)) + assert.Equal(t, dev.Hostname, m["hostname"].(string)) + assert.Equal(t, dev.User, m["user"].(string)) + assert.Equal(t, dev.NodeID, m["node_id"].(string)) + assert.Equal(t, dev.Addresses, m["addresses"].([]string)) + assert.Equal(t, dev.Tags, m["tags"].([]string)) + assert.Equal(t, dev.Authorized, m["authorized"].(bool)) + assert.Equal(t, dev.KeyExpiryDisabled, m["key_expiry_disabled"].(bool)) + assert.Equal(t, dev.BlocksIncomingConnections, m["blocks_incoming_connections"].(bool)) + assert.Equal(t, dev.ClientVersion, m["client_version"].(string)) + assert.Equal(t, created.Format(time.RFC3339), m["created"].(string)) + assert.Equal(t, expires.Format(time.RFC3339), m["expires"].(string)) + assert.Equal(t, dev.IsExternal, m["is_external"].(bool)) + assert.Equal(t, lastSeen.Format(time.RFC3339), m["last_seen"].(string)) + assert.Equal(t, dev.MachineKey, m["machine_key"].(string)) + assert.Equal(t, dev.NodeKey, m["node_key"].(string)) + assert.Equal(t, dev.OS, m["os"].(string)) + assert.Equal(t, dev.UpdateAvailable, m["update_available"].(bool)) + assert.Equal(t, dev.TailnetLockError, m["tailnet_lock_error"].(string)) + assert.Equal(t, dev.TailnetLockKey, m["tailnet_lock_key"].(string)) +} +func TestDeviceToMap_LastSeenNil(t *testing.T) { + t.Parallel() + cl := tstest.NewClock(tstest.ClockOpts{}) + created := tsclient.Time{Time: cl.Now().Truncate(time.Second)} + expires := tsclient.Time{Time: cl.Now().Truncate(time.Second).Add(24 * time.Hour)} + + dev := &tsclient.Device{ + Name: "host.example.ts.net", + Hostname: "host", + User: "user@example.com", + NodeID: "node-123", + Addresses: []string{"100.100.100.101", "fd7a:115c:a1e0::1"}, + Tags: []string{"tag:test1", "tag:test2"}, + Authorized: true, + KeyExpiryDisabled: true, + BlocksIncomingConnections: true, + ClientVersion: "1.88.4", + Created: created, + Expires: expires, + IsExternal: false, + LastSeen: nil, + MachineKey: "machine-key", + NodeKey: "node-key", + OS: "linux", + UpdateAvailable: true, + TailnetLockError: "lock-error", + TailnetLockKey: "lock-key", + } + + m := deviceToMap(dev) + + assert.Equal(t, dev.Name, m["name"].(string)) + assert.Equal(t, dev.Hostname, m["hostname"].(string)) + assert.Equal(t, dev.User, m["user"].(string)) + assert.Equal(t, dev.NodeID, m["node_id"].(string)) + assert.Equal(t, dev.Addresses, m["addresses"].([]string)) + assert.Equal(t, dev.Tags, m["tags"].([]string)) + assert.Equal(t, dev.Authorized, m["authorized"].(bool)) + assert.Equal(t, dev.KeyExpiryDisabled, m["key_expiry_disabled"].(bool)) + assert.Equal(t, dev.BlocksIncomingConnections, m["blocks_incoming_connections"].(bool)) + assert.Equal(t, dev.ClientVersion, m["client_version"].(string)) + assert.Equal(t, created.Format(time.RFC3339), m["created"].(string)) + assert.Equal(t, expires.Format(time.RFC3339), m["expires"].(string)) + assert.Equal(t, dev.IsExternal, m["is_external"].(bool)) + assert.Equal(t, "", m["last_seen"]) // Expect empty string for nil LastSeen + assert.Equal(t, dev.MachineKey, m["machine_key"].(string)) + assert.Equal(t, dev.NodeKey, m["node_key"].(string)) + assert.Equal(t, dev.OS, m["os"].(string)) + assert.Equal(t, dev.UpdateAvailable, m["update_available"].(bool)) + assert.Equal(t, dev.TailnetLockError, m["tailnet_lock_error"].(string)) + assert.Equal(t, dev.TailnetLockKey, m["tailnet_lock_key"].(string)) +} diff --git a/tailscale/data_source_devices.go b/tailscale/data_source_devices.go index 7737bfc7..d3e403e7 100644 --- a/tailscale/data_source_devices.go +++ b/tailscale/data_source_devices.go @@ -70,6 +70,76 @@ func dataSourceDevices() *schema.Resource { Type: schema.TypeString, }, }, + "authorized": { + Type: schema.TypeBool, + Description: "Whether the device is authorized to access the tailnet", + Computed: true, + }, + "key_expiry_disabled": { + Type: schema.TypeBool, + Description: "Whether the device's key expiry is disabled", + Computed: true, + }, + "blocks_incoming_connections": { + Type: schema.TypeBool, + Description: "Whether the device blocks incoming connections", + Computed: true, + }, + "client_version": { + Type: schema.TypeString, + Description: "The Tailscale client version running on the device", + Computed: true, + }, + "created": { + Type: schema.TypeString, + Description: "The creation time of the device", + Computed: true, + }, + "expires": { + Type: schema.TypeString, + Description: "The expiry time of the device's key", + Computed: true, + }, + "is_external": { + Type: schema.TypeBool, + Description: "Whether the device is marked as external", + Computed: true, + }, + "last_seen": { + Type: schema.TypeString, + Description: "The last seen time of the device", + Computed: true, + }, + "machine_key": { + Type: schema.TypeString, + Description: "The machine key of the device", + Computed: true, + }, + "node_key": { + Type: schema.TypeString, + Description: "The node key of the device", + Computed: true, + }, + "os": { + Type: schema.TypeString, + Description: "The operating system of the device", + Computed: true, + }, + "update_available": { + Type: schema.TypeBool, + Description: "Whether an update is available for the device", + Computed: true, + }, + "tailnet_lock_error": { + Type: schema.TypeString, + Description: "The tailnet lock error for the device, if any", + Computed: true, + }, + "tailnet_lock_key": { + Type: schema.TypeString, + Description: "The tailnet lock key for the device, if any", + Computed: true, + }, }, }, },