Skip to content

Commit

Permalink
tailscale: add webhook resource
Browse files Browse the repository at this point in the history
Add a `tailscale_webhook` resource to allow for managing tailscale
webhooks via terraform.

Updates tailscale/corp#21625

Signed-off-by: Mario Minardi <mario@tailscale.com>
  • Loading branch information
mpminardi committed Jul 22, 2024
1 parent dfe70e4 commit ad94c6a
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 6 deletions.
47 changes: 47 additions & 0 deletions docs/resources/webhook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "tailscale_webhook Resource - terraform-provider-tailscale"
subcategory: ""
description: |-
The webhook resource allows you to configure webhook endpoints for your Tailscale network. See https://tailscale.com/kb/1213/webhooks for more information.
---

# tailscale_webhook (Resource)

The webhook resource allows you to configure webhook endpoints for your Tailscale network. See https://tailscale.com/kb/1213/webhooks for more information.

## Example Usage

```terraform
resource "tailscale_webhook" "sample_webhook" {
endpoint_url = "https://example.com/webhook/endpoint"
provider_type = "slack"
subscriptions = ["nodeCreated", "userDeleted"]
}
```

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

### Required

- `endpoint_url` (String) The endpoint to send webhook events to.
- `subscriptions` (Set of String) The Tailscale events to subscribe this webhook to. See https://tailscale.com/kb/1213/webhooks#events for the list of valid events.

### Optional

- `provider_type` (String) The provider type of the endpoint URL. Also referred to as the 'destination' for the webhook in the admin panel. Webhook event payloads are formatted according to the provider type if it is set to a known value. Must be one of `slack`, `mattermost`, `googlechat`, or `discord` if set.

### Read-Only

- `id` (String) The ID of this resource.
- `secret` (String, Sensitive) The secret used for signing webhook payloads. Only set on resource creation. See https://tailscale.com/kb/1213/webhooks#webhook-secret for more information.

## Import

Import is supported using the following syntax:

```shell
# Webhooks can be imported using the endpoint id, e.g.,
terraform import tailscale_webhook.sample_webhook 123456789
```
2 changes: 2 additions & 0 deletions examples/resources/tailscale_webhook/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Webhooks can be imported using the endpoint id, e.g.,
terraform import tailscale_webhook.sample_webhook 123456789
5 changes: 5 additions & 0 deletions examples/resources/tailscale_webhook/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "tailscale_webhook" "sample_webhook" {
endpoint_url = "https://example.com/webhook/endpoint"
provider_type = "slack"
subscriptions = ["nodeCreated", "userDeleted"]
}
4 changes: 2 additions & 2 deletions 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.0
github.com/tailscale/tailscale-client-go v1.17.1-0.20240718200212-ff9b01c0d472
golang.org/x/tools v0.23.0
tailscale.com v1.68.2
)
Expand Down Expand Up @@ -79,7 +79,7 @@ require (
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.19.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +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.0 h1:4ZTsfB2hiiiIcx7prcXapZHO5K+TXB5fTdhGxdKGR88=
github.com/tailscale/tailscale-client-go v1.17.0/go.mod h1:Zxz9AWl4cNX8F+jE7iIeo6Me7dGPXNdCFIoVeovH6eI=
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/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 Expand Up @@ -231,8 +231,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
1 change: 1 addition & 0 deletions tailscale/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func Provider(options ...ProviderOption) *schema.Provider {
"tailscale_tailnet_key": resourceTailnetKey(),
"tailscale_device_tags": resourceDeviceTags(),
"tailscale_device_key": resourceDeviceKey(),
"tailscale_webhook": resourceWebhook(),
},
DataSourcesMap: map[string]*schema.Resource{
"tailscale_device": dataSourceDevice(),
Expand Down
171 changes: 171 additions & 0 deletions tailscale/resource_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package tailscale

import (
"context"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/tailscale/tailscale-client-go/tailscale"
)

func resourceWebhook() *schema.Resource {
return &schema.Resource{
Description: "The webhook resource allows you to configure webhook endpoints for your Tailscale network. See https://tailscale.com/kb/1213/webhooks for more information.",
ReadContext: resourceWebhookRead,
CreateContext: resourceWebhookCreate,
UpdateContext: resourceWebhookUpdate,
DeleteContext: resourceWebhookDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"endpoint_url": {
Type: schema.TypeString,
Description: "The endpoint to send webhook events to.",
Required: true,
ForceNew: true,
},
"provider_type": {
Type: schema.TypeString,
Description: "The provider type of the endpoint URL. Also referred to as the 'destination' for the webhook in the admin panel. Webhook event payloads are formatted according to the provider type if it is set to a known value. Must be one of `slack`, `mattermost`, `googlechat`, or `discord` if set.",
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice(
[]string{
string(tailscale.EmptyProviderType),
string(tailscale.SlackProviderType),
string(tailscale.MattermostProviderType),
string(tailscale.GoogleChatProviderType),
string(tailscale.DiscordProviderType),
},
false,
),
},
"subscriptions": {
Type: schema.TypeSet,
Description: "The Tailscale events to subscribe this webhook to. See https://tailscale.com/kb/1213/webhooks#events for the list of valid events.",
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringInSlice(
[]string{
string(tailscale.NodeCreated),
string(tailscale.NodeNeedsApproval),
string(tailscale.NodeApproved),
string(tailscale.NodeKeyExpiringInOneDay),
string(tailscale.NodeKeyExpired),
string(tailscale.NodeDeleted),
string(tailscale.PolicyUpdate),
string(tailscale.UserCreated),
string(tailscale.UserNeedsApproval),
string(tailscale.UserSuspended),
string(tailscale.UserRestored),
string(tailscale.UserDeleted),
string(tailscale.UserApproved),
string(tailscale.UserRoleUpdated),
string(tailscale.SubnetIPForwardingNotEnabled),
string(tailscale.ExitNodeIPForwardingNotEnabled),
},
false,
),
},
},
"secret": {
Type: schema.TypeString,
Description: "The secret used for signing webhook payloads. Only set on resource creation. See https://tailscale.com/kb/1213/webhooks#webhook-secret for more information.",
Sensitive: true,
Computed: true,
},
},
}
}

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

