Skip to content

Commit

Permalink
tailscale: use v2 client for device tags
Browse files Browse the repository at this point in the history
 Updates tailscale/corp#21867

Signed-off-by: Percy Wegmann <percy@tailscale.com>
  • Loading branch information
oxtoacart committed Aug 20, 2024
1 parent 531e6bf commit a7aa735
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 77 deletions.
6 changes: 6 additions & 0 deletions tailscale/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"fmt"
"net/url"
"os"
"time"

"github.com/hashicorp/go-cty/cty"
Expand Down Expand Up @@ -310,3 +311,8 @@ func optional[T any](d *schema.ResourceData, key string) *T {
}
return tsclient.PointerTo(d.Get(key).(T))
}

// isAcceptanceTesting returns true if we're running acceptance tests.
func isAcceptanceTesting() bool {
return os.Getenv("TF_ACC") != ""
}
71 changes: 24 additions & 47 deletions tailscale/resource_device_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@ 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"
)

func resourceDeviceTags() *schema.Resource {
var deleteContext = resourceDeviceTagsDelete
if isAcceptanceTesting() {
// Tags cannot be removed without reauthorizing the device as a user.
// We have no way of doing this during testing.
// Because of https://github.com/hashicorp/terraform-plugin-sdk/issues/609,
// we also have no way of telling the Terraform acceptance test to not test
// resource deletion.
// So, as a workaround, we don't actually delete during acceptance tests.
deleteContext = schema.NoopContext
}

return &schema.Resource{
Description: "The device_tags resource is used to apply tags to Tailscale devices. See https://tailscale.com/kb/1068/acl-tags/ for more details.",
ReadContext: resourceDeviceTagsRead,
CreateContext: resourceDeviceTagsCreate,
UpdateContext: resourceDeviceTagsUpdate,
DeleteContext: resourceDeviceTagsDelete,
CreateContext: resourceDeviceTagsSet,
UpdateContext: resourceDeviceTagsSet,
DeleteContext: deleteContext,
Schema: map[string]*schema.Schema{
"device_id": {
Type: schema.TypeString,
Expand All @@ -35,53 +44,21 @@ func resourceDeviceTags() *schema.Resource {
}

func resourceDeviceTagsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*Clients).V1
client := m.(*Clients).V2
deviceID := d.Get("device_id").(string)

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

var selected *tailscale.Device
for _, device := range devices {
if device.ID != deviceID {
continue
}

selected = &device
break
return diagnosticsError(err, "Failed to fetch device")
}

if selected == nil {
return diag.Errorf("Could not find device with id %s", deviceID)
}

d.SetId(selected.ID)
d.Set("tags", selected.Tags)
d.SetId(device.ID)
d.Set("tags", device.Tags)
return nil
}

func resourceDeviceTagsCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*Clients).V1
deviceID := d.Get("device_id").(string)
set := d.Get("tags").(*schema.Set)

tags := make([]string, set.Len())
for i, item := range set.List() {
tags[i] = item.(string)
}

if err := client.SetDeviceTags(ctx, deviceID, tags); err != nil {
return diagnosticsError(err, "Failed to set device tags")
}

d.SetId(deviceID)
return resourceDeviceTagsRead(ctx, d, m)
}

func resourceDeviceTagsUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*Clients).V1
func resourceDeviceTagsSet(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*Clients).V2
deviceID := d.Get("device_id").(string)
set := d.Get("tags").(*schema.Set)

Expand All @@ -90,7 +67,7 @@ func resourceDeviceTagsUpdate(ctx context.Context, d *schema.ResourceData, m int
tags[i] = item.(string)
}

if err := client.SetDeviceTags(ctx, deviceID, tags); err != nil {
if err := client.Devices().SetTags(ctx, deviceID, tags); err != nil {
return diagnosticsError(err, "Failed to set device tags")
}

Expand All @@ -99,10 +76,10 @@ func resourceDeviceTagsUpdate(ctx context.Context, d *schema.ResourceData, m int
}

func resourceDeviceTagsDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*Clients).V1
client := m.(*Clients).V2
deviceID := d.Get("device_id").(string)

