-
Notifications
You must be signed in to change notification settings - Fork 31
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -91,6 +92,7 @@ func (c *Client) init() { | |
c.http = &http.Client{Timeout: defaultHttpClientTimeout} | ||
} | ||
c.devices = &DevicesResource{c} | ||
c.webhooks = &WebhooksResource{c} | ||
}) | ||
} | ||
|
||
|
@@ -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 | ||
|
@@ -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 { | ||
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))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
package tailscale | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
There was a problem hiding this comment.
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)