diff --git a/docs/resources/logstream_configuration.md b/docs/resources/logstream_configuration.md index 87ea1b02..3caef049 100644 --- a/docs/resources/logstream_configuration.md +++ b/docs/resources/logstream_configuration.md @@ -38,3 +38,12 @@ resource "tailscale_logstream_configuration" "sample_logstream_configuration" { ### Read-Only - `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +# Logstream configuration can be imported using the logstream configuration id, e.g., +terraform import tailscale_logstream_configuration.sample_logstream_configuration 123456789 +``` diff --git a/docs/resources/posture_integration.md b/docs/resources/posture_integration.md index 6bba84bc..60857602 100644 --- a/docs/resources/posture_integration.md +++ b/docs/resources/posture_integration.md @@ -38,3 +38,12 @@ resource "tailscale_posture_integration" "sample_posture_integration" { ### Read-Only - `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +# Posture integration can be imported using the posture integration id, e.g., +terraform import tailscale_posture_integration.sample_posture_integration 123456789 +``` diff --git a/examples/resources/tailscale_logstream_configuration/import.sh b/examples/resources/tailscale_logstream_configuration/import.sh new file mode 100644 index 00000000..35ab18cc --- /dev/null +++ b/examples/resources/tailscale_logstream_configuration/import.sh @@ -0,0 +1,2 @@ +# Logstream configuration can be imported using the logstream configuration id, e.g., +terraform import tailscale_logstream_configuration.sample_logstream_configuration 123456789 diff --git a/examples/resources/tailscale_posture_integration/import.sh b/examples/resources/tailscale_posture_integration/import.sh new file mode 100644 index 00000000..7f931402 --- /dev/null +++ b/examples/resources/tailscale_posture_integration/import.sh @@ -0,0 +1,2 @@ +# Posture integration can be imported using the posture integration id, e.g., +terraform import tailscale_posture_integration.sample_posture_integration 123456789 diff --git a/tailscale/resource_device_key_test.go b/tailscale/resource_device_key_test.go index d63effeb..28788165 100644 --- a/tailscale/resource_device_key_test.go +++ b/tailscale/resource_device_key_test.go @@ -78,6 +78,11 @@ func TestAccTailscaleDeviceKey(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "key_expiry_disabled", "true"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } diff --git a/tailscale/resource_device_subnet_routes.go b/tailscale/resource_device_subnet_routes.go index efecf752..d64034fc 100644 --- a/tailscale/resource_device_subnet_routes.go +++ b/tailscale/resource_device_subnet_routes.go @@ -27,7 +27,17 @@ func resourceDeviceSubnetRoutes() *schema.Resource { UpdateContext: resourceDeviceSubnetRoutesUpdate, DeleteContext: resourceDeviceSubnetRoutesDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + // We can't do a simple passthrough here as the ID used for this resource is a + // randomly generated UUID and we need to instead fetch based on the device_id. + // + // TODO(mpminardi): investigate changing the ID in state to be the device_id instead + // in an eventual major version bump. + d.Set("device_id", d.Id()) + d.SetId(createUUID()) + + return []*schema.ResourceData{d}, nil + }, }, Schema: map[string]*schema.Schema{ "device_id": { @@ -49,14 +59,13 @@ func resourceDeviceSubnetRoutes() *schema.Resource { func resourceDeviceSubnetRoutesRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { client := m.(*tsclient.Client) - deviceID := d.Id() + deviceID := d.Get("device_id").(string) routes, err := client.Devices().SubnetRoutes(ctx, deviceID) if err != nil { return diagnosticsError(err, "Failed to fetch device subnet routes") } - d.Set("device_id", deviceID) if err = d.Set("routes", routes.Enabled); err != nil { return diag.FromErr(err) } diff --git a/tailscale/resource_device_subnet_routes_test.go b/tailscale/resource_device_subnet_routes_test.go index 6207146c..49af3c93 100644 --- a/tailscale/resource_device_subnet_routes_test.go +++ b/tailscale/resource_device_subnet_routes_test.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "reflect" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -62,6 +63,7 @@ func TestAccTailscaleDeviceSubnetRoutes(t *testing.T) { } } + var deviceId string resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviderFactories(t), @@ -84,6 +86,67 @@ func TestAccTailscaleDeviceSubnetRoutes(t *testing.T) { resource.TestCheckTypeSetElemAttr(resourceName, "routes.*", "2.0.0.0/24"), ), }, + { + ResourceName: resourceName, + ImportState: true, + // Need import state ID func to dynamically grab device_id for import. + ImportStateIdFunc: func(state *terraform.State) (string, error) { + rs, ok := state.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("resource not found: %s", resourceName) + + } + + deviceId = rs.Primary.Attributes["device_id"] + + return deviceId, nil + }, + // Need a custom import state check due to the fact that the ID for this + // resource is re-generated on import. + ImportStateCheck: func(states []*terraform.InstanceState) error { + if len(states) != 1 { + return fmt.Errorf("expected 1 state: %+v", states) + } + + rs := states[0] + elemCheck := func(attr string, value string) bool { + attrParts := strings.Split(attr, ".") + for stateKey, stateValue := range rs.Attributes { + if stateValue == value { + stateKeyParts := strings.Split(stateKey, ".") + if len(stateKeyParts) == len(attrParts) { + for i := range attrParts { + if attrParts[i] != stateKeyParts[i] && attrParts[i] != "*" { + break + } + if i == len(attrParts)-1 { + return true + } + } + } + } + } + + return false + } + + if rs.Attributes["device_id"] != deviceId { + return fmt.Errorf("expected device_id to be %q but was: %q", deviceId, rs.Attributes["device_id"]) + } + + if !elemCheck("routes.*", "10.0.1.0/24") { + return fmt.Errorf("expected routes to contain '10.0.1.0/24': %#v", rs.Attributes) + } + if !elemCheck("routes.*", "1.2.0.0/16") { + return fmt.Errorf("expected routes to contain '1.2.0.0/16': %#v", rs.Attributes) + } + if !elemCheck("routes.*", "2.0.0.0/24") { + return fmt.Errorf("expected routes to contain '2.0.0.0/24': %#v", rs.Attributes) + } + + return nil + }, + }, }, }) } diff --git a/tailscale/resource_device_tags_test.go b/tailscale/resource_device_tags_test.go index cc7d73eb..dfd22f0f 100644 --- a/tailscale/resource_device_tags_test.go +++ b/tailscale/resource_device_tags_test.go @@ -98,6 +98,11 @@ func TestAccTailscaleDeviceTags(t *testing.T) { resource.TestCheckTypeSetElemAttr(resourceName, "tags.*", "tag:c"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } diff --git a/tailscale/resource_dns_nameservers_test.go b/tailscale/resource_dns_nameservers_test.go index b4dc76d8..d4ebb973 100644 --- a/tailscale/resource_dns_nameservers_test.go +++ b/tailscale/resource_dns_nameservers_test.go @@ -86,6 +86,11 @@ func TestAccTailscaleDNSNameservers(t *testing.T) { resource.TestCheckTypeSetElemAttr(resourceName, "nameservers.*", "1.1.1.1"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } diff --git a/tailscale/resource_dns_search_paths_test.go b/tailscale/resource_dns_search_paths_test.go index 5c6b92ef..7c18ee31 100644 --- a/tailscale/resource_dns_search_paths_test.go +++ b/tailscale/resource_dns_search_paths_test.go @@ -86,6 +86,11 @@ func TestAccTailscaleDNSSearchPaths(t *testing.T) { resource.TestCheckTypeSetElemAttr(resourceName, "search_paths.*", "example.com"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } diff --git a/tailscale/resource_logstream_configuration.go b/tailscale/resource_logstream_configuration.go index c5832f93..56525f1a 100644 --- a/tailscale/resource_logstream_configuration.go +++ b/tailscale/resource_logstream_configuration.go @@ -20,6 +20,9 @@ func resourceLogstreamConfiguration() *schema.Resource { CreateContext: resourceLogstreamConfigurationCreate, UpdateContext: resourceLogstreamUpdate, DeleteContext: resourceLogstreamDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, Schema: map[string]*schema.Schema{ "log_type": { Type: schema.TypeString, diff --git a/tailscale/resource_logstream_configuration_test.go b/tailscale/resource_logstream_configuration_test.go index acadf032..cd728dff 100644 --- a/tailscale/resource_logstream_configuration_test.go +++ b/tailscale/resource_logstream_configuration_test.go @@ -141,6 +141,12 @@ func TestAccTailscaleLogstreamConfiguration_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "token", "some-token"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token"}, + }, }, }) } diff --git a/tailscale/resource_posture_integration.go b/tailscale/resource_posture_integration.go index a922387d..9375bdc9 100644 --- a/tailscale/resource_posture_integration.go +++ b/tailscale/resource_posture_integration.go @@ -20,6 +20,9 @@ func resourcePostureIntegration() *schema.Resource { CreateContext: resourcePostureIntegrationCreate, UpdateContext: resourcePostureIntegrationUpdate, DeleteContext: resourcePostureIntegrationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, Schema: map[string]*schema.Schema{ "posture_provider": { Type: schema.TypeString, diff --git a/tailscale/resource_posture_integration_test.go b/tailscale/resource_posture_integration_test.go index d3b58d46..96e0663d 100644 --- a/tailscale/resource_posture_integration_test.go +++ b/tailscale/resource_posture_integration_test.go @@ -128,6 +128,12 @@ func TestAccTailscalePostureIntegration(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "client_secret", "test-secret3"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"client_secret"}, + }, }, }) }