diff --git a/tailscale/provider.go b/tailscale/provider.go index 548d8cc8..67b0481e 100644 --- a/tailscale/provider.go +++ b/tailscale/provider.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "net/url" + "os" "time" "github.com/hashicorp/go-cty/cty" @@ -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") != "" +} diff --git a/tailscale/resource_device_tags.go b/tailscale/resource_device_tags.go index 69eca865..59bd7e72 100644 --- a/tailscale/resource_device_tags.go +++ b/tailscale/resource_device_tags.go @@ -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, @@ -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) @@ -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") } @@ -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") } diff --git a/tailscale/resource_device_tags_test.go b/tailscale/resource_device_tags_test.go index 5aec7af5..74623ad5 100644 --- a/tailscale/resource_device_tags_test.go +++ b/tailscale/resource_device_tags_test.go @@ -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"), + ), + }, }, }) }