Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2: added WebhooksResource #91

Merged
merged 1 commit into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions v2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ type (
initOnce sync.Once

// Specific resources
devices *DevicesResource
devices *DevicesResource
webhooks *WebhooksResource
}

// APIError type describes an error as returned by the Tailscale API.
Expand Down Expand Up @@ -91,6 +92,7 @@ func (c *Client) init() {
c.http = &http.Client{Timeout: defaultHttpClientTimeout}
}
c.devices = &DevicesResource{c}
c.webhooks = &WebhooksResource{c}
})
}

Expand All @@ -113,6 +115,11 @@ func (c *Client) Devices() *DevicesResource {
return c.devices
}

func (c *Client) Webhooks() *WebhooksResource {
c.init()
return c.webhooks
}

type requestParams struct {
headers map[string]string
body any
Expand All @@ -139,18 +146,29 @@ func requestContentType(ct string) requestOption {
}
}

// buildURL builds a url to /api/v2/... using the given pathElements. It
// url escapes each path element, so the caller doesn't need to worry about
// that.
// buildURL builds a url to /api/v2/... using the given pathElements.
// It url escapes each path element, so the caller doesn't need to worry about that.
func (c *Client) buildURL(pathElements ...any) *url.URL {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for ourselves that can be addressed outside of this PR: might be good to have some basic regression unit tests around this method (namely for our expectations around path escaping)

elem := make([]string, 1, len(pathElements)+1)
elem[0] = "/api/v2"
for _, pathElement := range pathElements {
elem = append(elem, fmt.Sprint(pathElement))
elem = append(elem, url.PathEscape(fmt.Sprint(pathElement)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another note for future us: will likely want to use url.QueryEscape for any query params we allow interacting with on endpoints that accept them

}
return c.BaseURL.JoinPath(elem...)
}

// buildTailnetURL builds a url to /api/v2/tailnet/<tailnet>/... using the given pathElements.
// It url escapes each path element, so the caller doesn't need to worry about that.
func (c *Client) buildTailnetURL(pathElements ...any) *url.URL {
allElements := make([]any, 2, len(pathElements)+2)
allElements[0] = "tailnet"
allElements[1] = url.PathEscape(c.Tailnet)
for _, element := range pathElements {
allElements = append(allElements, element)
}
return c.buildURL(allElements...)
}

func (c *Client) buildRequest(ctx context.Context, method string, uri *url.URL, opts ...requestOption) (*http.Request, error) {
rof := &requestParams{
contentType: defaultContentType,
Expand Down
2 changes: 1 addition & 1 deletion v2/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (dr *DevicesResource) Get(ctx context.Context, deviceID string) (*Device, e

// List lists the devices in a tailnet.
func (dr *DevicesResource) List(ctx context.Context) ([]Device, error) {
req, err := dr.buildRequest(ctx, http.MethodGet, dr.buildURL("tailnet", dr.Tailnet, "devices"))
req, err := dr.buildRequest(ctx, http.MethodGet, dr.buildTailnetURL("devices"))
if err != nil {
return nil, err
}
Expand Down
152 changes: 152 additions & 0 deletions v2/webhooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package tailscale
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file copied almost verbatim from V1. I'll point out differences in PR.


import (
"context"
"net/http"
"time"
)

const (
WebhookEmptyProviderType WebhookProviderType = ""
WebhookSlackProviderType WebhookProviderType = "slack"
WebhookMattermostProviderType WebhookProviderType = "mattermost"
WebhookGoogleChatProviderType WebhookProviderType = "googlechat"
WebhookDiscordProviderType WebhookProviderType = "discord"
)

const (
WebhookNodeCreated WebhookSubscriptionType = "nodeCreated"
WebhookNodeNeedsApproval WebhookSubscriptionType = "nodeNeedsApproval"
WebhookNodeApproved WebhookSubscriptionType = "nodeApproved"
WebhookNodeKeyExpiringInOneDay WebhookSubscriptionType = "nodeKeyExpiringInOneDay"
WebhookNodeKeyExpired WebhookSubscriptionType = "nodeKeyExpired"
WebhookNodeDeleted WebhookSubscriptionType = "nodeDeleted"
WebhookPolicyUpdate WebhookSubscriptionType = "policyUpdate"
WebhookUserCreated WebhookSubscriptionType = "userCreated"
WebhookUserNeedsApproval WebhookSubscriptionType = "userNeedsApproval"
WebhookUserSuspended WebhookSubscriptionType = "userSuspended"
WebhookUserRestored WebhookSubscriptionType = "userRestored"
WebhookUserDeleted WebhookSubscriptionType = "userDeleted"
WebhookUserApproved WebhookSubscriptionType = "userApproved"
WebhookUserRoleUpdated WebhookSubscriptionType = "userRoleUpdated"
WebhookSubnetIPForwardingNotEnabled WebhookSubscriptionType = "subnetIPForwardingNotEnabled"
WebhookExitNodeIPForwardingNotEnabled WebhookSubscriptionType = "exitNodeIPForwardingNotEnabled"
)

type (
// WebhookProviderType defines the provider type for a Webhook destination.
WebhookProviderType string

// WebhookSubscriptionType defines events in tailscale to subscribe a Webhook to.
WebhookSubscriptionType string

// Webhook type defines a webhook endpoint within a tailnet.
Webhook struct {
EndpointID string `json:"endpointId"`
EndpointURL string `json:"endpointUrl"`
ProviderType WebhookProviderType `json:"providerType"`
CreatorLoginName string `json:"creatorLoginName"`
Created time.Time `json:"created"`
LastModified time.Time `json:"lastModified"`
Subscriptions []WebhookSubscriptionType `json:"subscriptions"`
// Secret is only populated on Webhook creation and after secret rotation.
Secret *string `json:"secret,omitempty"`
}

// CreateWebhookRequest type describes the configuration for creating a Webhook.
CreateWebhookRequest struct {
EndpointURL string `json:"endpointUrl"`
ProviderType WebhookProviderType `json:"providerType"`
Subscriptions []WebhookSubscriptionType `json:"subscriptions"`
}
)

type WebhooksResource struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New - organizes functions into a Resource.

*Client
}

// Create creates a new webhook with the specifications provided in the CreateWebhookRequest.
// Returns a Webhook if successful.
func (wr *WebhooksResource) Create(ctx context.Context, request CreateWebhookRequest) (*Webhook, error) {
req, err := wr.buildRequest(ctx, http.MethodPost, wr.buildTailnetURL("webhooks"), requestBody(request))
if err != nil {
return nil, err
}

var webhook Webhook
return &webhook, wr.do(req, &webhook)
}

// List lists the webhooks in a tailnet.
func (wr *WebhooksResource) List(ctx context.Context) ([]Webhook, error) {
req, err := wr.buildRequest(ctx, http.MethodGet, wr.buildTailnetURL("webhooks"))
if err != nil {
return nil, err
}

resp := make(map[string][]Webhook)
if err = wr.do(req, &resp); err != nil {
return nil, err
}

return resp["webhooks"], nil
}

// Get retrieves a specific webhook.
func (wr *WebhooksResource) Get(ctx context.Context, endpointID string) (*Webhook, error) {
req, err := wr.buildRequest(ctx, http.MethodGet, wr.buildURL("webhooks", endpointID))
if err != nil {
return nil, err
}

var webhook Webhook
return &webhook, wr.do(req, &webhook)
}

// Update updates an existing webhook's subscriptions.
// Returns a Webhook on success.
func (wr *WebhooksResource) Update(ctx context.Context, endpointID string, subscriptions []WebhookSubscriptionType) (*Webhook, error) {
req, err := wr.buildRequest(ctx, http.MethodPatch, wr.buildURL("webhooks", endpointID), requestBody(map[string][]WebhookSubscriptionType{
"subscriptions": subscriptions,
}))
if err != nil {
return nil, err
}

var webhook Webhook
return &webhook, wr.do(req, &webhook)
}

// Delete deletes a specific webhook.
func (wr *WebhooksResource) Delete(ctx context.Context, endpointID string) error {
req, err := wr.buildRequest(ctx, http.MethodDelete, wr.buildURL("webhooks", endpointID))
if err != nil {
return err
}

return wr.do(req, nil)
}

// Test queues a test event to be sent to a specific webhook.
// Sending the test event is an asynchronous operation which will
// typically happen a few seconds after using this method.
func (wr *WebhooksResource) Test(ctx context.Context, endpointID string) error {
req, err := wr.buildRequest(ctx, http.MethodPost, wr.buildURL("webhooks", endpointID, "test"))
if err != nil {
return err
}

return wr.do(req, nil)
}

// RotateSecret rotates the secret associated with a webhook.
// A new secret will be generated and set on the returned Webhook.
func (wr *WebhooksResource) RotateSecret(ctx context.Context, endpointID string) (*Webhook, error) {
req, err := wr.buildRequest(ctx, http.MethodPost, wr.buildURL("webhooks", endpointID, "rotate"))
if err != nil {
return nil, err
}

var webhook Webhook
return &webhook, wr.do(req, &webhook)
}
Loading
Loading