Initial AS213905 SDK release
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Go SDK
|
||||
|
||||
```bash
|
||||
go get git.datacoria.com/Phylex/as213905-sdk/go
|
||||
```
|
||||
|
||||
```go
|
||||
client := as213905.New(os.Getenv("AS213905_API_KEY"))
|
||||
org, err := client.Organization(context.Background())
|
||||
```
|
||||
|
||||
The client is safe for concurrent use. Use `NewWithOptions` to supply a custom
|
||||
base URL, HTTP client or user agent.
|
||||
@@ -0,0 +1,258 @@
|
||||
package as213905
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://as213905.com"
|
||||
|
||||
type Options struct {
|
||||
BaseURL string
|
||||
HTTPClient *http.Client
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
apiKey string
|
||||
base *url.URL
|
||||
http *http.Client
|
||||
userAgent string
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Status int
|
||||
Code string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
RequiredScope string `json:"required_scope"`
|
||||
RetryAfter int `json:"retry_after"`
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
if e.Message != "" {
|
||||
return fmt.Sprintf("as213905: %s (%d): %s", e.Code, e.Status, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("as213905: %s (%d)", e.Code, e.Status)
|
||||
}
|
||||
|
||||
func New(apiKey string) *Client {
|
||||
c, err := NewWithOptions(apiKey, Options{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func NewWithOptions(apiKey string, opts Options) (*Client, error) {
|
||||
if strings.TrimSpace(apiKey) == "" {
|
||||
return nil, errors.New("as213905: api key is required")
|
||||
}
|
||||
base := opts.BaseURL
|
||||
if base == "" {
|
||||
base = defaultBaseURL
|
||||
}
|
||||
u, err := url.Parse(strings.TrimRight(base, "/"))
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
return nil, errors.New("as213905: invalid base URL")
|
||||
}
|
||||
hc := opts.HTTPClient
|
||||
if hc == nil {
|
||||
hc = &http.Client{Timeout: 30 * time.Second}
|
||||
}
|
||||
ua := opts.UserAgent
|
||||
if ua == "" {
|
||||
ua = "as213905-go/1.1.0"
|
||||
}
|
||||
return &Client{apiKey: apiKey, base: u, http: hc, userAgent: ua}, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path, organizationID string, body, out any) error {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader = bytes.NewReader(payload)
|
||||
}
|
||||
rel, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := c.base.ResolveReference(rel)
|
||||
req, err := http.NewRequestWithContext(ctx, method, u.String(), reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if organizationID != "" {
|
||||
req.Header.Set("X-Organization-ID", organizationID)
|
||||
}
|
||||
res, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(res.Body, 4<<20))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
e := &APIError{Status: res.StatusCode, Code: "http_error"}
|
||||
_ = json.Unmarshal(data, e)
|
||||
if e.Code == "" {
|
||||
e.Code = "http_error"
|
||||
}
|
||||
if e.RetryAfter == 0 {
|
||||
e.RetryAfter, _ = strconv.Atoi(res.Header.Get("Retry-After"))
|
||||
}
|
||||
return e
|
||||
}
|
||||
if out == nil || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(data, out); err != nil {
|
||||
return fmt.Errorf("as213905: decode response: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func withTrafficQuery(path string, q *TrafficQuery) string {
|
||||
if q == nil {
|
||||
return path
|
||||
}
|
||||
v := url.Values{}
|
||||
if q.Days > 0 {
|
||||
v.Set("days", strconv.Itoa(q.Days))
|
||||
}
|
||||
if q.From != "" {
|
||||
v.Set("from", q.From)
|
||||
}
|
||||
if q.To != "" {
|
||||
v.Set("to", q.To)
|
||||
}
|
||||
if len(v) > 0 {
|
||||
return path + "?" + v.Encode()
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (c *Client) Organization(ctx context.Context) (Organization, error) {
|
||||
var r struct {
|
||||
Organization Organization `json:"organization"`
|
||||
}
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/organization", "", nil, &r)
|
||||
return r.Organization, err
|
||||
}
|
||||
func (c *Client) Invoices(ctx context.Context) ([]Invoice, error) {
|
||||
var r struct {
|
||||
Invoices []Invoice `json:"invoices"`
|
||||
}
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/invoices", "", nil, &r)
|
||||
return r.Invoices, err
|
||||
}
|
||||
func (c *Client) Services(ctx context.Context) ([]Service, error) {
|
||||
var r struct {
|
||||
Services []Service `json:"services"`
|
||||
}
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/services", "", nil, &r)
|
||||
return r.Services, err
|
||||
}
|
||||
func (c *Client) Service(ctx context.Context, id string) (Service, error) {
|
||||
var r struct {
|
||||
Service Service `json:"service"`
|
||||
}
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/services/"+url.PathEscape(id), "", nil, &r)
|
||||
return r.Service, err
|
||||
}
|
||||
func (c *Client) ServiceMetrics(ctx context.Context, id, target, metricRange string) (MetricSeries, error) {
|
||||
q := url.Values{}
|
||||
if target != "" {
|
||||
q.Set("target", target)
|
||||
}
|
||||
if metricRange != "" {
|
||||
q.Set("range", metricRange)
|
||||
}
|
||||
path := "/api/v1/services/" + url.PathEscape(id) + "/metrics"
|
||||
if len(q) > 0 {
|
||||
path += "?" + q.Encode()
|
||||
}
|
||||
var r MetricSeries
|
||||
err := c.do(ctx, http.MethodGet, path, "", nil, &r)
|
||||
return r, err
|
||||
}
|
||||
func (c *Client) PowerService(ctx context.Context, id, action string) error {
|
||||
return c.do(ctx, http.MethodPost, "/api/v1/services/"+url.PathEscape(id)+"/power", "", map[string]string{"action": action}, nil)
|
||||
}
|
||||
func (c *Client) Traffic(ctx context.Context, q *TrafficQuery) (Traffic, error) {
|
||||
var r Traffic
|
||||
err := c.do(ctx, http.MethodGet, withTrafficQuery("/api/v1/traffic", q), "", nil, &r)
|
||||
return r, err
|
||||
}
|
||||
func (c *Client) Tunnels(ctx context.Context) ([]Tunnel, error) {
|
||||
var r struct {
|
||||
Tunnels []Tunnel `json:"tunnels"`
|
||||
}
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/tunnels", "", nil, &r)
|
||||
return r.Tunnels, err
|
||||
}
|
||||
func (c *Client) CreateTunnel(ctx context.Context, input CreateTunnelRequest) (Tunnel, error) {
|
||||
var r struct {
|
||||
Tunnel Tunnel `json:"tunnel"`
|
||||
}
|
||||
err := c.do(ctx, http.MethodPost, "/api/v1/tunnels", "", input, &r)
|
||||
return r.Tunnel, err
|
||||
}
|
||||
func (c *Client) DeleteTunnel(ctx context.Context, id string) error {
|
||||
return c.do(ctx, http.MethodDelete, "/api/v1/tunnels/"+url.PathEscape(id), "", nil, nil)
|
||||
}
|
||||
func (c *Client) TunnelTraffic(ctx context.Context, id string, q *TrafficQuery) (Traffic, error) {
|
||||
var r Traffic
|
||||
err := c.do(ctx, http.MethodGet, withTrafficQuery("/api/v1/tunnels/"+url.PathEscape(id)+"/traffic", q), "", nil, &r)
|
||||
return r, err
|
||||
}
|
||||
func (c *Client) Geofeeds(ctx context.Context, org string) (GeofeedList, error) {
|
||||
var r GeofeedList
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/geofeeds", org, nil, &r)
|
||||
return r, err
|
||||
}
|
||||
func (c *Client) CreateGeofeed(ctx context.Context, org, name string) (GeofeedList, error) {
|
||||
var r GeofeedList
|
||||
err := c.do(ctx, http.MethodPost, "/api/v1/geofeeds", org, map[string]string{"name": name}, &r)
|
||||
return r, err
|
||||
}
|
||||
func (c *Client) Geofeed(ctx context.Context, org, id string) (Geofeed, error) {
|
||||
var r struct {
|
||||
Geofeed Geofeed `json:"geofeed"`
|
||||
}
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/geofeeds/"+url.PathEscape(id), org, nil, &r)
|
||||
return r.Geofeed, err
|
||||
}
|
||||
func (c *Client) RenameGeofeed(ctx context.Context, org, id, name string) error {
|
||||
return c.do(ctx, http.MethodPatch, "/api/v1/geofeeds/"+url.PathEscape(id), org, map[string]string{"name": name}, nil)
|
||||
}
|
||||
func (c *Client) DeleteGeofeed(ctx context.Context, org, id string) error {
|
||||
return c.do(ctx, http.MethodDelete, "/api/v1/geofeeds/"+url.PathEscape(id), org, nil, nil)
|
||||
}
|
||||
func (c *Client) ReplaceGeofeedRecords(ctx context.Context, org, id string, records []GeofeedRecord) error {
|
||||
return c.do(ctx, http.MethodPut, "/api/v1/geofeeds/"+url.PathEscape(id)+"/records", org, map[string]any{"records": records}, nil)
|
||||
}
|
||||
func (c *Client) RotateGeofeedURL(ctx context.Context, org, id string) (GeofeedList, error) {
|
||||
var r GeofeedList
|
||||
err := c.do(ctx, http.MethodPost, "/api/v1/geofeeds/"+url.PathEscape(id)+"/rotate", org, nil, &r)
|
||||
return r, err
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package as213905
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOrganizationAndHeaders(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer secret" {
|
||||
t.Errorf("authorization = %q", got)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"organization":{"id":"o","asn":213905,"name":"ISPLABS","status":"active","commit_kbps":50000}}`))
|
||||
}))
|
||||
defer s.Close()
|
||||
c, err := NewWithOptions("secret", Options{BaseURL: s.URL})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
org, err := c.Organization(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if org.ASN != 213905 {
|
||||
t.Fatalf("asn = %d", org.ASN)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIError(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(403)
|
||||
_, _ = w.Write([]byte(`{"error":"scope_required","message":"need traffic:read"}`))
|
||||
}))
|
||||
defer s.Close()
|
||||
c, _ := NewWithOptions("secret", Options{BaseURL: s.URL})
|
||||
_, err := c.Traffic(context.Background(), nil)
|
||||
e, ok := err.(*APIError)
|
||||
if !ok || e.Code != "scope_required" || e.Status != 403 {
|
||||
t.Fatalf("error = %#v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package as213905
|
||||
|
||||
import "time"
|
||||
|
||||
type Organization struct {
|
||||
ID string `json:"id"`
|
||||
ASN int64 `json:"asn"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CommitKbps int64 `json:"commit_kbps"`
|
||||
}
|
||||
|
||||
type Invoice struct {
|
||||
ID string `json:"id"`
|
||||
Number string `json:"number"`
|
||||
Status string `json:"status"`
|
||||
Currency string `json:"currency"`
|
||||
TotalCents int64 `json:"total_cents"`
|
||||
PeriodFrom *string `json:"period_from,omitempty"`
|
||||
PeriodTo *string `json:"period_to,omitempty"`
|
||||
IssuedAt *time.Time `json:"issued_at,omitempty"`
|
||||
DueAt *time.Time `json:"due_at,omitempty"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Label string `json:"label"`
|
||||
Status string `json:"status"`
|
||||
MonthlyCents int64 `json:"monthly_cents"`
|
||||
NextDueOn *string `json:"next_due_on,omitempty"`
|
||||
Controllable bool `json:"controllable,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Configuration map[string]any `json:"configuration,omitempty"`
|
||||
}
|
||||
|
||||
type MetricPoint struct {
|
||||
Time time.Time `json:"t"`
|
||||
Value *float64 `json:"v,omitempty"`
|
||||
}
|
||||
|
||||
type MetricSeries struct {
|
||||
Metric string `json:"metric"`
|
||||
Target string `json:"target"`
|
||||
Range string `json:"range"`
|
||||
Points []MetricPoint `json:"points"`
|
||||
}
|
||||
|
||||
type Period struct {
|
||||
From time.Time `json:"from"`
|
||||
To time.Time `json:"to"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type Traffic struct {
|
||||
P95Kbps int64 `json:"p95_kbps"`
|
||||
CommitKbps int64 `json:"commit_kbps,omitempty"`
|
||||
OverCommit bool `json:"over_commit,omitempty"`
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
Period Period `json:"period"`
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Location string `json:"location"`
|
||||
PeerEndpoint string `json:"peer_endpoint"`
|
||||
IfName string `json:"ifname,omitempty"`
|
||||
InnerV4 string `json:"inner_v4,omitempty"`
|
||||
InnerV6 string `json:"inner_v6,omitempty"`
|
||||
Announce bool `json:"announce,omitempty"`
|
||||
}
|
||||
|
||||
type TrafficQuery struct {
|
||||
Days int
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
type CreateTunnelRequest struct {
|
||||
PeerEndpoint string `json:"peer_endpoint"`
|
||||
Location string `json:"location"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Announce bool `json:"announce,omitempty"`
|
||||
}
|
||||
|
||||
type Geofeed struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
RecordCount int `json:"record_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Records []GeofeedRecord `json:"records,omitempty"`
|
||||
}
|
||||
|
||||
type GeofeedRecord struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Prefix string `json:"prefix"`
|
||||
CountryCode string `json:"country_code"`
|
||||
RegionCode string `json:"region_code,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
PostalCode string `json:"postal_code,omitempty"`
|
||||
}
|
||||
|
||||
type GeofeedList struct {
|
||||
Geofeeds []Geofeed `json:"geofeeds"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
as213905 "git.datacoria.com/Phylex/as213905-sdk/go/as213905"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := as213905.New(os.Getenv("AS213905_API_KEY"))
|
||||
traffic, err := client.Traffic(context.Background(), &as213905.TrafficQuery{Days: 7})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("p95: %d kbit/s, volume: %.2f GB\n", traffic.P95Kbps, traffic.TotalGB)
|
||||
}
|
||||
Reference in New Issue
Block a user