endpointURL := d.Get("endpoint_url").(string)
providerType := tailscale.ProviderType(d.Get("provider_type").(string))
subscriptions := d.Get("subscriptions").(*schema.Set).List()

var requestSubscriptions []tailscale.SubscriptionType
for _, subscription := range subscriptions {
requestSubscriptions = append(requestSubscriptions, tailscale.SubscriptionType(subscription.(string)))
}

request := tailscale.CreateWebhookRequest{
EndpointURL: endpointURL,
ProviderType: providerType,
Subscriptions: requestSubscriptions,
}

webhook, err := client.CreateWebhook(ctx, request)
if err != nil {
return diagnosticsError(err, "Failed to create webhook")
}

d.SetId(webhook.EndpointID)
// Secret is only returned on create.
d.Set("secret", webhook.Secret)

return resourceWebhookRead(ctx, d, m)
}

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

webhook, err := client.Webhook(ctx, d.Id())
if err != nil {
return diagnosticsError(err, "Failed to fetch webhook")
}

if err = d.Set("endpoint_url", webhook.EndpointURL); err != nil {
return diagnosticsError(err, "Failed to set endpoint_url field")
}

if err = d.Set("provider_type", webhook.ProviderType); err != nil {
return diagnosticsError(err, "Failed to set provider_type field")
}

if err = d.Set("subscriptions", webhook.Subscriptions); err != nil {
return diagnosticsError(err, "Failed to set subscriptions field")
}

if err = d.Set("secret", d.Get("secret").(string)); err != nil {
return diagnosticsError(err, "Failed to set secret field")
}

return nil
}

func resourceWebhookUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
if !d.HasChange("subscriptions") {
return resourceWebhookRead(ctx, d, m)
}

client := m.(*tailscale.Client)
subscriptions := d.Get("subscriptions").(*schema.Set).List()

var requestSubscriptions []tailscale.SubscriptionType
for _, subscription := range subscriptions {
requestSubscriptions = append(requestSubscriptions, tailscale.SubscriptionType(subscription.(string)))
}

_, err := client.UpdateWebhook(ctx, d.Id(), requestSubscriptions)
if err != nil {
return diagnosticsError(err, "Failed to update webhook")
}

return resourceWebhookRead(ctx, d, m)
}

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

err := client.DeleteWebhook(ctx, d.Id())
if err != nil {
return diagnosticsError(err, "Failed to delete webhook")
}

return nil
}
34 changes: 34 additions & 0 deletions tailscale/resource_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package tailscale_test

import (
"net/http"
"testing"

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

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

const testWebhook = `
resource "tailscale_webhook" "test_webhook" {
endpoint_url = "https://example.com/endpoint"
provider_type = "slack"
subscriptions = ["userNeedsApproval", "nodeCreated"]
}`

func TestProvider_TailscaleWebhook(t *testing.T) {
resource.Test(t, resource.TestCase{
IsUnitTest: true,
PreCheck: func() {
testServer.ResponseCode = http.StatusOK
testServer.ResponseBody = tailscale.Webhook{
EndpointID: "12345",
}
},
ProviderFactories: testProviderFactories(t),
Steps: []resource.TestStep{
testResourceCreated("tailscale_webhook.test_webhook", testWebhook),
testResourceDestroyed("tailscale_webhook.test_webhook", testWebhook),
},
})
}

0 comments on commit ad94c6a

Please sign in to comment.