Skip to content
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
165 changes: 165 additions & 0 deletions planetscale/auth_attempt_exports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package planetscale

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"path"
"strconv"
"strings"
"time"
)

type AuthAttemptExportFilters struct {
SourceIPs []string `json:"source_ips,omitempty"`
Branches []string `json:"branches,omitempty"`
Outcomes []string `json:"outcomes,omitempty"`
Usernames []string `json:"usernames,omitempty"`
StartupDatabases []string `json:"startup_databases,omitempty"`
FailureReasons []string `json:"failure_reasons,omitempty"`
BackendRoutes []string `json:"backend_routes,omitempty"`
}

type CreateAuthAttemptExportRequest struct {
Organization string `json:"-"`
StartAt time.Time `json:"start_at"`
EndAt time.Time `json:"end_at"`
Format string `json:"format"`
Filters AuthAttemptExportFilters `json:"filters,omitempty"`
}

type GetAuthAttemptExportRequest struct {
Organization string
Export string
}

type DownloadAuthAttemptExportRequest struct {
Organization string
Export string
}

type AuthAttemptExport struct {
PublicID string `json:"id"`
State string `json:"state"`
Expired bool `json:"expired"`
StartAt time.Time `json:"start_at"`
EndAt time.Time `json:"end_at"`
Filters AuthAttemptExportFilters `json:"filters"`
Format string `json:"format"`
ResolvedBranchPublicIDs []string `json:"resolved_branch_public_ids"`
CreatedAt time.Time `json:"created_at"`
StartedAt *time.Time `json:"started_at"`
GeneratedAt *time.Time `json:"generated_at"`
FinishedAt *time.Time `json:"finished_at"`
ExpiresAt *time.Time `json:"expires_at"`
FailureReason string `json:"failure_reason"`
FailureDetail string `json:"failure_detail"`
RecoveryHint string `json:"recovery_hint"`
RetryAfter time.Duration `json:"-"`
}

type AuthAttemptExportsService interface {
CreateExport(context.Context, *CreateAuthAttemptExportRequest) (*AuthAttemptExport, error)
GetExport(context.Context, *GetAuthAttemptExportRequest) (*AuthAttemptExport, error)
DownloadExport(context.Context, *DownloadAuthAttemptExportRequest) (io.ReadCloser, error)
}

type authAttemptExportsService struct {
client *Client
}

var _ AuthAttemptExportsService = &authAttemptExportsService{}

func (s *authAttemptExportsService) CreateExport(ctx context.Context, createReq *CreateAuthAttemptExportRequest) (*AuthAttemptExport, error) {
req, err := s.client.newRequest(http.MethodPost, authAttemptExportsAPIPath(createReq.Organization), createReq)
if err != nil {
return nil, fmt.Errorf("error creating http request: %w", err)
}

export := &AuthAttemptExport{}
headers, err := s.client.doWithHeaders(ctx, req, export)
if err != nil {
return nil, err
}
export.RetryAfter = parseRetryAfter(headers.Get("Retry-After"))
return export, nil
}

func (s *authAttemptExportsService) GetExport(ctx context.Context, getReq *GetAuthAttemptExportRequest) (*AuthAttemptExport, error) {
req, err := s.client.newRequest(http.MethodGet, authAttemptExportAPIPath(getReq.Organization, getReq.Export), nil)
if err != nil {
return nil, fmt.Errorf("error creating http request: %w", err)
}

export := &AuthAttemptExport{}
headers, err := s.client.doWithHeaders(ctx, req, export)
if err != nil {
if expiredErr := authAttemptExportExpiredError(err); expiredErr != nil {
return nil, expiredErr
}
return nil, err
}
export.RetryAfter = parseRetryAfter(headers.Get("Retry-After"))
return export, nil
}

