Skip to content

Commit

Permalink
tailscale: add contacts resource (#387)
Browse files Browse the repository at this point in the history
Add a `tailscale_contacts` resource to allow for managing tailscale
contacts settings via Terraform.

Updates tailscale/corp#21631

Signed-off-by: Mario Minardi <mario@tailscale.com>
  • Loading branch information
mpminardi authored Aug 1, 2024
1 parent 5196360 commit 6bc9542
Show file tree
Hide file tree
Showing 8 changed files with 497 additions and 5 deletions.
77 changes: 77 additions & 0 deletions docs/resources/contacts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
# generated by /~https://github.com/hashicorp/terraform-plugin-docs
page_title: "tailscale_contacts Resource - terraform-provider-tailscale"
subcategory: ""
description: |-
The contacts resource allows you to configure contact details for your Tailscale network. See https://tailscale.com/kb/1224/contact-preferences for more information.
Destroying this resource does not unset or modify values in the tailscale control plane, and simply removes the resource from Terraform state.
---

# tailscale_contacts (Resource)

The contacts resource allows you to configure contact details for your Tailscale network. See https://tailscale.com/kb/1224/contact-preferences for more information.

Destroying this resource does not unset or modify values in the tailscale control plane, and simply removes the resource from Terraform state.

## Example Usage

```terraform
resource "tailscale_contacts" "sample_contacts" {
account {
email = "account@example.com"
}
support {
email = "support@example.com"
}
security {
email = "security@example.com"
}
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `account` (Block Set, Min: 1, Max: 1) Configuration for communications about important changes to your tailnet (see [below for nested schema](#nestedblock--account))
- `security` (Block Set, Min: 1, Max: 1) Configuration for communications about security issues affecting your tailnet (see [below for nested schema](#nestedblock--security))
- `support` (Block Set, Min: 1, Max: 1) Configuration for communications about misconfigurations in your tailnet (see [below for nested schema](#nestedblock--support))

### Read-Only

- `id` (String) The ID of this resource.

<a id="nestedblock--account"></a>
### Nested Schema for `account`

Required:

- `email` (String) Email address to send communications to


<a id="nestedblock--security"></a>
### Nested Schema for `security`

Required:

- `email` (String) Email address to send communications to


<a id="nestedblock--support"></a>
### Nested Schema for `support`

Required:

- `email` (String) Email address to send communications to

## Import

Import is supported using the following syntax:

```shell
# ID doesn't matter.
terraform import tailscale_contacts.sample_contacts contacts
```
2 changes: 2 additions & 0 deletions examples/resources/tailscale_contacts/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# ID doesn't matter.
terraform import tailscale_contacts.sample_contacts contacts
13 changes: 13 additions & 0 deletions examples/resources/tailscale_contacts/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
resource "tailscale_contacts" "sample_contacts" {
account {
email = "account@example.com"
}

support {
email = "support@example.com"
}

security {
email = "security@example.com"
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0
github.com/stretchr/testify v1.9.0
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/tailscale-client-go v1.17.1-0.20240724064152-5d834701bd85
github.com/tailscale/tailscale-client-go v1.17.1-0.20240729175651-90a1e935cc19
golang.org/x/tools v0.23.0
tailscale.com v1.70.0
)
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/tailscale-client-go v1.17.1-0.20240718200212-ff9b01c0d472 h1:BLqo/AEtgxdsqPC/WU0lNDwMLjQwvkXappShYmZrZ34=
github.com/tailscale/tailscale-client-go v1.17.1-0.20240718200212-ff9b01c0d472/go.mod h1:jbwJyHniK3nyLttwcDTXnfdDQEnADvc4VMOP8hZWnR0=
github.com/tailscale/tailscale-client-go v1.17.1-0.20240724064152-5d834701bd85 h1:F6nQg/GLWZDx27RPiAkJwCR3LYKYxBquUuiPcnuLesA=
github.com/tailscale/tailscale-client-go v1.17.1-0.20240724064152-5d834701bd85/go.mod h1:jbwJyHniK3nyLttwcDTXnfdDQEnADvc4VMOP8hZWnR0=
github.com/tailscale/tailscale-client-go v1.17.1-0.20240729175651-90a1e935cc19 h1:fRLv1yZH1ueL1cnpLhOnOymoBfMCIviCn0e0VkAjkK4=
github.com/tailscale/tailscale-client-go v1.17.1-0.20240729175651-90a1e935cc19/go.mod h1:jbwJyHniK3nyLttwcDTXnfdDQEnADvc4VMOP8hZWnR0=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
Expand Down
1 change: 1 addition & 0 deletions tailscale/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func Provider(options ...ProviderOption) *schema.Provider {
"tailscale_device_tags": resourceDeviceTags(),
"tailscale_device_key": resourceDeviceKey(),
"tailscale_webhook": resourceWebhook(),
"tailscale_contacts": resourceContacts(),
},
DataSourcesMap: map[string]*schema.Resource{
"tailscale_device": dataSourceDevice(),
Expand Down
181 changes: 181 additions & 0 deletions tailscale/resource_contacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package tailscale

import (
"context"

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

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

const resourceContactsDescription = `The contacts resource allows you to configure contact details for your Tailscale network. See https://tailscale.com/kb/1224/contact-preferences for more information.
Destroying this resource does not unset or modify values in the tailscale control plane, and simply removes the resource from Terraform state.
`

func resourceContacts() *schema.Resource {
return &schema.Resource{
Description: resourceContactsDescription,
ReadContext: resourceContactsRead,
CreateContext: resourceContactsCreate,
UpdateContext: resourceContactsUpdate,
DeleteContext: resourceContactsDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"account": {
Type: schema.TypeSet,
Description: "Configuration for communications about important changes to your tailnet",
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"email": {
Type: schema.TypeString,
Description: "Email address to send communications to",
Required: true,
},
},
},
},
"support": {
Type: schema.TypeSet,
Description: "Configuration for communications about misconfigurations in your tailnet",
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"email": {
Type: schema.TypeString,
Description: "Email address to send communications to",
Required: true,
},
},
},
},
"security": {
Type: schema.TypeSet,
Description: "Configuration for communications about security issues affecting your tailnet",
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"email": {
Type: schema.TypeString,
Description: "Email address to send communications to",
Required: true,
},
},
},
},
},
}
}

func resourceContactsCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*tailscale.Client)

if diagErr := updateContact(ctx, client, d, tailscale.ContactAccount); diagErr != nil {
return diagErr
}

if diagErr := updateContact(ctx, client, d, tailscale.ContactSupport); diagErr != nil {
return diagErr
}

if diagErr := updateContact(ctx, client, d, tailscale.ContactSecurity); diagErr != nil {
return diagErr
}

d.SetId(createUUID())
return resourceContactsRead(ctx, d, m)
}

func resourceContactsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*tailscale.Client)

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

if err = d.Set("account", buildContactMap(contacts.Account)); err != nil {
return diagnosticsError(err, "Failed to set account field")
}

if err = d.Set("support", buildContactMap(contacts.Support)); err != nil {
return diagnosticsError(err, "Failed to set support field")
}

if err = d.Set("security", buildContactMap(contacts.Security)); err != nil {
return diagnosticsError(err, "Failed to set security field")
}

return nil
}

func resourceContactsUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*tailscale.Client)

if d.HasChange("account") {
if diagErr := updateContact(ctx, client, d, tailscale.ContactAccount); diagErr != nil {
return diagErr
}
}

if d.HasChange("support") {
if diagErr := updateContact(ctx, client, d, tailscale.ContactSupport); diagErr != nil {
return diagErr
}
}

if d.HasChange("security") {
if diagErr := updateContact(ctx, client, d, tailscale.ContactSecurity); diagErr != nil {
return diagErr
}
}

return resourceContactsRead(ctx, d, m)
}

func resourceContactsDelete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
// Deleting is a no-op since we cannot have unset contact information.
// Deletion in this context is simply removing from terraform state.
const diagDetail = `This resource has been successfully destroyed, but values in tailscale will remain set.
See https://tailscale.com/kb/1224/contact-preferences to learn more.`

return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Warning,
Summary: "Destroying tailscale_contacts does not unset contact values on tailscale",
Detail: diagDetail,
},
}
}

// buildContactMap transforms a tailscale.Contact into an equivalnet single element
// slice of map[string]interface{} so that it can be set on a schema.TypeSet property
// in the resource.
func buildContactMap(contact tailscale.Contact) []map[string]interface{} {
return []map[string]interface{}{
{
"email": contact.Email,
},
}
}

// updateContact updates the contact specified by the tailscale.ContactType by
// reading the resource property with the correct name and using it to build a
// request to the underlying client.
func updateContact(ctx context.Context, client *tailscale.Client, d *schema.ResourceData, contactType tailscale.ContactType) diag.Diagnostics {
contact := d.Get(string(contactType)).(*schema.Set).List()
contactEmail := contact[0].(map[string]interface{})["email"].(string)

if err := client.UpdateContact(ctx, contactType, tailscale.UpdateContactRequest{Email: &contactEmail}); err != nil {
return diagnosticsError(err, "Failed to create contacts")
}

return nil
}
Loading

0 comments on commit 6bc9542

Please sign in to comment.