if err := client.SetDeviceTags(ctx, deviceID, []string{}); err != nil {
if err := client.Devices().SetTags(ctx, deviceID, []string{}); err != nil {
return diagnosticsError(err, "Failed to set device tags")
}

Expand Down
115 changes: 85 additions & 30 deletions tailscale/resource_device_tags_test.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,101 @@
package tailscale_test

import (
"net/http"
"context"
"fmt"
"os"
"reflect"
"testing"

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

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

const testDeviceTags = `
data "tailscale_device" "test_device" {
name = "device.example.com"
wait_for = "60s"
func TestAccTailscaleDeviceTags(t *testing.T) {
const resourceName = "tailscale_device_tags.test_tags"

const testDeviceTagsCreate = `
data "tailscale_device" "test_device" {
name = "%s"
wait_for = "60s"
}
resource "tailscale_device_tags" "test_tags" {
device_id = data.tailscale_device.test_device.id
tags = [
"tag:a",
"tag:b",
]
}`

const testDeviceTagsUpdate = `
data "tailscale_device" "test_device" {
name = "%s"
wait_for = "60s"
}
resource "tailscale_device_tags" "test_tags" {
device_id = data.tailscale_device.test_device.id
tags = [
"tag:b",
"tag:c",
]
}`

checkProperties := func(expectedTags []string) func(client *tsclient.Client, rs *terraform.ResourceState) error {
return func(client *tsclient.Client, rs *terraform.ResourceState) error {
deviceID := rs.Primary.Attributes["device_id"]

device, err := client.Devices().Get(context.Background(), deviceID)
if err != nil {
return fmt.Errorf("failed to fetch device: %s", err)
}

if !reflect.DeepEqual(device.Tags, expectedTags) {
return fmt.Errorf("bad tags: %#v", device.Tags)
}
return nil
}
}
resource "tailscale_device_tags" "test_tags" {
device_id = data.tailscale_device.test_device.id
tags = [
"a:b",
"b:c",
]
}`

func TestProvider_TailscaleDeviceTags(t *testing.T) {

resource.Test(t, resource.TestCase{
IsUnitTest: true,
PreCheck: func() {
testServer.ResponseCode = http.StatusOK
testServer.ResponseBody = map[string][]tailscale.Device{
"devices": {
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories(t),
Steps: []resource.TestStep{
{
PreConfig: func() {
// Set up ACLs to allow the required tags
client := testAccProvider.Meta().(*tailscale.Clients).V2
err := client.PolicyFile().Set(context.Background(), `
{
Name: "device.example.com",
ID: "123",
},
"tagOwners": {
"tag:a": ["autogroup:member"],
"tag:b": ["autogroup:member"],
"tag:c": ["autogroup:member"],
},
}`, "")
if err != nil {
panic(err)
}
},
}
},
ProviderFactories: testProviderFactories(t),
Steps: []resource.TestStep{
testResourceCreated("tailscale_device_tags.test_tags", testDeviceTags),
testResourceDestroyed("tailscale_device_tags.test_tags", testDeviceTags),
Config: fmt.Sprintf(testDeviceTagsCreate, os.Getenv("TAILSCALE_TEST_DEVICE_NAME")),
Check: resource.ComposeTestCheckFunc(
checkResourceRemoteProperties(resourceName, checkProperties([]string{"tag:a", "tag:b"})),
resource.TestCheckTypeSetElemAttr(resourceName, "tags.*", "tag:a"),
resource.TestCheckTypeSetElemAttr(resourceName, "tags.*", "tag:b"),
),
},
{
Config: fmt.Sprintf(testDeviceTagsUpdate, os.Getenv("TAILSCALE_TEST_DEVICE_NAME")),
Check: resource.ComposeTestCheckFunc(
checkResourceRemoteProperties(resourceName, checkProperties([]string{"tag:b", "tag:c"})),
resource.TestCheckTypeSetElemAttr(resourceName, "tags.*", "tag:b"),
resource.TestCheckTypeSetElemAttr(resourceName, "tags.*", "tag:c"),
),
},
},
})
}

0 comments on commit a7aa735

Please sign in to comment.