func (s *authAttemptExportsService) DownloadExport(ctx context.Context, downloadReq *DownloadAuthAttemptExportRequest) (io.ReadCloser, error) {
reqPath := path.Join(authAttemptExportAPIPath(downloadReq.Organization, downloadReq.Export), "download")
req, err := s.client.newRequest(http.MethodGet, reqPath, nil)
if err != nil {
return nil, fmt.Errorf("error creating http request: %w", err)
}
body, err := s.client.downloadSignedURL(ctx, req)
if err != nil {
if expiredErr := authAttemptExportExpiredError(err); expiredErr != nil {
return nil, expiredErr
}
return nil, fmt.Errorf("downloading auth attempt export: %w", err)
}
return body, nil
}

func authAttemptExportExpiredError(err error) error {
var apiErr *Error
if !errors.As(err, &apiErr) || apiErr.Meta["http_status"] != http.StatusText(http.StatusGone) {
return nil
}

export := &AuthAttemptExport{}
if json.Unmarshal([]byte(apiErr.Meta["body"]), export) != nil || !export.Expired || export.PublicID == "" || export.RecoveryHint == "" {
return nil
}
return fmt.Errorf("auth attempt export %s expired: %s", export.PublicID, export.RecoveryHint)
}

func authAttemptExportsAPIPath(org string) string {
return path.Join("v1/organizations", org, "auth-attempt-exports")
}

func authAttemptExportAPIPath(org, export string) string {
return path.Join(authAttemptExportsAPIPath(org), export)
}

func parseRetryAfter(value string) time.Duration {
value = strings.TrimSpace(value)
if value == "" {
return 0
}

seconds, err := strconv.ParseUint(value, 10, 64)
if err == nil {
if seconds > 0 && seconds <= uint64((time.Duration(1<<63-1))/time.Second) {
return time.Duration(seconds) * time.Second
}
return 0
}

when, err := http.ParseTime(value)
if err != nil || !when.After(time.Now()) {
return 0
}
return time.Until(when)
}
183 changes: 183 additions & 0 deletions planetscale/backup_policies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package planetscale

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

