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

feat(automation_tokens): Add support for automation-tokens #547

Merged
merged 2 commits into from
Sep 24, 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
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)
kpfleming marked this conversation as resolved.
Show resolved Hide resolved
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
Loading