Skip to content

Commit

Permalink
feat(automation_tokens): Add support for automation-tokens (#547)
Browse files Browse the repository at this point in the history
* feat(automation_tokens): add support for automation-tokens

* tidy up comments
  • Loading branch information
Jesse-Hills authored Sep 24, 2024
1 parent bd8b494 commit 8283d41
Show file tree
Hide file tree
Showing 6 changed files with 513 additions and 0 deletions.
187 changes: 187 additions & 0 deletions fastly/automation_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package fastly

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

// AutomationTokenRole is used to match possible automation token roles.
type AutomationTokenRole string

const (
// BillingRole allows view access to basic information about service configurations,
// invoices, and account billing history.
BillingRole AutomationTokenRole = "billing"
// EngineerRole allows creating services and managing their configurations.
EngineerRole AutomationTokenRole = "engineer"
// UserRole allows view access to basic information about service configurations,
// and controls.
UserRole AutomationTokenRole = "user"
)

// AutomationTokenPaginator is used for pagination on AutomationToken endpoints.
// as they return JSONAPI data.
type AutomationTokenPaginator struct {
Data []*AutomationToken `mapstructure:"data"`
Meta AutomationTokenPaginatorMeta `mapstructure:"meta"`
}

// AutomationTokenPaginatorMeta represents the metadata for an AutomationTokenPaginator.
type AutomationTokenPaginatorMeta struct {
CurrentPage int `mapstructure:"current_page"`
PerPage int `mapstructure:"per_page"`
RecordCount int `mapstructure:"record_count"`
TotalPages int `mapstructure:"total_pages"`
}

// AutomationToken represents an API token which allows non-human clients to
// authenticate requests to the Fastly API.
type AutomationToken struct {
AccessToken *string `mapstructure:"access_token"`
CreatedAt *time.Time `mapstructure:"created_at"`
ExpiresAt *time.Time `mapstructure:"expires_at"`
IP *string `mapstructure:"ip"`
LastUsedAt *time.Time `mapstructure:"last_used_at"`
Name *string `mapstructure:"name"`
Role *AutomationTokenRole `mapstructure:"role"`
Scope *TokenScope `mapstructure:"scope"`
Services []string `mapstructure:"services"`
TLSAccess *bool `mapstructure:"tls_access"`
TokenID *string `mapstructure:"id"`
UserID *string `mapstructure:"user_id"`
CustomerID *string `mapstructure:"customer_id"`
}

// GetAutomationTokensInput is used as input to the GetAutomationTokens function.
type GetAutomationTokensInput struct {
// Page is the current page.
Page *int
// PerPage is the number of records per page.
PerPage *int
}

// GetAutomationTokens retrieves all resources.
func (c *Client) GetAutomationTokens(i *GetAutomationTokensInput) *ListPaginator[AutomationTokenPaginator] {
input := ListOpts{}
if i.Page != nil {
input.Page = *i.Page
}
if i.PerPage != nil {
input.PerPage = *i.PerPage
}
return NewPaginator[AutomationTokenPaginator](c, input, "/automation-tokens")
}

// ListAutomationTokens retrieves all resources.
func (c *Client) ListAutomationTokens() ([]*AutomationToken, error) {
p := c.GetAutomationTokens(&GetAutomationTokensInput{})
var results []*AutomationToken
for p.HasNext() {
data, err := p.GetNext()
if err != nil {
return nil, fmt.Errorf("failed to get next page (remaining: %d): %s", p.Remaining(), err)
}

for _, t := range data {
results = append(results, t.Data...)
}
}
return results, nil
}

// GetAutomationTokenInput is used as input to the GetAutomationToken function.
type GetAutomationTokenInput struct {
// TokenID is an alphanumeric string identifying the token (required).
TokenID string
}

// GetAutomationToken retrieves a specific resource by ID.
func (c *Client) GetAutomationToken(i *GetAutomationTokenInput) (*AutomationToken, error) {
if i.TokenID == "" {
return nil, ErrMissingTokenID
}

path := ToSafeURL("automation-tokens", i.TokenID)

resp, err := c.Get(path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var t *AutomationToken
if err := decodeBodyMap(resp.Body, &t); err != nil {
return nil, err
}
return t, nil
}

// CreateAutomationTokenInput is used as input to the CreateAutomationToken function.
type CreateAutomationTokenInput struct {
// ExpiresAt is a time-stamp (UTC) of when the token will expire.
ExpiresAt time.Time `json:"expires_at" url:"expires_at,omitempty"`
// Name is the name of the token.
Name string `json:"name" url:"name,omitempty"`
// Password is the token password.
Password *string `json:"-" url:"password,omitempty"`
// Role is the role on the token (billing, engineer, user).
Role AutomationTokenRole `json:"role" url:"role,omitempty"`
// Scope is a space-delimited list of authorization scope (global, purge_select, purge_all, global).
Scope *TokenScope `json:"scope,omitempty" url:"scope,omitempty"`
// Services is a list of alphanumeric strings identifying services.
// If no services are specified, the token will have access to all services on the account.
Services []string `json:"services" url:"services,omitempty,brackets"`
// Username is the email of the user the token is assigned to.
Username *string `json:"-" url:"username,omitempty"`
// TLSAccess indicates whether TLS access is enabled for the token.
TLSAccess bool `json:"tls_access" url:"tls_access,omitempty"`
}

// CreateAutomationToken creates a new resource.
//
// Requires sudo capability for the token being used.
func (c *Client) CreateAutomationToken(i *CreateAutomationTokenInput) (*AutomationToken, error) {
_, err := c.PostForm("/sudo", i, nil)
if err != nil {
return nil, err
}

resp, err := c.PostJSON("/automation-tokens", i, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var t *AutomationToken
if err := decodeBodyMap(resp.Body, &t); err != nil {
return nil, err
}
return t, nil
}

// DeleteAutomationTokenInput is used as input to the DeleteAutomationToken function.
type DeleteAutomationTokenInput struct {
// TokenID is an alphanumeric string identifying a token (required).
TokenID string
}

// DeleteAutomationToken deletes the specified resource.
func (c *Client) DeleteAutomationToken(i *DeleteAutomationTokenInput) error {
if i.TokenID == "" {
return ErrMissingTokenID
}

path := ToSafeURL("tokens", i.TokenID)

resp, err := c.Delete(path, nil)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
return ErrNotOK
}
return nil
}
83 changes: 83 additions & 0 deletions fastly/automation_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package fastly

import (
"testing"
)

func TestClient_ListAutomationTokens(t *testing.T) {
t.Parallel()

var tokens []*AutomationToken
var err error
record(t, "automation_tokens/list", func(c *Client) {
tokens, err = c.ListAutomationTokens()
})
if err != nil {
t.Fatal(err)
}
if len(tokens) < 1 {
t.Errorf("bad tokens: %v", tokens)
}
}

func TestClient_GetAutomationToken(t *testing.T) {
t.Parallel()

input := &GetAutomationTokenInput{
TokenID: "XXXXXXXXXXXXXXXXXXXXXX",
}

var token *AutomationToken
var err error
record(t, "automation_tokens/get", func(c *Client) {
token, err = c.GetAutomationToken(input)
})
if err != nil {
t.Fatal(err)
}
t.Logf("%+v", token)
}

func TestClient_CreateAutomationToken(t *testing.T) {
t.Parallel()

input := &CreateAutomationTokenInput{
Name: "my-test-token",
Role: EngineerRole,
Scope: ToPointer(GlobalScope),
Username: ToPointer("XXXXXXXXXXXXXXXXXXXXXX"),
Password: ToPointer("XXXXXXXXXXXXXXXXXXXXXX"),
}

var token *AutomationToken
var err error
record(t, "automation_tokens/create", func(c *Client) {
token, err = c.CreateAutomationToken(input)
})
if err != nil {
t.Fatal(err)
}

if *token.Name != input.Name {
t.Errorf("returned invalid name, got %s, want %s", *token.Name, input.Name)
}
if *token.Scope != *input.Scope {
t.Errorf("returned invalid scope, got %s, want %s", *token.Scope, *input.Scope)
}
}

func TestClient_DeleteAutomationToken(t *testing.T) {
t.Parallel()

input := &DeleteAutomationTokenInput{
TokenID: "XXXXXXXXXXXXXXXXXXXXXX",
}

var err error
record(t, "automation_tokens/delete", func(c *Client) {
err = c.DeleteAutomationToken(input)
})
if err != nil {
t.Fatal(err)
}
}
110 changes: 110 additions & 0 deletions fastly/fixtures/automation_tokens/create.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
version: 1
interactions:
- request:
body: name=my-test-token&password=XXXXXXXXXXXXXXXXXXXXXX&role=engineer&scope=global&username=XXXXXXXXXXXXXXXXXXXXXX
form:
name:
- my-test-token
password:
- XXXXXXXXXXXXXXXXXXXXXX
role:
- engineer
scope:
- global
username:
- XXXXXXXXXXXXXXXXXXXXXX
headers:
Content-Type:
- application/x-www-form-urlencoded
User-Agent:
- FastlyGo/9.8.0 (+github.com/fastly/go-fastly; go1.22.2)
url: https://api.fastly.com/sudo
method: POST
response:
body: '{"expiry_time":"2024-09-21T04:01:00+00:00"}'
headers:
Accept-Ranges:
- bytes
Cache-Control:
- no-store
Content-Type:
- application/json
Date:
- Sat, 21 Sep 2024 03:56:00 GMT
Fastly-Ratelimit-Remaining:
- "975"
Fastly-Ratelimit-Reset:
- "1726891200"
Pragma:
- no-cache
Server:
- control-gateway
Status:
- 200 OK
Strict-Transport-Security:
- max-age=31536000
Vary:
- Accept-Encoding
Via:
- 1.1 varnish, 1.1 varnish
X-Cache:
- MISS, MISS
X-Cache-Hits:
- 0, 0
X-Served-By:
- cache-chi-kigq8000058-CHI, cache-per12629-PER
X-Timer:
- S1726890960.858190,VS0,VE534
status: 200 OK
code: 200
duration: ""
- request:
body: '{"expires_at":"0001-01-01T00:00:00Z","name":"my-test-token","role":"engineer","scope":"global","services":null,"tls_access":false}'
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- FastlyGo/9.8.0 (+github.com/fastly/go-fastly; go1.22.2)
url: https://api.fastly.com/automation-tokens
method: POST
response:
body: |
{"id":"XXXXXXXXXXXXXXXXXXXXXX","services":[],"name":"my-test-token","role":"engineer","access_token":"XXXXXXXXXXXXXXXXXXXXXX","scope":"global","ip":"","created_at":"2024-09-21T03:56:00Z","last_used_at":"0001-01-01T00:00:00Z","expires_at":null,"user_agent":""}
headers:
Accept-Ranges:
- bytes
Cache-Control:
- no-store
Content-Length:
- "270"
Content-Type:
- application/json
Date:
- Sat, 21 Sep 2024 03:56:01 GMT
Fastly-Ratelimit-Remaining:
- "993"
Fastly-Ratelimit-Reset:
- "1726891200"
Pragma:
- no-cache
Server:
- control-gateway
Strict-Transport-Security:
- max-age=31536000
Via:
- 1.1 varnish, 1.1 varnish
X-Cache:
- MISS, MISS
X-Cache-Hits:
- 0, 0
X-Served-By:
- cache-chi-kigq8000082-CHI, cache-per12629-PER
X-Timer:
- S1726890960.404951,VS0,VE753
status: 200 OK
code: 200
duration: ""
Loading

0 comments on commit 8283d41

Please sign in to comment.