// BackupPolicy represents a scheduled backup policy for a database.
type BackupPolicy struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Target string `json:"target"`
RetentionValue int `json:"retention_value"`
RetentionUnit string `json:"retention_unit"`
FrequencyValue int `json:"frequency_value"`
FrequencyUnit string `json:"frequency_unit"`
ScheduleTime string `json:"schedule_time"`
ScheduleDay *int `json:"schedule_day"`
ScheduleWeek *int `json:"schedule_week"`
Required bool `json:"required"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastRanAt *time.Time `json:"last_ran_at"`
NextRunAt *time.Time `json:"next_run_at"`
}

type backupPoliciesResponse struct {
Policies []*BackupPolicy `json:"data"`
}

// ListBackupPoliciesRequest encapsulates listing backup policies for a database.
type ListBackupPoliciesRequest struct {
Organization string
Database string
}

// GetBackupPolicyRequest encapsulates getting a single backup policy.
type GetBackupPolicyRequest struct {
Organization string
Database string
Policy string
}

// CreateBackupPolicyRequest encapsulates creating a backup policy.
type CreateBackupPolicyRequest struct {
Organization string `json:"-"`
Database string `json:"-"`
Name string `json:"name,omitempty"`
Target string `json:"target"`
RetentionValue int `json:"retention_value"`
RetentionUnit string `json:"retention_unit"`
FrequencyValue int `json:"frequency_value"`
FrequencyUnit string `json:"frequency_unit"`
ScheduleTime string `json:"schedule_time"`
ScheduleDay *int `json:"schedule_day,omitempty"`
ScheduleWeek *int `json:"schedule_week,omitempty"`
}

// UpdateBackupPolicyRequest encapsulates updating a backup policy.
type UpdateBackupPolicyRequest struct {
Organization string `json:"-"`
Database string `json:"-"`
Policy string `json:"-"`
Name *string `json:"name,omitempty"`
Target *string `json:"target,omitempty"`
RetentionValue *int `json:"retention_value,omitempty"`
RetentionUnit *string `json:"retention_unit,omitempty"`
FrequencyValue *int `json:"frequency_value,omitempty"`
FrequencyUnit *string `json:"frequency_unit,omitempty"`
ScheduleTime *string `json:"schedule_time,omitempty"`
ScheduleDay *int `json:"schedule_day,omitempty"`
ScheduleWeek *int `json:"schedule_week,omitempty"`
}

// DeleteBackupPolicyRequest encapsulates deleting a backup policy.
type DeleteBackupPolicyRequest struct {
Organization string
Database string
Policy string
}

// BackupPoliciesService is an interface for the PlanetScale backup policies API.
type BackupPoliciesService interface {
List(context.Context, *ListBackupPoliciesRequest, ...ListOption) ([]*BackupPolicy, error)
Get(context.Context, *GetBackupPolicyRequest) (*BackupPolicy, error)
Create(context.Context, *CreateBackupPolicyRequest) (*BackupPolicy, error)
Update(context.Context, *UpdateBackupPolicyRequest) (*BackupPolicy, error)
Delete(context.Context, *DeleteBackupPolicyRequest) error
}

type backupPoliciesService struct {
client *Client
}

var _ BackupPoliciesService = &backupPoliciesService{}

func NewBackupPoliciesService(client *Client) *backupPoliciesService {
return &backupPoliciesService{client: client}
}

func (s *backupPoliciesService) List(ctx context.Context, listReq *ListBackupPoliciesRequest, opts ...ListOption) ([]*BackupPolicy, error) {
listOpts := defaultListOptions(WithPerPage(100))
for _, opt := range opts {
if err := opt(listOpts); err != nil {
return nil, err
}
}

req, err := s.client.newRequest(http.MethodGet, backupPoliciesAPIPath(listReq.Organization, listReq.Database), nil, WithQueryParams(*listOpts.URLValues))
if err != nil {
return nil, fmt.Errorf("error creating request for list backup policies: %w", err)
}

resp := &backupPoliciesResponse{}
if err := s.client.do(ctx, req, &resp); err != nil {
return nil, err
}

return resp.Policies, nil
}

func (s *backupPoliciesService) Get(ctx context.Context, getReq *GetBackupPolicyRequest) (*BackupPolicy, error) {
req, err := s.client.newRequest(http.MethodGet, backupPolicyAPIPath(getReq.Organization, getReq.Database, getReq.Policy), nil)
if err != nil {
return nil, fmt.Errorf("error creating request for get backup policy: %w", err)
}

policy := &BackupPolicy{}
if err := s.client.do(ctx, req, &policy); err != nil {
return nil, err
}

return policy, nil
}

func (s *backupPoliciesService) Create(ctx context.Context, createReq *CreateBackupPolicyRequest) (*BackupPolicy, error) {
req, err := s.client.newRequest(http.MethodPost, backupPoliciesAPIPath(createReq.Organization, createReq.Database), createReq)
if err != nil {
return nil, fmt.Errorf("error creating request for create backup policy: %w", err)
}

policy := &BackupPolicy{}
if err := s.client.do(ctx, req, &policy); err != nil {
return nil, err
}

return policy, nil
}

func (s *backupPoliciesService) Update(ctx context.Context, updateReq *UpdateBackupPolicyRequest) (*BackupPolicy, error) {
req, err := s.client.newRequest(http.MethodPatch, backupPolicyAPIPath(updateReq.Organization, updateReq.Database, updateReq.Policy), updateReq)
if err != nil {
return nil, fmt.Errorf("error creating request for update backup policy: %w", err)
}

policy := &BackupPolicy{}
if err := s.client.do(ctx, req, &policy); err != nil {
return nil, err
}

return policy, nil
}

func (s *backupPoliciesService) Delete(ctx context.Context, deleteReq *DeleteBackupPolicyRequest) error {
req, err := s.client.newRequest(http.MethodDelete, backupPolicyAPIPath(deleteReq.Organization, deleteReq.Database, deleteReq.Policy), nil)
if err != nil {
return fmt.Errorf("error creating request for delete backup policy: %w", err)
}

return s.client.do(ctx, req, nil)
}

func backupPoliciesAPIPath(org, db string) string {
return path.Join("v1/organizations", org, "databases", db, "backup-policies")
}

func backupPolicyAPIPath(org, db, policy string) string {
return path.Join(backupPoliciesAPIPath(org, db), policy)
}
Loading
Loading