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

tailscale: polish API and documentation in preparation for tagging re… #106

Merged
merged 1 commit into from
Aug 28, 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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/tailscale/tailscale-client-go)](https://goreportcard.com/report/github.com/tailscale/tailscale-client-go)
![Github Actions](https://github.com/tailscale/tailscale-client-go/actions/workflows/ci.yml/badge.svg?branch=master)

DEPRECATED - V1 is no longer being maintained. The [V2 SDK](v2) provides a more complete wrapper around the V2 [Tailscale API](https://tailscale.com/api).

---

A client implementation for the [Tailscale](https://tailscale.com) HTTP API.
For more details, please see [API documentation](https://github.com/tailscale/tailscale/blob/main/api.md).

A [V2](v2) implementation of the client is under active development, use at your own risk and expect breaking changes.

# Example

```go
Expand Down
6 changes: 3 additions & 3 deletions tailscale/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,14 +537,14 @@ type setACLParams struct {
type SetACLOption func(p *setACLParams)

// WithETag allows passing an ETag value with Set ACL API call that
// will be used in the `If-Match` HTTP request header.
// will be used in the "If-Match" HTTP request header.
func WithETag(etag string) SetACLOption {
return func(p *setACLParams) {
p.headers["If-Match"] = fmt.Sprintf("%q", etag)
}
}

// SetACL sets the ACL for the given tailnet. `acl` can either be an [ACL],
// SetACL sets the ACL for the given tailnet. "acl" can either be an [ACL],
// or a HuJSON string.
func (c *Client) SetACL(ctx context.Context, acl any, opts ...SetACLOption) error {
const uriFmt = "/api/v2/tailnet/%s/acl"
Expand Down Expand Up @@ -574,7 +574,7 @@ func (c *Client) SetACL(ctx context.Context, acl any, opts ...SetACLOption) erro
return c.performRequest(req, nil)
}

// ValidateACL validates the provided ACL via the API. `acl` can either be an [ACL],
// ValidateACL validates the provided ACL via the API. "acl" can either be an [ACL],
// or a HuJSON string.
func (c *Client) ValidateACL(ctx context.Context, acl any) error {
const uriFmt = "/api/v2/tailnet/%s/acl/validate"
Expand Down
62 changes: 62 additions & 0 deletions v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# tailscale-client-go/v2

[![Go Reference](https://pkg.go.dev/badge/github.com/tailscale/tailscale-client-go/v2.svg)](https://pkg.go.dev/github.com/tailscale/tailscale-client-go/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/tailscale/tailscale-client-go/v2)](https://goreportcard.com/report/github.com/tailscale/tailscale-client-go/v2)
![Github Actions](https://github.com/tailscale/tailscale-client-go/actions/workflows/ci.yml/badge.svg?branch=main)

A client implementation for the [Tailscale](https://tailscale.com) HTTP API.
For more details, please see [API documentation](https://tailscale.com/api).

## Example (Using API Key)

```go
package main

import (
"context"
"log"
"os"

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

func main() {
apiKey := os.Getenv("TAILSCALE_API_KEY")
tailnet := os.Getenv("TAILSCALE_TAILNET")

&tsclient.Client{
APIKey: apiKey,
Tailnet: tailnet,
}

devices, err := client.Devices().List(context.Background())
}
```

## Example (Using OAuth)

```go
package main

import (
"context"
"log"
"os"

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

func main() {
oauthClientID := os.Getenv("TAILSCALE_OAUTH_CLIENT_ID")
tailnet := os.Getenv("TAILSCALE_OAUTH_CLIENT_SECRET")
oauthScopes := []string{"all:write"}

&tsclient.Client{
APIKey: apiKey,
Tailnet: tailnet,
}
clientV2.UseOAuth(oauthClientID, oauthClientSecret, oauthScopes)

devices, err := client.Devices().List(context.Background())
}
```
38 changes: 23 additions & 15 deletions v2/client.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Package tsclient contains a basic implementation of a client for the Tailscale HTTP api. Documentation is here:
// https://tailscale.com/api
//
// WARNING - this v2 implementation is under active development, use at your own risk and expect breaking changes.
package tsclient

import (
Expand All @@ -21,11 +19,11 @@ import (
)

type (
// Client type is used to perform actions against the Tailscale API.
// Client is used to perform actions against the Tailscale API.
Client struct {
// BaseURL is the base URL for accessing the Tailscale API server. Defaults to https://api.tailscale.com.
BaseURL *url.URL
// UserAgent configures the User-Agent HTTP header for requests, defaults to "tailscale-client-go"
// UserAgent configures the User-Agent HTTP header for requests. Defaults to "tailscale-client-go".
UserAgent string
// APIKey allows specifying an APIKey to use for authentication.
APIKey string
Expand All @@ -34,7 +32,7 @@ type (

// HTTP is the [http.Client] to use for requests to the API server.
// If not specified, a new [http.Client] with a Timeout of 1 minute will be used.
// This will be ignored if using [Client.UseOAuth].
// This will be ignored if using [Client].UseOAuth.
HTTP *http.Client

initOnce sync.Once
Expand Down Expand Up @@ -66,6 +64,10 @@ type (
}
)

const defaultContentType = "application/json"
const defaultHttpClientTimeout = time.Minute
const defaultUserAgent = "tailscale-client-go"

var defaultBaseURL *url.URL
var oauthRelTokenURL *url.URL

Expand All @@ -82,14 +84,10 @@ func init() {
}
}

const defaultContentType = "application/json"
const defaultHttpClientTimeout = time.Minute
const defaultUserAgent = "tailscale-client-go"

// init returns a new instance of the Client type that will perform operations against a chosen tailnet and will
// provide the apiKey for authorization. Additional options can be provided, see ClientOption for more details.
// provide the apiKey for authorization.
//
// To use OAuth Client credentials, call [UseOAuth].
// To use OAuth Client credentials, call [Client].UseOAuth.
func (c *Client) init() {
c.initOnce.Do(func() {
if c.BaseURL == nil {
Expand All @@ -115,7 +113,7 @@ func (c *Client) init() {
}

// UseOAuth configures the client to use the specified OAuth credentials.
// If [Client.HTTP] was previously specified, this replaces it.
// If [Client].HTTP was previously specified, this replaces it.
func (c *Client) UseOAuth(clientID, clientSecret string, scopes []string) {
oauthConfig := clientcredentials.Config{
ClientID: clientID,
Expand All @@ -129,51 +127,61 @@ func (c *Client) UseOAuth(clientID, clientSecret string, scopes []string) {
c.HTTP.Timeout = defaultHttpClientTimeout
}

// Contacts() provides access to https://tailscale.com/api#tag/contacts.
func (c *Client) Contacts() *ContactsResource {
c.init()
return c.contacts
}

// DevicePosture provides access to https://tailscale.com/api#tag/deviceposture.
func (c *Client) DevicePosture() *DevicePostureResource {
c.init()
return c.devicePosture
}

// Devices provides access to https://tailscale.com/api#tag/devices.
func (c *Client) Devices() *DevicesResource {
c.init()
return c.devices
}

// DNS provides access to https://tailscale.com/api#tag/dns.
func (c *Client) DNS() *DNSResource {
c.init()
return c.dns
}

// Keys provides access to https://tailscale.com/api#tag/keys.
func (c *Client) Keys() *KeysResource {
c.init()
return c.keys
}

// Logging provides access to https://tailscale.com/api#tag/logging.
func (c *Client) Logging() *LoggingResource {
c.init()
return c.logging
}

// PolicyFile provides access to https://tailscale.com/api#tag/policyfile.
func (c *Client) PolicyFile() *PolicyFileResource {
c.init()
return c.policyFile
}

// TailnetSettings provides access to https://tailscale.com/api#tag/tailnetsettings.
func (c *Client) TailnetSettings() *TailnetSettingsResource {
c.init()
return c.tailnetSettings
}

// Users provides access to https://tailscale.com/api#tag/users.
func (c *Client) Users() *UsersResource {
c.init()
return c.users
}

// Webhooks provides access to https://tailscale.com/api#tag/webhooks.
func (c *Client) Webhooks() *WebhooksResource {
c.init()
return c.webhooks
Expand Down Expand Up @@ -341,8 +349,8 @@ func IsNotFound(err error) bool {
return false
}

// ErrorData returns the contents of the APIError.Data field from the provided error if it is of type APIError. Returns
// a nil slice if the given error is not of type APIError.
// ErrorData returns the contents of the [APIError].Data field from the provided error if it is of type [APIError].
// Returns a nil slice if the given error is not of type [APIError].
func ErrorData(err error) []APIErrorData {
var apiErr APIError
if errors.As(err, &apiErr) {
Expand All @@ -352,7 +360,7 @@ func ErrorData(err error) []APIErrorData {
return nil
}

// Duration type wraps a time.Duration, allowing it to be JSON marshalled as a string like "20h" rather than
// Duration wraps a [time.Duration], allowing it to be JSON marshalled as a string like "20h" rather than
// a numeric value.
type Duration time.Duration

Expand Down
5 changes: 3 additions & 2 deletions v2/contacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
)

// ContactsResource provides access to https://tailscale.com/api#tag/contacts.
type ContactsResource struct {
*Client
}
Expand Down Expand Up @@ -41,7 +42,7 @@ type (
}
)

// Contacts retieves the contact information for a tailnet.
// Get retieves the [Contacts] for the tailnet.
func (cr *ContactsResource) Get(ctx context.Context) (*Contacts, error) {
req, err := cr.buildRequest(ctx, http.MethodGet, cr.buildTailnetURL("contacts"))
if err != nil {
Expand All @@ -52,7 +53,7 @@ func (cr *ContactsResource) Get(ctx context.Context) (*Contacts, error) {
return &contacts, cr.do(req, &contacts)
}

// UpdateContact updates the email for the specified ContactType within the tailnet.
// Update updates the email for the specified [ContactType] within the tailnet.
// If the email address changes, the system will send a verification email to confirm the change.
func (cr *ContactsResource) Update(ctx context.Context, contactType ContactType, contact UpdateContactRequest) error {
req, err := cr.buildRequest(ctx, http.MethodPatch, cr.buildTailnetURL("contacts", contactType), requestBody(contact))
Expand Down
7 changes: 4 additions & 3 deletions v2/device_posture.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
)

// DevicePostureResource provides access to https://tailscale.com/api#tag/deviceposture.
type DevicePostureResource struct {
*Client
}
Expand Down Expand Up @@ -50,7 +51,7 @@ type (
}
)

// List lists all configured [PostureIntegration]s.
// List lists every configured [PostureIntegration].
func (pr *DevicePostureResource) ListIntegrations(ctx context.Context) ([]PostureIntegration, error) {
req, err := pr.buildRequest(ctx, http.MethodGet, pr.buildTailnetURL("posture", "integrations"))
if err != nil {
Expand Down Expand Up @@ -88,7 +89,7 @@ func (pr *DevicePostureResource) UpdateIntegration(ctx context.Context, id strin
return &resp, pr.do(req, &resp)
}

// DeleteIntegration deletes the PostureIntegration identified by id.
// DeleteIntegration deletes the posture integration identified by id.
func (pr *DevicePostureResource) DeleteIntegration(ctx context.Context, id string) error {
req, err := pr.buildRequest(ctx, http.MethodDelete, pr.buildURL("posture", "integrations", id))
if err != nil {
Expand All @@ -98,7 +99,7 @@ func (pr *DevicePostureResource) DeleteIntegration(ctx context.Context, id strin
return pr.do(req, nil)
}

// GetIntegration gets the PostureIntegration identified by id.
// GetIntegration gets the posture integration identified by id.
func (pr *DevicePostureResource) GetIntegration(ctx context.Context, id string) (*PostureIntegration, error) {
req, err := pr.buildRequest(ctx, http.MethodGet, pr.buildURL("posture", "integrations", id))
if err != nil {
Expand Down
9 changes: 5 additions & 4 deletions v2/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"
)

// DevicesResource provides access to https://tailscale.com/api#tag/devices.
type DevicesResource struct {
*Client
}
Expand Down Expand Up @@ -63,7 +64,7 @@ type Device struct {
UpdateAvailable bool `json:"updateAvailable"`
}

// Get gets a single device
// Get gets the [Device] identified by deviceID.
func (dr *DevicesResource) Get(ctx context.Context, deviceID string) (*Device, error) {
req, err := dr.buildRequest(ctx, http.MethodGet, dr.buildURL("device", deviceID))
if err != nil {
Expand All @@ -74,7 +75,7 @@ func (dr *DevicesResource) Get(ctx context.Context, deviceID string) (*Device, e
return &result, dr.do(req, &result)
}

// List lists the devices in a tailnet.
// List lists every [Device] in the tailnet.
func (dr *DevicesResource) List(ctx context.Context) ([]Device, error) {
req, err := dr.buildRequest(ctx, http.MethodGet, dr.buildTailnetURL("devices"))
if err != nil {
Expand Down Expand Up @@ -102,7 +103,7 @@ func (dr *DevicesResource) SetAuthorized(ctx context.Context, deviceID string, a
return dr.do(req, nil)
}

// Delete deletes the device given its deviceID.
// Delete deletes the device identified by deviceID.
func (dr *DevicesResource) Delete(ctx context.Context, deviceID string) error {
req, err := dr.buildRequest(ctx, http.MethodDelete, dr.buildURL("device", deviceID))
if err != nil {
Expand All @@ -112,7 +113,7 @@ func (dr *DevicesResource) Delete(ctx context.Context, deviceID string) error {
return dr.do(req, nil)
}

// SetTags updates the tags of a target device.
// SetTags updates the tags of the device identified by deviceID.
func (dr *DevicesResource) SetTags(ctx context.Context, deviceID string, tags []string) error {
req, err := dr.buildRequest(ctx, http.MethodPost, dr.buildURL("device", deviceID, "tags"), requestBody(map[string][]string{
"tags": tags,
Expand Down
Loading
Loading