Skip to content

Commit

Permalink
tailscale: use V2 client for device and devices data sources
Browse files Browse the repository at this point in the history
Updates tailscale/corp#21867

Signed-off-by: Percy Wegmann <[email protected]>
  • Loading branch information
oxtoacart committed Aug 23, 2024
1 parent 67c8f6f commit 548482c
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 37 deletions.
46 changes: 19 additions & 27 deletions tailscale/data_source_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/tailscale/tailscale-client-go/tailscale"
tsclient "github.com/tailscale/tailscale-client-go/v2"
)

func dataSourceDevice() *schema.Resource {
Expand Down Expand Up @@ -71,31 +71,31 @@ func dataSourceDevice() *schema.Resource {
}

func dataSourceDeviceRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*Clients).V1
client := m.(*Clients).V2

var filter func(d tailscale.Device) bool
var filter func(d tsclient.Device) bool
var filterDesc string

if name, ok := d.GetOk("name"); ok {
filter = func(d tailscale.Device) bool {
filter = func(d tsclient.Device) bool {
return d.Name == name.(string)
}
filterDesc = fmt.Sprintf("name=%q", name.(string))
}

if hostname, ok := d.GetOk("hostname"); ok {
filter = func(d tailscale.Device) bool {
filter = func(d tsclient.Device) bool {
return d.Hostname == hostname.(string)
}
filterDesc = fmt.Sprintf("hostname=%q", hostname.(string))
}

devices, err := client.Devices(ctx)
devices, err := client.Devices().List(ctx)
if err != nil {
return diagnosticsError(err, "Failed to fetch devices")
}

var selected *tailscale.Device
var selected *tsclient.Device
for _, device := range devices {
if filter(device) {
selected = &device
Expand All @@ -108,26 +108,18 @@ func dataSourceDeviceRead(ctx context.Context, d *schema.ResourceData, m interfa
}

d.SetId(selected.ID)
return setProperties(d, DeviceToMap(selected))
}

if err = d.Set("name", selected.Name); err != nil {
return diagnosticsError(err, "Failed to set name")
}

if err = d.Set("hostname", selected.Hostname); err != nil {
return diagnosticsError(err, "Failed to set hostname")
}

if err = d.Set("user", selected.User); err != nil {
return diagnosticsError(err, "Failed to set user")
}

if err = d.Set("addresses", selected.Addresses); err != nil {
return diagnosticsError(err, "Failed to set addresses")
}

if err = d.Set("tags", selected.Tags); err != nil {
return diagnosticsError(err, "Failed to set tags")
// DeviceToMap converts the given device into a map representing the device as a
// resource in Terraform. This omits the "id" which is expected to be set
// using [schema.ResourceData.SetId].
func DeviceToMap(device *tsclient.Device) map[string]any {
return map[string]any{
"name": device.Name,
"hostname": device.Hostname,
"user": device.User,
"addresses": device.Addresses,
"tags": device.Tags,
}

return nil
}
15 changes: 5 additions & 10 deletions tailscale/data_source_devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ func dataSourceDevices() *schema.Resource {
}

func dataSourceDevicesRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*Clients).V1
client := m.(*Clients).V2

devices, err := client.Devices(ctx)
devices, err := client.Devices().List(ctx)
if err != nil {
return diagnosticsError(err, "Failed to fetch devices")
}
Expand All @@ -82,14 +82,9 @@ func dataSourceDevicesRead(ctx context.Context, d *schema.ResourceData, m interf
continue
}

deviceMaps = append(deviceMaps, map[string]interface{}{
"addresses": device.Addresses,
"name": device.Name,
"hostname": device.Hostname,
"user": device.User,
"id": device.ID,
"tags": device.Tags,
})
m := DeviceToMap(&device)
m["id"] = device.ID
deviceMaps = append(deviceMaps, m)
}

if err = d.Set("devices", deviceMaps); err != nil {
Expand Down
134 changes: 134 additions & 0 deletions tailscale/datasource_devices_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package tailscale_test

import (
"context"
"fmt"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"

"github.com/tailscale/terraform-provider-tailscale/tailscale"
)

func TestAccTailscaleDevices(t *testing.T) {
resourceName := "data.tailscale_devices.all_devices"

// This is a string containing tailscale_device datasource configurations
devicesDataSources := &strings.Builder{}

toResourceComponent := func(str string) string {
return strings.ReplaceAll(str, " ", "_")
}

// First test the tailscale_devices datasource, which will give us a list of
// all device IDs.
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories(t),
Steps: []resource.TestStep{
{
Config: `data "tailscale_devices" "all_devices" {}`,
Check: func(s *terraform.State) error {
client := testAccProvider.Meta().(*tailscale.Clients).V2
devices, err := client.Devices().List(context.Background())
if err != nil {
return fmt.Errorf("unable to list devices: %s", err)
}

devicesByID := make(map[string]map[string]any)
for _, device := range devices {
m := tailscale.DeviceToMap(&device)
m["id"] = device.ID
devicesByID[device.ID] = m
}

rs := s.RootModule().Resources[resourceName].Primary

// first find indexes for devices
deviceIndexes := make(map[string]string)
for k, v := range rs.Attributes {
if strings.HasSuffix(k, ".id") {
idx := strings.Split(k, ".")[1]
deviceIndexes[idx] = v
}
}

// make sure we got the right number of devices
if len(deviceIndexes) != len(devicesByID) {
return fmt.Errorf("wrong number of devices in datasource, want %d, got %d", len(devicesByID), len(deviceIndexes))
}

// now compare datasource attributes to expected values
for k, v := range rs.Attributes {
if strings.HasPrefix(k, "devices.") {
parts := strings.Split(k, ".")
if len(parts) != 3 {
continue
}
prop := parts[2]
if prop == "%" {
continue
}
idx := parts[1]
id := deviceIndexes[idx]
expected := fmt.Sprint(devicesByID[id][prop])
if v != expected {
return fmt.Errorf("wrong value of %s for device %s, want %q, got %q", prop, id, expected, v)
}
}
}

// Now set up device datasources for each device. This is used in the following test
// of the tailscale_device datasource.
for _, device := range devices {
if device.Hostname != "" {
devicesDataSources.WriteString(fmt.Sprintf("\ndata \"tailscale_device\" \"%s\" {\n hostname = \"%s\"\n}\n", toResourceComponent(device.Hostname), device.Hostname))
} else {
devicesDataSources.WriteString(fmt.Sprintf("\ndata \"tailscale_device\" \"%s\" {\n name = \"%s\"\n}\n", toResourceComponent(device.Name), device.Name))
}
}

return nil
},
},
},
})

// Now test the individual tailscale_device data sources for each device,
// making sure that it pulls in the relevant details for each device.
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories(t),
Steps: []resource.TestStep{
{
Config: devicesDataSources.String(),
Check: func(s *terraform.State) error {
client := testAccProvider.Meta().(*tailscale.Clients).V2
devices, err := client.Devices().List(context.Background())
if err != nil {
return fmt.Errorf("unable to list devices: %s", err)
}

for _, device := range devices {
expected := tailscale.DeviceToMap(&device)
expected["id"] = device.ID
var nameComponent string
if device.Hostname != "" {
nameComponent = device.Hostname
} else {
nameComponent = device.Name
}
resourceName := fmt.Sprintf("data.tailscale_device.%s", toResourceComponent(nameComponent))
if err := checkPropertiesMatch(resourceName, s, expected); err != nil {
return err
}
}

return nil
},
},
},
})
}

0 comments on commit 548482c

Please sign in to comment.